印象深刻的一道题。
题意描述
在一棵 $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; }