【CF1399E1】 Weights Division (easy version)

  印象深刻的一道题。


题意描述

  在一棵 $n$ 个节点、根为 $1$ 号点的树上,每一条边都有边权。

  每次操作可以使一条边的边权除以 $2$ 并下取整,问最少操作多少次可以让点 $1$ 到各个叶子结点的距离和不超过 $S$。


  首先,很容易想到一个贪心策略:哪条边当前的边权 × 该边出现次数最大,就优先让这条边的边权除以 $2$。

  但是这个策略有漏洞,做法需要进行修改。

  第一步:求出点 $1$ 到各个叶子结点的路径中,每条边出现的次数。

  有一个显而易见的结论:一个点的出现次数,等于这个点到达它的父亲,经过的边的出现次数。

  而题目一开始就说了 $1$ 号点是根,所以从 $1$ 号点开始 dfs,把每条边的边权信息存到较深的节点就可以了(边权也需要传)。

  第二步:每次操作时,找到当前影响最大的边,并将其边权除以 $2$ 下取整,直到距离总和小于 $S$。

  这步也不难,我们可以拿一个堆(或 set 什么的都可以),每次只需要 $log$ 级别的复杂度就可以添加、删除节点。

  为了判断距离总和是否小于 $S$,需要做:

  • 操作前算出所有的边权 × 该边出现次数之和,命名为 $S_0$。
  • 假设当前优先级最大边的边权为 $w$,出现次数为 $c$,那么做一次操作之后,$S_0leftarrow S_0 - left(w - lfloor frac{w}{2} floor ight) imes c$。

  如果使用的是堆,我们就需要设计它的优先级,为此我曾设计过两种做法:

  • 第一种(非正确做法):把当前边权 × 该边出现次数最大的边找出来,边权变为 $frac{ ext{边权 * 该边出现次数}}{2}$

  这个做法的反例如下:

  边权 出现次数
  53 2
  15 7

  虽然 $53 imes 2 > 15 imes 7$,但是 $left(53-lfloorfrac{53}{2} floor ight) imes 2 < left(15-lfloorfrac{15}{2} floor ight) imes 7$,明显后者可以减小的数更大,即操作后作用越大,所以这个做法是错误的。

  • 第二种做法(正确做法):优先级判定应该为,选出当前最大的

$$left( ext{边权}-lfloorfrac{ ext{边权}}{2} floor ight) imes ext{出现次数}$$

  然后,将 边权(注意除法和出现次数无关,不要拿乘积作除法) 除以 $2$ 下取整。

  程序的主体内容就结束了,剩下的就没什么难度了。


复杂度分析

  对于每组数据(因为 $sum n le 10^5$,所以没有影响),我们有 $n$ 个数塞进堆里。

  因为每次操作都是将数字除以 $2$ 下取整,所以把数 $a$ 变为 0 的操作次数只有 $log$ 级别。

  当所有数字变为 $0$,距离和就一定不超过 $S$($S$ 是正整数)。

  所以最多在堆内操作 $n log w$ 次。

  故复杂度就是 $O((n log w) log (n log w))$,在 $3$ 秒时间限制下是可以通过的。


代码

  

#include <cstdio>
#include <queue>
#define N 100010
typedef long long ll;
using namespace std;
 
struct Edge{
    int to, nxt;
    ll val;
}e[N << 1];
int t, n, fir[N], ans, cnt;
ll S, s[N], sum[N], W[N], l, r, SS;
 
struct P{
    int id;                             // 这里设 id 是为了方便查找边的出现次数 
    ll val;
};
priority_queue <P> q;                   // 优先队列 

bool operator < (P a, P b){             // 判断优先级 
    return (a.val - a.val / 2) * s[a.id] < (b.val - b.val / 2) * s[b.id];
}
 
inline void add(int x, int y, ll z){    // 建边 
    e[++cnt].to = y;
    e[cnt].nxt = fir[x];
    e[cnt].val = z;
    fir[x] = cnt;
}
 
ll dfs(int x, int fa){                  // 统计每条边出现次数、以及和边权的乘积 
    ll ss = 0;
    bool bb = 0;
    for(int i = fir[x], y; i; i = e[i].nxt){
        y = e[i].to;
        if(y == fa) continue;           // 对于每次搜索不要返回去搜索父亲,不然死循环 
        bb = 1;                         // 确定这不是叶子节点 
        ss += dfs(y, x);
        W[y] = e[i].val;                // 把边的信息化成点的信息,方便之后统计 
        sum[y] = s[y] * e[i].val;        
    }
    if(!bb) return s[x] = 1;            // 是叶子节点就要把自己信息带上去 
    else return s[x] = ss;              // 不然只需搬运下面的节点的信息 
}

void CLEAR(){
    for(int i = 1; i <= n; i++)
        fir[i] = s[i] = sum[i] = W[i] = 0;
    while(!q.empty())                   // 优先队列清空 
        q.pop();
}
 
int main(){
 
    scanf("%d", &t);
    while(t--){
        scanf("%d %lld", &n, &S);
        cnt = ans = 0, SS = 0;
        for(int i = 1, u, v; i < n; i++){
            ll w;
            scanf("%d %d %lld", &u, &v, &w);
            add(u, v, w), add(v, u, w);
        }
        dfs(1, 0);
        for(int i = 2; i <= n; i++)                 // 从 2 开始,因为 1 没有通往父亲的边 
            q.push((P){i, W[i]}), SS += sum[i];
        int xx;
        ll yy;
        while(SS > S){                              // 一直操作直到结束 
            xx = q.top().id, yy = q.top().val;
            q.pop(), ++ans;                         // 答案累加 
            SS -= yy * s[xx] - (yy / 2) * s[xx];    // 减小的量 
            q.push((P){xx, yy / 2});
        }
        CLEAR();                                    // 初始化 
        printf("%d
", ans);
    }
 
    return 0;
}
原文地址:https://www.cnblogs.com/zengpeichen/p/13547670.html