zoukankan      html  css  js  c++  java
  • 浅谈树上分治算法[国集2019]

    1.点分治 && 边分治

    这两种算法都是用于处理这样一类问题:

    给定一棵树,求有多少条路径满足xxx性质?/ 最xxx的路径是什么?

    例题:给定一棵树,求有多少条路径长度小于等于k?

    题目

    使用点/边分治就可以将暴力枚举两点的(O(n^2))优化到(O(nlog n))

    点分治

    我们改一下这道题 改成求有多少条经过1的路径长度小于等于k

    这个就比较好求了 我们把所有点到1的距离全部装进一个数组(q)里,然后只需要计算(q)数组里有多少对数字相加小于等于即可,这个可以排序后维护双指针计算

    但是这样做是错的。。。

    (q)数组里有(0,2,5),那么(0+2,0+5,2+5)都小于等于(k),求得答案有三个,但是实际上合法的经过(1)的路径只有(1)(2)(1)(3)两条
    我们注意到(2+5)是不合法的 你不能这么走:2->1->3 所以只有 不在(1)的同一个儿子的子树中 的两个点才能相加配对

    怎么处理?也很简单 按上面的方法计算完后 显然是有些不合法的情况 于是再用同样的方法计算(1)的每一个儿子的子树 把儿子的子树里的所有点进行配对 这些配对都是不合法的 减去这些配对的贡献即可

    然后看点分治


    看这个分叉的菊花图 找到它的重心是(1)

    我们可以用上面的方法计算出有多少经过1的路径长度小于等于k,然后我们删除点1

    现在图上还剩下三棵子树 再如法炮制分别找到这三棵子树的重心,然后分别统计过这三个重心的合法路径条数,然后再分别删除这三个重心......

    树的重心有个性质:它每个儿子的子树大小都不会超过(frac{n}{2})

    所以最多会往下递归(log n)层,此题的总复杂度(O(nlog^2 n))

    核心步骤:找到当前子树重心 -> 统计经过重心的路径的贡献 -> 删除重心,递归进入子树

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    typedef long long ll;
    
    inline int read() {
    	int x = 0, f = 1; char ch = getchar();
    	for (; ch > '9' || ch < '0'; ch = getchar()) if (ch == '-') f = -1;
    	for (; ch <= '9' && ch >= '0'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ '0');
    	return x * f;
    }
    
    const int inf = 0x7fffffff;
    int n, k, rt, nowsz, mx;
    int head[40005], pre[80005], to[80005], val[80005], sz; 
    int siz[40005], dis[40005];
    ll ans;
    bool vis[40005];
    
    void init() {
    	memset(head, 0, sizeof(head));
    	memset(vis, 0, sizeof(vis));
    	sz = 0; ans = 0;
    }
    
    inline void addedge(int u, int v, int w) {
    	pre[++sz] = head[u]; head[u] = sz; to[sz] = v; val[sz] = w; 
    	pre[++sz] = head[v]; head[v] = sz; to[sz] = u; val[sz] = w; 
    }
    
    void getsiz(int x, int fa) {
    	siz[x] = 1;
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa || vis[y]) continue;
    		getsiz(y, x);
    		siz[x] += siz[y];
    	}
    }
    
    void getrt(int x, int fa, int tot) {
    	int nowmx = 0;
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa || vis[y]) continue;
    		getrt(y, x, tot);
    		nowmx = max(nowmx, siz[y]);
    	}
    	nowmx = max(nowmx, tot - siz[x]);
    	if (nowmx < mx) {
    		mx = nowmx, rt = x;
    	}
    }
    
    int l, r, q[40005];
    
    void getdis(int x, int fa) {
    	q[++r] = dis[x];
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa || vis[y]) continue;
    		dis[y] = dis[x] + val[i];
    		getdis(y, x);
    	}
    }
    
    ll calc(int x, int d) {
    	l = 1, r = 0;
    	dis[x] = d;
    	getdis(x, 0);
    	sort(q + 1, q + r + 1);
    	ll ret = 0;
    	while (l < r) {
    		if (q[l] + q[r] <= k) {
    			ret += r - l, l++;
    		} else r--;
    	}
    	return ret;
    }
    
    void divide(int x) {
    	ans += calc(x, 0);
    	vis[x] = 1;
    	getsiz(x, 0); 
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (vis[y]) continue;
    		ans -= calc(y, val[i]);
    		mx = inf;
    		getrt(y, 0, siz[y]);
    		divide(rt); 
    	} 
    }
    
    int main() {
    	n = read();
    	init();
    	for (int i = 1; i < n; i++) {
    		int u = read(), v = read(), w = read();
    		addedge(u, v, w);
    	}
    	k = read();
    	mx = inf;
    	getsiz(1, 0);
    	getrt(1, 0, n);
    	divide(rt);
    	printf("%lld
    ", ans);
    	return 0;
    } 
    

    边分治

    上面那道题还有第二种做法,即边分治

    点分治是每次选出中间的重心点,边分治则是选出一条比较靠近中间的边,使得这条边连接的两棵子树中,较大的那棵子树尽可能小

    然后统计完所有经过这条边的路径后把这条边删除,再递归进入左右两棵子树

    这里统计贡献时就不用向上面一样去重了,因为一定是把这条边左边的点和右边的点进行配对,就不存在不合法的情况

    时间复杂度是和点的度数有关的 当度数均为常数时,时间复杂度约为(O(nlog n))

    菊花图怎么办?这里有一个优化 我们想让每个点的度数尽可能小,可以通过新加入一些点和边来减小每个点的度数

    具体地说,如果一个点(x)有多于两个儿子,我们就新建两个点(a,b),把这两个点的父亲都设为(x),然后把(x)的儿子一半给(a),一半给(b)

    如果此时(a,b)的儿子多于两个了,过一会还可以在(a,b)的下面继续建虚点

    这样实际上最后会得到一棵二叉树 总点数依然是(2n)左右的 不过每个点的度数都不会超过3

    统计答案时要注意虚点不能被计入答案

    int siz[400005], ct, mx, sum;
    
    void findct(int x, int fa) {
    	siz[x] = 1;
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa || vis[i>>1]) continue;
    		findct(y, x);
    		siz[x] += siz[y];
    		int now = max(siz[y], sum - siz[y]);
    		if (now < mx) {
    			mx = now;
    			ct = i;
    		}
    	}
    }
    
    int q[2][400005], top[2];
    
    void getdis(int x, int fa, int dis, int o) {
    	if (x <= nn) q[o][++top[o]] = dis;
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa || vis[i>>1]) continue;
    		getdis(y, x, dis + val[i], o);
    	}
    }
    
    ll calc(int v) {
    	sort(q[0] + 1, q[0] + top[0] + 1);
    	sort(q[1] + 1, q[1] + top[1] + 1);
    	int l = 1, r = top[1];
    	ll ret = 0;
    	while (l <= top[0] && r >= 1) {
    		if (q[0][l] + q[1][r] + v <= k) {
    			ret += r;
    			l++;
    		} else r--;
    	}
    	return ret;
    }
    
    void divide(int x, int _siz) {
    	ct = 0, mx = 0x7fffffff, sum = _siz;
    	findct(x, 0);
    	if (!ct) return;
    	int l = to[ct], r = to[ct^1];
    	vis[ct>>1] = 1;
    	top[0] = top[1] = 0;
    	getdis(l, 0, 0, 0); getdis(r, 0, 0, 1);
    	ans += calc(val[ct]);
    	divide(l, siz[to[ct]]); divide(r, _siz - siz[to[ct]]);
    }
    

    例题1:[CTSC2018]暴力写挂

    边分治的例题

    注意到题目给的这个

    [mathrm{depth}(x) + mathrm{depth}(y) - {mathrm{depth}(mathrm{LCA}(x,y))}-{mathrm{depth'}(mathrm{LCA'}(x,y))} ]

    似乎不太好算

    我们把前3项转换一下 发现上面这个式子实际上等于

    [dfrac{1}{2}(mathrm{depth}(x) + mathrm{depth}(y) + mathrm{dis}(x,y) - 2 * {mathrm{depth'}(mathrm{LCA'}(x,y))}) ]

    这样一来,前三项可以通过边分治处理出来,然后最后一项则需要在第二棵树上来计算

    具体地说,我们对第一棵树进行边分治,然后将当前分治边左边的点标为黑点,右边标为白点

    假设一个点(x)到分治边的距离为(mathrm{d}(x)),分治边的长度是(v),那么上面式子的前3项实际上就等于(mathrm{depth}(x) + mathrm{depth}(y) + (mathrm{d}(x) + mathrm{d}(y) + v))

    所以把每个点的点权(mathrm{val}(x))设为(mathrm{depth}(x) + mathrm{d}(x)),然后就可以去处理第二棵树了

    在第二棵树中枚举每个点作为lca,那么现在目标就是找到两个颜色不同,且在两个不同儿子子树里的点使得它们的(mathrm{val})之和最大

    (f[x][0])表示(x)子树中最大的黑点权值,(f[x][1])表示最大白点权值;然后就可以在第二棵树上进行dp来得到最大值 具体dp转移见代码

    但是dp一次是(O(n))的 所以我们还需要在dp之前对第二棵树建虚树 在虚树上dp

    这样总时间复杂度就是(O(nlog^2 n))的 依然会被卡掉。。。

    如果想要(O(nlog n))可以加上欧拉序+ST表求LCA以及基数排序建虚树来强行降低复杂度 这里我只写了个(O(1))求LCA 吸氧后勉强卡过 基数排序什么的表示不懂

    代码实在太长太长了。。。所以放个链接吧 https://www.luogu.com.cn/paste/4qxuvhi5

    思考题:给定两个树,找出两个点x,y,使得第一棵树上x,y的距离和第二棵树上x,y的距离之和最小

    例题2:Juruo and tree components

    没有找到原题 所以自己造了一下数据

    首先进行点分治,然后对于一个分治中心,考虑所有包含它的连通块

    我们先以分治中心为根,处理出当前子树的dfs序;

    然后再进行dfs,对于一个非根的点 (x) 有两种选择:

    • (x) 加入连通块,在新图中从dfn[x]dfn[x]+1连边权为 (v_x) 的边

    • 连通块中不包含 (x) 的子树,在新图中从dfn[x]ed[x]+1 连边权为 (0) 的边

    如果一条边的终点超过当前的总dfs序了,就把这条边连向汇点

    然后这样做一定能建出若干个这样的图 建立超级源点向每个源点连边权为 (0) 的边,建立超级汇点,从每个汇点向超级汇点连边

    此时这个新图应该是一个有 (nlog n) 个点和 (nlog n) 条边的图,其中每条从超级源点到超级汇点的路径都代表原树的一个连通块

    最后在这张新图上跑k短路求出答案

    #include <bits/stdc++.h>
    #define N 100005
    #define M 2000005
    using namespace std;
    
    template<typename T>
    inline void read(T &num) {
    	T x = 0, f = 1; char ch = getchar();
    	for (; ch > '9' || ch < '0'; ch = getchar()) if (ch == '-') f = -1;
    	for (; ch <= '9' && ch >= '0'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ '0');
    	num = x * f;
    }
    
    const int inf = 0x3f3f3f3f;
    int n, k, a[N], siz[N], mn, rt;
    int head[M], pre[M<<1], to[M<<1], val[M<<1], sz;
    int head2[M], pre2[M<<1], to2[M<<1], val2[M<<1], sz2;
    vector<int> e[N];
    bool vis[M];
    
    inline void addedge(int u, int v, int w) {
    	pre[++sz] = head[u]; head[u] = sz; to[sz] = v; val[sz] = w;
    }
    
    inline void addedge2(int u, int v, int w) {
    	pre2[++sz2] = head2[u]; head2[u] = sz2; to2[sz2] = v; val2[sz2] = w;
    }
    
    void getsiz(int x, int fa) {
    	siz[x] = 1;
    	for (auto y : e[x]) {
    		if (y == fa || vis[y]) continue;
    		getsiz(y, x);
    		siz[x] += siz[y];
    	}
    }
    
    void getrt(int x, int fa, int S) {
    	int now = 0;
    	for (auto y : e[x]) {
    		if (y == fa || vis[y]) continue;
    		getrt(y, x, S);
    		now = max(now, siz[y]);
    	}
    	now = max(now, S - siz[x]);
    	if (now < mn) {
    		mn = now; 
    		rt = x;
    	}
    }
    
    int st[N], ed[N], rnk[M], tme = 1, s = 0, t = 1;
    
    void calc(int x, int fa) {
    	st[x] = ++tme; rnk[tme] = x;
    	for (auto y : e[x]) {
    		if (y == fa || vis[y]) continue;
    		calc(y, x);
    	}
    	ed[x] = tme;
    }
    
    void calc2(int x, int fa, int R) {
    	if (x != R) {
    		addedge(st[x], ed[x] + 1 > tme ? t : ed[x] + 1, 0);
    		addedge2(ed[x] + 1 > tme ? t : ed[x] + 1, st[x], 0);
    	}
    	addedge(st[x], st[x] + 1 > tme ? t : st[x] + 1, a[x]);
    	addedge2(st[x] + 1 > tme ? t : st[x] + 1, st[x], a[x]);
    	for (auto y : e[x]) {
    		if (y == fa || vis[y]) continue;
    		calc2(y, x, R);
    	}
    }
    
    void divide(int x) {
    	calc(x, 0);
    	calc2(x, 0, x);
    	addedge(s, st[x], 0);
    	addedge2(st[x], s, 0); 
    	getsiz(x, 0);
    	vis[x] = 1;
    	for (auto y : e[x]) {
    		if (vis[y]) continue;
    		mn = inf;
    		getrt(y, 0, siz[y]);
    		divide(rt);
    	}
    }
    
    int h[M];
    priority_queue<pair<int, int> > qq;
    void dijkstra() {
    	memset(vis, 0, sizeof(vis));
    	memset(h, 0x3f, sizeof(h));
    	qq.push(make_pair(0, 1));
    	h[1] = 0; 
    	while (!qq.empty()) {
    		int x = qq.top().second; qq.pop();
    		if (vis[x]) continue;
    		vis[x] = 1;
    		for (int i = head2[x]; i; i = pre2[i]) {
    			int y = to2[i];
    			if (h[y] > h[x] + val2[i]) {
    				h[y] = h[x] + val2[i];
    				qq.push(make_pair(-h[y], y));
    			}
    		}
    	} 
    } 
    
    struct node{
    	int x, f, g;
    	node(int xx = 0, int ff = 0, int gg = 0): x(xx), f(ff), g(gg) {}
    	bool operator < (const node b) const {
    		return f > b.f;
    	}
    };
    
    priority_queue<node> q;
    int cnt[M];
    int A_star() {
    	q.push(node(0, 0, 0));
    	while (!q.empty()) {
    		node now = q.top(); q.pop();
    		int x = now.x; cnt[x]++;
    		if (x == 1 && cnt[x] == k) {
    			return now.f;
    		}
    		if (cnt[x] > k) continue;
    		for (int i = head[x]; i; i = pre[i]) {
    			int y = to[i];
    			q.push(node(y, now.g + val[i] + h[y], now.g + val[i]));
    		}
    	}
    }
    
    int main() {
    	read(n); read(k);
    	for (int i = 1; i <= n; i++) read(a[i]);
    	for (int i = 1, u, v; i < n; i++) {
    		read(u); read(v);
    		e[u].push_back(v); e[v].push_back(u);
    	}
    	mn = inf; 
    	getsiz(1, 0);
    	getrt(1, 0, n);
    	divide(rt);
    	dijkstra();
    	printf("%d
    ", A_star());
    	return 0;
    }
    

    2. 全局平衡二叉树

    一般在这样一些问题里可以用于优化LCT:树的结构不会发生变化(加边或删边),询问整颗树或子树的答案(询问路径好像也可以做,但是很难写,不如用LCT)

    由此可见,这个"全局平衡二叉树"的可应用区间是很小的

    不过对于一些动态DP问题,显然是满足上面的两个可应用条件的,这时候它的优势就显现出来了:

    例题一 Luogu P4751

    动态DP的模板题 使用大众写法树链剖分时间复杂度为 (O(nlog^2 n))

    使用LCT虽然时间复杂度为 (O(nlog n)),但是常数较大

    注意到每次询问都是询问根节点信息,而且树的形态不会改变,所以可以用全局平衡二叉树,时间复杂度 (O(nlog n)),常数小,行!

    全局平衡二叉树在解决动态DP问题时与LCT和树剖各有相似之处:

    和树剖一样,都是将轻儿子和重链的信息分开维护(LCT是不是也是啊),然后在重链上合并

    和LCT一样,建出来的新树也是由若干棵二叉树通过轻边相连,每棵二叉树代表一条重链

    和LCT不一样的地方则是:全局平衡二叉树不再需要Splay那样的旋转 而是一棵静态的树

    既然它是静态的 那我们自然希望它的高度越小越好 所以建树时我们就希望把它的高度控制在 (log n) 左右

    这是一棵树对应的全局平衡二叉树的例子 粗边代表重边 可以发现它和LCT长得很像

    如何建树?我们设 (siz[x]) 表示 (x) 的子树大小,(son[x])(x) 的重儿子

    (lsiz[x]=siz[x]-siz[son[x]]) 表示 (x) 和它所有轻子树的大小之和,令 (lsiz[x]) 为每个点的点权

    那么对于每条重链对应的二叉树,我们找出这段重链中的一个点,使得它左边的点权和与它右边的点权和之间的差距较小,把它作为二叉树的根,然后向左右儿子递归

    可以证明,这样建出的全局平衡二叉树树高是 (log n)

    建树代码如下:

    int build2(int l, int r) {
    	if (l > r) return 0;
    	int sum = 0;
    	for (int i = l; i <= r; i++) sum += lsiz[stk[i]]; 
    	for (int i = l, now = lsiz[stk[l]]; i <= r; i++, now += lsiz[stk[i]]) {
                //将每个点的点权设为lsiz[x],找到位于中间的点,作为当前二叉树的根
    		if (now * 2 >= sum) {
    			int lc = build2(l, i - 1), rc = build2(i + 1, r); //递归建出左右儿子
    			ch[stk[i]][0] = lc; ch[stk[i]][1] = rc;
    			fa[lc] = fa[rc] = stk[i];
    			pushup(stk[i]); //pushup在后面讲
    			return stk[i];
    		}
    	}
    }
    
    int build(int x) { //建出x的全局平衡二叉树 (x是一条重链的开头)
    	for (int y = x; y; y = son[y]) {
    		vis[y] = 1; //将当前重链上的点打上标记
    	}
    	for (int y = x; y; y = son[y]) {
    		for (int i = head[y]; i; i = pre[i]) {
    			int z = to[i]; //遍历每个和当前重链上任意一点相连的轻儿子
    			if (!vis[z]) { //z为轻儿子,一定是另外一条重链的开头
    				int rtz = build(z);
    				fa[rtz] = y; 
                                  //将下面那棵树的根节点的父亲设为y
    			}
    		}
    	}
    	top = 0;
    	for (int y = x; y; y = son[y]) {
    		stk[++top] = y; //提取出以x开头的重链
    	}
    	int ret = build2(1, top); //建出这棵重链的二叉树
    	return ret;
    }
    

    众所周知,动态DP是用矩阵乘法来维护的,这里我们一样通过矩阵乘法来维护DP值

    这道题具体的转移矩阵就不写了,可以自行去看题解

    类似LCT维护子树信息的思路,我们对每个点维护两个矩阵 (now)(now2)

    其中 (now) 存储的是自己和所有轻子树的信息,而 (now2) 则是 (now) 乘上左右儿子的信息

    pushup操作:

    inline void pushup(int x) {
    	now2[x] = now[x];
    	if (ch[x][0]) {
    		now2[x] = now2[ch[x][0]] * now2[x];
    	}
    	if (ch[x][1]) {
    		now2[x] = now2[x] * now2[ch[x][1]];
    	}
    }
    

    每次进行修改操作x v时,先修改 (x)(now) 矩阵中对应的信息,然后暴力向上跳父亲

    如果父亲连向自己的边是重边,就可以直接pushup,否则是轻边,需要修改父亲的 (now) 矩阵信息

    修改也很简单 先将 (fa[x]) 的矩阵减去原先 (x) 的贡献,再加上 (x) 修改后的贡献即可

    以此题为例:

    void update(int x, int v) {
    	now[x].m[1][0] += v - a[x];
    	a[x] = v;
    	for (int y = x; y; y = fa[y]) {
    		if (fa[y] && ch[fa[y]][0] != y && ch[fa[y]][1] != y) { //轻边
    			now[fa[y]].m[0][0] -= max(now2[y].m[0][0], now2[y].m[1][0]); 
    			now[fa[y]].m[0][1] = now[fa[y]].m[0][0];
    			now[fa[y]].m[1][0] -= now2[y].m[0][0];
                            //减去旧的信息
    			pushup(y);
    			now[fa[y]].m[0][0] += max(now2[y].m[0][0], now2[y].m[1][0]);
    			now[fa[y]].m[0][1] = now[fa[y]].m[0][0];
    			now[fa[y]].m[1][0] += now2[y].m[0][0];
                            //加上新的信息
    		} else pushup(y); //重边
    	}
    }
    

    修改完后,根节点的 (now2) 矩阵就是最终答案

    由于全局平衡二叉树的树高是 (log n) 的,所以一次修改操作的时间复杂度即为 (O(log n))

    主要的难点在于如何建树,修改操作与树剖及LCT区别不大

    只要学会建树,代码难度其实远小于树剖或LCT

    #include <bits/stdc++.h>
    #define N 100005
    using namespace std;
    
    template<typename T>
    inline void read(T &num) {
    	T x = 0, f = 1; char ch = getchar();
    	for (; ch > '9' || ch < '0'; ch = getchar()) if (ch == '-') f = -1;
    	for (; ch <= '9' && ch >= '0'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ '0');
    	num = x * f; 
    }
    
    int n, m, rt, a[N], head[N], pre[N<<1], to[N<<1], sz; 
    int siz[N], lsiz[N], son[N], fa[N], ch[N][2]; 
    int f[N][2], g[N][2];
    
    struct matrix{
    	int m[2][2];
    	matrix() {
    		m[0][0] = m[1][1] = m[0][1] = m[1][0] = 0;
    	}
    	matrix operator * (const matrix b) const {
    		matrix c;
    		c.m[0][0] = max(m[0][0] + b.m[0][0], m[0][1] + b.m[1][0]);
    		c.m[0][1] = max(m[0][0] + b.m[0][1], m[0][1] + b.m[1][1]);
    		c.m[1][0] = max(m[1][0] + b.m[0][0], m[1][1] + b.m[1][0]);
    		c.m[1][1] = max(m[1][0] + b.m[0][1], m[1][1] + b.m[1][1]);
    		return c;
    	}
    } now[N], now2[N];
    
    inline void addedge(int u, int v) {
    	pre[++sz] = head[u]; head[u] = sz; to[sz] = v;
    	pre[++sz] = head[v]; head[v] = sz; to[sz] = u;
    }
    
    void dfs(int x, int fa) {
    	siz[x] = 1;
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa) continue;
    		dfs(y, x);
    		siz[x] += siz[y];
    		if (!son[x] || siz[son[x]] < siz[y]) son[x] = y;
    	}
    	lsiz[x] = siz[x] - siz[son[x]];
    }
    
    void dfs2(int x, int fa) {
    	f[x][0] = g[x][0] = 0;
    	f[x][1] = g[x][1] = a[x];
    	if (son[x]) {
    		dfs2(son[x], x);
    		f[x][0] += max(f[son[x]][0], f[son[x]][1]);
    		f[x][1] += f[son[x]][0];
    	}
    	for (int i = head[x]; i; i = pre[i]) {
    		int y = to[i];
    		if (y == fa || y == son[x]) continue;
    		dfs2(y, x);
    		f[x][0] += max(f[y][0], f[y][1]);
    		f[x][1] += f[y][0];
    		g[x][0] += max(f[y][0], f[y][1]);
    		g[x][1] += f[y][0];
    	} 
    }
    
    int stk[N], top;
    bool vis[N];
    
    inline void pushup(int x) {
    	now2[x] = now[x];
    	if (ch[x][0]) {
    		now2[x] = now2[ch[x][0]] * now2[x];
    	}
    	if (ch[x][1]) {
    		now2[x] = now2[x] * now2[ch[x][1]];
    	}
    }
    
    int build2(int l, int r) {
    	if (l > r) return 0;
    	int sum = 0;
    	for (int i = l; i <= r; i++) sum += lsiz[stk[i]];
    	for (int i = l, now = lsiz[stk[l]]; i <= r; i++, now += lsiz[stk[i]]) {
    		if (now * 2 >= sum) {
    			int lc = build2(l, i - 1), rc = build2(i + 1, r);
    			ch[stk[i]][0] = lc; ch[stk[i]][1] = rc;
    			fa[lc] = fa[rc] = stk[i];
    			pushup(stk[i]);
    			return stk[i];
    		}
    	}
    }
    
    int build(int x) {
    	for (int y = x; y; y = son[y]) {
    		vis[y] = 1;
    	}
    	for (int y = x; y; y = son[y]) {
    		for (int i = head[y]; i; i = pre[i]) {
    			int z = to[i];
    			if (!vis[z]) { //z为轻儿子 
    				int rtz = build(z);
    				fa[rtz] = y;
    			}
    		}
    	}
    	top = 0;
    	for (int y = x; y; y = son[y]) {
    		stk[++top] = y;
    	}
    	int ret = build2(1, top);
    	return ret;
    }
    
    void update(int x, int v) {
    	now[x].m[1][0] += v - a[x];
    	a[x] = v;
    	for (int y = x; y; y = fa[y]) {
    		if (fa[y] && ch[fa[y]][0] != y && ch[fa[y]][1] != y) {
    			now[fa[y]].m[0][0] -= max(now2[y].m[0][0], now2[y].m[1][0]);
    			now[fa[y]].m[0][1] = now[fa[y]].m[0][0];
    			now[fa[y]].m[1][0] -= now2[y].m[0][0];
    			pushup(y);
    			now[fa[y]].m[0][0] += max(now2[y].m[0][0], now2[y].m[1][0]);
    			now[fa[y]].m[0][1] = now[fa[y]].m[0][0];
    			now[fa[y]].m[1][0] += now2[y].m[0][0];
    		} else pushup(y);
    	}
    }
    
    int main() {
    	read(n); read(m);
    	for (int i = 1; i <= n; i++) read(a[i]);
    	for (int i = 1, u, v; i < n; i++) {
    		read(u); read(v);
    		addedge(u, v);
    	}
    	dfs(1, 0); dfs2(1, 0);
    	for (int i = 1; i <= n; i++) {
    		now[i].m[0][0] = now[i].m[0][1] = g[i][0];
    		now[i].m[1][0] = g[i][1]; now[i].m[1][1] = -0x3f3f3f3f;
    	}
    	rt = build(1);
    	for (int i = 1, x, y; i <= m; i++) {
    		read(x); read(y);
    		update(x, y);
    		printf("%d
    ", max(now2[rt].m[0][0], now2[rt].m[1][0]));
    	}
    	return 0;
    }
    
  • 相关阅读:
    lucene1.0.1写入分析
    esm数据迁移
    datadog入门
    elasticsearch datehistogram聚合
    cookie实战
    泛型编程
    lucene分析
    2020年12月阅读文章
    迭代
    lucene搜索
  • 原文地址:https://www.cnblogs.com/ak-dream/p/13364887.html
Copyright © 2011-2022 走看看