DS(3):树上问题的一些杂谈

一些碎碎念

本文内容极为浅显和模板,欢迎朋友们补充题目使本文变得完善!谢谢大家!

树上问题有很多,常用的手法有:

  1. 对于树上统计,最常用的手法是 dsu on tree 或者点分治。dsu on tree 的本质我认为是以类似于轻重链剖分的方式来简化信息加入的次数,比较暴力,所以感觉啥都能统计统计。点分治通常用于对于联通块或者序列的统计,统计的信息可能略有局限性,但是也因为和树的形态联系紧密(尤其是有的时候可以处理联通块相关),比如有的时候也会用到优化建图中(无敌了这个东西我似乎见过两次但是一次代码都没写,一个是 zr 模拟赛敬爱的将军的题,一个是 PA 的 miny)。点分治的结构抽象出来是点分树,在点分树上可以做一些特别的事情。dsu on tree 一般以 LCA 身份在子树内统计,点分治则以“路径/联通块中某个点身份”被统计。

  2. 维护树上链信息最通用的是 LCT 和轻重链剖分。LCT 我已经忘掉了,反正也被考纲踢出去了。轻重链剖分伟大无需多言。

  3. 维护子树信息的,一般采用 dfn 序,拍到序列上统计。若序列不方便合并,直接在树上合并会采用合并数据结构(线段树,trie 或者可并堆比较常见)。

  4. 若信息满足可减法性。如果是子树问题,利用一种类欧拉序。如果维护的信息满足可减性,那么就在 dfs 进入子树的时候记录,出去的时候把这部分信息减去。本质上是在欧拉序上差分,有奇效。那么对于路径问题也可以尝试使用可持久化数据结构来做减法以此合并。

  5. 别忘了这两个基础的东西:树上倍增,一般来说对于偏静态的问题,它可以很好的维护信息满足结合律的情况,并且拥有随时加入叶子只需要 \(O(\log n)\) 的代价,这是只有 LCT 才能做到的。另一个则是我们树上差分。

  6. 其它杂项有动态 dp(常常应用在树上),全局平衡二叉树我还不会。应用比较局限的有虚树(除了自身的用途也可以当作一个思考工具),还有长链剖分,这一部分线先咕咕咕。

当然,这篇文章将会从零开始。

以及一些维护信息的常用手法:

  1. 通常而言,整体刻画树上路径还是会选择点分治。
  2. 对于很多树上问题,需要满足 \(u, v\) 在不同子树,这个时候选择直接统计子树内的贡献,然后减去不合法的贡献。
  3. 链上统计更多还是要考虑在 LCA 处统计。当然也有固定一个端点求其它可能合法点。
  4. 很多时候对于路径上包含 \(u\) 点,可以在路径起点终点分别打标记,然后再 \(fat(LCA(s, t))\) 处再打一个抵消标记,含 \(u\) 的路径就是子树中的标记。

LCA

市面上常见的 LCA 有:

  1. 倍增。在 2025 年倍增已经很少单独作为求解 LCA 的手段了,除非你要顺手维护一些比较独特的信息,顺手用倍增求解。
  2. 树剖。单纯求 LCA 除了预处理 \(O(n)\) 几乎在当代毫无优势,小常数能小过我们 \(O(1)\) 吗?
  3. 欧拉序。时代的眼泪。
  4. tarjan。没学过,感觉没用。
  5. dfn 序。\(O(n\log n)\) 预处理 \(O(1)\) 查询只需要求解 dfn 和写一个 ST 即可,写起来清爽干净常数及其小可以说是真正的超模数值怪。这里是教程
  6. 斜二进制:从理解斜二倍增,到放弃斜二倍增

LCA 是解决很多树上链问题的基础。

dsu on tree

dsu on tree 的流程是这样的:

  1. 求解以 \(u\) 为根的子树的信息,首先对于每个儿子子树求解一下。
  2. 保留 \(u\) 重儿子子树的信息,轻儿子子树信息直接往重儿子子树里面合并。

我们证明一个树上信息只会被合并 \(O(\log n)\) 次:对于点 \(x\),它在它的祖先 \(u\) 处被合并,当且仅当 \(u\) 并不在 \(u\) 重儿子子树内,即此时 \(u\)\(x\) 的儿子 \(v\) 为轻儿子。因为一个点到根的重链数量不超过 \(O(\log n)\),所以轻儿子数量也不超过 \(O(\log n)\),合并次数就是这个量级。

写法有两种,一种是把信息挂到重链底部,然后直接合并。我非常喜欢这么写。另一种写法是每次清空轻儿子信息,保留重儿子信息,然后再暴力扫描轻儿子加入。

一些例子:

CF600E

对应着上面说的第二种,如何直接求解子树内颜色数量?最后求解重儿子保留颜色桶,然后暴力扫描轻儿子加入颜色桶,直接统计即可。

P5327

对着 LCA 统计有点难啊,考虑对于一个点统计它能到的所有点:对于 \(u\) 点,能到的就是含 \(u\) 路径取并的联通块大小。判定 \(u\) 在路径 \((s, t)\) 上,可以在 \(s, t\) 处分别打一个标记,然后在 \(fa[LCA(s, t)]\) 处撤销这个标记,那么 \(u\) 所在联通块点集合就是 dfs 这个子树后所保留的标记。用 dsu on tree 的合并方法就可以合并出所有的关键点集合,求解关键点集合的联通块大小用 dfn 序的结论用一个 multiset 即可。

参考代码

Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5, L = 18;
int n, m;
vector <int> gra[N + 10];
namespace lca {
	int dfn[N + 10], C = 0, dep[N + 10], fat[N + 10];
	int mind[N + 3][L + 3];
	void dfs(int u, int fa) {
		fat[u] = fa;
		dfn[u] = ++C;
		mind[C][0] = u;
		dep[u] = dep[fa] + 1;
		for(int i = 0; i < gra[u].size(); i++) {
			int v = gra[u][i];
			if(v == fa) continue;
			dfs(v, u);
		}
	}
	void prep() {
		dfs(1, 0);
		for(int j = 1; j <= L; j++)
			for(int i = 1; i + (1 << j) - 1 <= n; i++)
				if(dep[mind[i][j - 1]] < dep[mind[i + (1 << (j - 1))][j - 1]])
					mind[i][j] = mind[i][j - 1];
				else mind[i][j] = mind[i + (1 << (j - 1))][j - 1];
	}
	int lca(int u, int v) {
		if(u == v) return u;
		if(dfn[v] < dfn[u]) swap(u, v);
		int L = __lg(dfn[v] - dfn[u]);
		if(dep[mind[dfn[u] + 1][L]] < dep[mind[dfn[v] - (1 << L) + 1][L]])
			return fat[mind[dfn[u] + 1][L]];
		else return fat[mind[dfn[v] - (1 << L) + 1][L]]; 
	}
} 
int dist(int u, int v) {
	return lca::dep[u] + lca::dep[v] - 2 * lca::dep[lca::lca(u, v)];
}

vector <int> ad[N + 3], de[N + 3];
int siz[N + 10], son[N + 10];
void dfs1(int u, int fa) {
	siz[u] = 1;
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa) continue;
		dfs1(v, u);
		siz[u] += siz[v];
		if(siz[son[u]] <= siz[v])
			son[u] = v;
	}
}
int bot[N + 3], top[N + 3];
void dfs2(int u, int fa, int t) {
	top[u] = t;
	bot[t] = u;//重链底部
	if(son[u]) 
		dfs2(son[u], u, t);
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa || v == son[u]) continue;
		dfs2(v, u, v);
	}
}

using namespace lca;
struct nd {
	int x;
	bool operator < (const nd &other) const {
		return dfn[x] < dfn[other.x];
	}
};
int qx[N + 3], qy[N + 3];
ll ans = 0;
struct ds {//chain 是可能的链编号,dot 是点,sum 是联通块大小
	set <int> chain;	
	multiset <nd> dot;
	int sum;
	void clear() {
		dot.clear();
		chain.clear();
		sum = 0;
	}
	void print() {
		cout << "sum:" << sum << endl;
		cout << "chain:";
		for(set <int>::iterator it = chain.begin(); it != chain.end(); it++)
			cout << (*it) << ' ';
			cout << endl;
			cout << "dot:";
		for(set <nd>::iterator it = dot.begin(); it != dot.end(); it++)
			cout << (*it).x << ' ';
		cout << endl;
	}
	void dotins(int u) {
		if(dot.empty()) {
			dot.insert((nd){u});
			return ;
		}
		
		multiset <nd>::iterator pre;
		multiset <nd>::iterator nxt = dot.lower_bound((nd){u});
		if(nxt != dot.end()) {
			if(nxt == dot.begin()) pre = dot.end(), pre--;
			else pre = nxt, pre--;
		} 
		else {
			pre = nxt, pre--;
			nxt = dot.begin();
		}
		int p = (*pre).x, t = (*nxt).x;
		sum = sum - dist(p, t) + dist(p, u) + dist(u, t);
		dot.insert((nd){u});
	}
	void dotdel(int u) {
		multiset <nd>::iterator dt = dot.lower_bound((nd){u});
		dot.erase(dt);
		
		if(dot.empty()) return ;
		multiset <nd>::iterator pre;
		multiset <nd>::iterator nxt = dot.lower_bound((nd){u});
		if(nxt != dot.end()) {
			if(nxt == dot.begin()) pre = dot.end(), pre--;
			else pre = nxt, pre--;
		} 
		else {
			pre = nxt, pre--;
			nxt = dot.begin();
		}
		int p = (*pre).x, t = (*nxt).x;
		sum = sum + dist(p, t) - dist(p, u) - dist(u, t);
	}
} T[N + 5]; 

void dfs3(int u, int fa) {
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa) continue;
		dfs3(v, u);
	}
	
	int rest = 0;
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa || v == son[u]) continue;
		
		for(set <int>::iterator it = T[bot[top[v]]].chain.begin();
		it != T[bot[top[v]]].chain.end(); it++) {//轻儿子直接暴力合并
			int w = (*it);
			set <int>::iterator itt = T[bot[top[u]]].chain.lower_bound(w);
			if(!(itt != T[bot[top[u]]].chain.end() && (*itt) == w)) {
				T[bot[top[u]]].chain.insert(w);
				T[bot[top[u]]].dotins(qx[w]);
				T[bot[top[u]]].dotins(qy[w]);
			}
		}
	}
	//然后合并 u 点上的标记
	for(int i = 0; i < ad[u].size(); i++) {
		int w = ad[u][i];
		set <int>::iterator itt = T[bot[top[u]]].chain.lower_bound(w);
		if(!(itt != T[bot[top[u]]].chain.end() && (*itt) == w)) {
			T[bot[top[u]]].chain.insert(w);
			T[bot[top[u]]].dotins(qx[w]);
			T[bot[top[u]]].dotins(qy[w]);
		}
	}
	for(int i = 0; i < de[u].size(); i++) {
		int w = de[u][i];
		set <int>::iterator itt = T[bot[top[u]]].chain.lower_bound(w);
		if(itt != T[bot[top[u]]].chain.end() && (*itt) == w) {
			T[bot[top[u]]].chain.erase(itt);
			T[bot[top[u]]].dotdel(qx[w]);
			T[bot[top[u]]].dotdel(qy[w]);
		}
	}
	
//	cout << u << endl;
	ans += T[bot[top[u]]].sum;
//	T[bot[top[u]]].print();
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1, x, y; i < n; i++) {
		cin >> x >> y;
		gra[x].push_back(y);
		gra[y].push_back(x);
	}
	prep();
	
	for(int i = 1; i <= m; i++) {
		cin >> qx[i] >> qy[i];
		int l = fat[lca::lca(qx[i], qy[i])];
		ad[qx[i]].push_back(i);
		ad[qy[i]].push_back(i);
		de[l].push_back(i);//加入标记
		
		siz[qx[i]]++, siz[qy[i]]++, siz[l]--;
	}
	
	dfs1(1, 0);
	dfs2(1, 0, 1);
	dfs3(1, 0);
	cout << ans / 4ll << endl;
}

P7880

结合一点支配对

考虑 \(u\) 被计算的次数:

  1. \(i, j\) 均在轻子树,那么对于 \(i\) 有且仅有唯一 \(j\)
  2. \(i, j\) 中有一个在重子树,那么至少有一个在轻子树,所以在轻子树里面统计。

最后可能贡献的点对这样就是轻子树之和同阶,是 \(O(n\log n)\) 的,数颜色做一个二维数点即可。

点分治

点分治的核心是:对于路径统计/联通块统计,考虑 \(u\) 是否在联通块/路径内。如果在,那么就以 \(u\) 为根统计,如果不在,那么就删除 \(u\),合法的对象一定在剩下的逆序对中。如果每次取重心删除,那么这样只会进行 \(O(\log n)\) 层。暴力扫描每一层的所有元素时间复杂度仍然是 \(O(n\log n)\) 的。

通常情况下对于所有树上满足某种条件路径,会选择利用点分治来整体地刻画它。

路径方面

一些模板

P3806 模板点分治

P4178 Tree

P4149 Race

P2634 聪聪可可

这些题目都很模板啊。

P9058

结合支配对。

对于这种问题直接考虑用支配对思想尽可能化简可能的答案点对集合,然后做二维数点。用点分治刻画路径长度,找到中转点 \(u\) 和它的子树中的点,那么构造成一个序列 \((x, d, b)\) 代表 \(x\)\(u\) 距离为 \(b\),属于 \(u\)\(b\) 号子树。不过发现如果 \(b_x=b_y\) 也在这个地方作为最优候选之一那么说明在真正的 LCA 处贡献它会更小,在这里贡献也没有关系。所以可以把 \(b\) 不相等的约束丢掉。

考虑 \((l, r)\) 为有效点对的必要条件,那么一定有 \(k\in [l+1, r - 1], d_k \ge \max(d_l, d_r)\)\(\min_{k \in[l+1, r - 1]}d_k \ge \max(d_l, d_r)\)

讨论一下:\(d_l \ge d_r\)\(\min_{k\in[l+1, r - 1]}d_k \ge d_l\),所以向右找到第一个小于 \(d_l\)\(d_r\) 即可。\(d_l < d_r\) 同理。点对数量锐减到 \(O(n\log n)\) 后做二维数点。

支配对还是要考虑充要条件呀。

P12692

这题应该有一车做法,这里给出一个比较笨蛋的做法,目前是最优解 rk2(截止至 2025/6/13)

感谢呕象的文章。让我学习了这类问题的较为通用的一个方法。

对于 k 优问题往往是:从一个(或者多个)起始最优状态开始,通过一个特定的方法逐步向外扩展次优状态。要求这样拓展时没有重复没有遗漏即可。回到这道题来。树上统计问题可以以点分树为框架进行,我们可以将过点 \(u\) 的最大路径扔到堆里面去。每次取出最大值令是过 \(u\) 的第 \(k\) 大路径,那么我们就将第 \((k + 1)\) 大路径给丢进堆里。很显然,这是满足上面不重不漏,并且一定是次优的状态的原则。

问题转化成了如何快速求出对于 \(u\) 求出第 \(1\sim k\) 大的路径。点分治的时候对于 \(u\) 求出 \(u\) 到子树中每个点的距离 \(d\) 和这个点在 \(u\) 的哪个子树内 \(c\)。抽象出来我们的问题就是求出权值前 \(k\) 大的点对 \((i, j)\),点对权值定义为 \(d_i + d_j\),点对要求是 \(c_i\not= c_j\)

这似乎并不是很困难。仍然是 \(k\) 优问题,按照 \(d\) 从大到小排序所有点,一个点对 \((i, j)\) 即可刻画一个状态。拓展时我们强制令 \(j\) 改变,这样可以保证不重不漏。对于每个点 \(i\) 求出最小的 \(j\)\(j>i, c_j\not= c_i\),将点对 \((i, j)\) 扔进堆里。每次取出后拓展 \(j\) 就是求出 \((j, lim]\) 内第一个和 \(c_i\) 不同的位置。这还是比较容易的,具体细节请见代码。

代码实现并不精细,有点丑陋,大家见谅啦 qwq

#include <bits/stdc++.h>
#define il inline
using namespace std;
const int N = 5e4;
const int inf = 6e8;
il int read() {
	int k = 0;
	char ch = getchar();
	while(ch < '0' || ch > '9') ch = getchar();
	while(ch >= '0' && ch <= '9') {
		k = k * 10 + ch - '0';
		ch = getchar();
	}
	return k;
}
il void write(int x) {
	if(!x) {
		putchar('0');
		return ;
	}
	int tmp[20], len = 0;
	while(x) tmp[++len] = x % 10, x /= 10;
	for(int i = len; i >= 1; i--)
		putchar(char(tmp[i] + '0'));
}
struct edon {
	int c, v;
	bool operator < (const edon &other) const {
		return v > other.v;
	}
};
struct udon {
	int u, v, val;
	bool operator < (const udon &other) const {
		return val < other.val;
	}
};
struct ds {//维护过点 u 的第 k 优
	vector <edon> vec; 
	priority_queue <udon> que;
	vector <int> skip;

	void init() {
		sort(vec.begin(), vec.end());
		for(int i = 0; i < vec.size(); i++)
			skip.push_back(i);

		skip[vec.size() - 1] = vec.size();
//找(j, vec.size() - 1] 内与 c[i] 不一样的点。
//如果 c[j+1]!=c[i] 那就行了,否则就是 c[j +1]=c[j+2] = ... =c[x - 1]!= c[x]
//skip[i] 保存的就是这个 x,代表距离 i 最近的 c[i]!=c[x] 的 x
		for(int i = vec.size() - 2; i >= 0; i--)
			if(vec[i].c != vec[i + 1].c) skip[i] = i + 1;
			else skip[i] = skip[i + 1];
		for(int i = 0; i < vec.size() - 1; i++) {
			int u = i, v = i + 1;
			if(vec[u].c == vec[v].c) v = skip[v];
			if(v < vec.size())
				que.push((udon){u, v, vec[u].v + vec[v].v});
		}
	}
	vector <int> cur;
	il void getnxt() {//下一个
		if(que.empty())
			return ;

		udon fr = que.top(); que.pop();
		cur.push_back(fr.val);

		fr.v++;
		if(fr.v < vec.size()) {
			if(vec[fr.v].c == vec[fr.u].c) fr.v = skip[fr.v];
			if(fr.v < vec.size())
				que.push((udon){fr.u, fr.v, vec[fr.u].v + vec[fr.v].v});
		}
	}
	il int getrnk(int x) {
		if(x < cur.size()) return cur[x];
		else {
			int T = x - cur.size() + 1;
			while(T--) {
				if(que.empty()) break;
				getnxt();
			}
			if(x < cur.size()) return cur[x];
			return -inf;
		}
	}

} D[N + 10];

struct node {
	int to, val;
	node(int T, int V) {
		to = T, val = V;
	}
};
vector <node> gra[N + 10];
bool vis[N + 10];
int n, m;
int remain = n, maxpart = n, root = 0;
int siz[N + 10];
il void getsiz(int u, int fa) {
	siz[u] = 1;
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i].to;
		if(v == fa || vis[v]) continue;
		getsiz(v, u);
		siz[u] += siz[v];
	}
}
il void getroot(int u, int fa) {
	int rest = 0;
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i].to;
		if(v == fa || vis[v]) continue;
		getroot(v, u);
		rest = max(rest, siz[v]);
	}
	rest = max(rest, remain - siz[u]);
	if(rest < maxpart) {
		maxpart = rest;
		root = u;
	}
}

il void getdis(int u, int fa, int dist, int bl, int f) {
	D[f].vec.push_back((edon){bl, dist});
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i].to, w = gra[u][i].val;
		if(v == fa || vis[v]) continue;
		getdis(v, u, dist + w, bl, f);
	}
}
il void calc(int u) {
	D[u].vec.push_back((edon){0, 0});//加入点
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i].to, w = gra[u][i].val;
		if(vis[v]) continue;
		getdis(v, u, w, v, u);
	}
}
il void work(int u) {
	vis[u] = 1;
	calc(u);
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i].to;
		if(vis[v]) continue;
		getsiz(v, 0);
		remain = siz[v], maxpart = n, root = 0;
		getroot(v, 0);
		work(root);
	}
}

il void init() {
	priority_queue <udon> que;
	for(int i = 1; i <= n; i++)
		que.push((udon){i, 0, D[i].getrnk(0)});
	// for(int i = 1; i <= n; i++) {
	// 	for(int j = 0; j <= 10; j++)
	// 		cout << D[i].getrnk(j) << ' ';
	// 	cout << endl;
	// 	// que.push((udon){i, 0, D[i].getrnk(0)});
	// }

	int cnt = 0;
	while(!que.empty()) {
		udon fr = que.top(); que.pop();
		write(fr.val), putchar('\n');
		cnt++;
		if(cnt >= m) break;

		que.push((udon){fr.u, fr.v + 1, D[fr.u].getrnk(fr.v + 1)});
	}
}
int main() {
	n = read(), m = read();
	for(int i = 1, x, y, z; i < n; i++) {
		x = read(), y = read(), z = read();
		gra[x].push_back(node(y, z));
		gra[y].push_back(node(x, z));
	}

	remain = n, maxpart = n, root = 0;
	getsiz(1, 0);
	getroot(1, 0);
	work(root);

	for(int i = 1; i <= n; i++)
		D[i].init();
	init();
}

联通块方面

P6326

我们很容易想到这样一个暴力:对于每个点为根暴力跑一遍树上有依赖背包。朴素的树上背包时候,虽然 \(u\) 点上我们要跑一个多重背包,但是这个并不太重要,单调队列一下这个是 \(O(nm)\) 的。重点在于合并 \(u\) 的儿子,这个怎么做都要做 \(O(nm^2)\) 的,不能用我们最熟悉的上下界优化。因为这个时候上界和子树大小没有任何关系。于是我们得到了优秀的 \(O(n^2m^2)\) 的算法。总之是多项式的(划掉

注意到,如果我们不选择根,那么答案一定不会跨过根,换而言之根把整棵树分成几个联通块,各个联通块之间可以独立求解,互不影响。很显然可以利用点分治,做到 \(O(nm^2\log_2 n)\)。然后我就不会了,这个树上背包我不会做。


于是我抄题解看到了这么大蛇的做法,给大蛇们磕头了。

我们分析一下此时的瓶颈。首先我们发现我们根本没必要求出每一个 \(f_u\),事实上我们只关心根的 \(f\)。其次我们发现我们没有充分利用好联通块的结构性质。树上一个含根的联通块可以看成树砍掉若干个子树。投射到 dfs 序上就是砍掉若干个区间内的点后操作。去掉部分区间的多重背包,这个问题看上去没有那么难。

我们来尝试在 dfs 序上 dp。子树相当于一个子结构,是在一个点后面的,所以我们从后向前 dp。显然 dfs 序是要设计进状态的,不难想到设 \(f[i, x]\) 为 dfs 序考虑到了 \(i\sim n\) 个点,花费的体积为 \(x\) 的最大收益。这下是不是会容易一点了?如果我们不选择 \(i\),那么就变成 \(f[i + siz_i, x]\)。如果选择的话我们就将 \(i\) 点上的物品合并到 \(f[i + 1]\) 中去。

点分树

这一块我研究不太多呢 QAQ其实前两块本身学的也很一般好吧!

点分治的流程是:每次取出重心,删除,然后考虑每个联通块,类似操作。注意到取重心这个操作,每次以取出的重心为根,剩余连通块的重心为儿子,以此类推构造出一个高度不超过 \(O(\log n)\) 的树,称作点分树。点分树的本质是点分治结构的抽象。

关键性质:\(u, v\) 在点分树上 LCA 仍在原树 \(u\)\(v\) 的路径上这是因为删除这个 lca 后 \(u, v\) 就不连通了,说明点分治流程中就是在这个点处路径 \((u, v)\) 被统计。

结构性质:如果暴力存储 \(u\) 子树内的所有点,总点数是 \(O(n\log n)\) 的。因为树高 \(O(\log n)\)

点分数的结构性质允许我们以暴力的姿态解决很多问题。

P7782

大常数选手惨遭资本做局。\(O(n\log n)\) 居然比 \(O(n\log^2 n)\) 慢,这究竟是人性的扭曲还是道德的沦丧!

答案看成 \([1, R]\) 减去 \([1, L - 1]\)。路径统计显然是点分治,很容易想到对于每个分治中心,统计联通块中每个点到它的距离,以及这个点在哪个子树里面,分别记为 \((d_i, b_i)\)。先预处理出来并按照 \(d_i\) 排序(bfs 一下加入即可)。分治重心为 \(o\),对于 \(d_u + d_v \le x, b_u \not = b_v\),我们分别在 \(u\)\(o\) 的路径上和 \(v\)\(o\) 的路径上加入贡献。特比的这样 \(o\) 点贡献会被算两次,要减去。树上差分即可。这个双指针一下正反扫两次。特别的要特判 \(d_u \le x\) 的情况。于是 \(O(n\log n)\)

口胡五分钟,卡常五小时。我卡常卡了一个晚上加上早上一个小时,需要注意

  1. 可以把一些 dfs 改成 bfs。
  2. \(n\)\(10^6\) 不要再用 vector 存图了。
  3. 统计的时候把一些重复的地方合并起来会变快。

本质就是利用点分树的结构。

代码:

int que[N + 10], fr = 1, tl = 0;
int in()
{
    int k=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9')k=k*10+c-'0',c=getchar();
    return k*f;
}
void out(ll x)
{
    if(x<0)putchar('-'),x=-x;
    if(x<10)putchar(x+'0');
    else out(x/10),putchar(x%10+'0');
}
bool vis[N + 10];
int siz[N + 10], fatt[N + 10], remain = N, maxpart = N, root = 0;
void getsiz(int u, int fa) {
    siz[u] = 1, fatt[u] = fa;
    for(int i = head[u]; i; i = nxt[i]) {
        int v = to[i];
        if(v == fa || vis[v]) continue;
        getsiz(v, u);
        siz[u] += siz[v];
    }
}
void getroot(int u, int fa) {
    fr = 1, tl = 0;
    que[++tl] = u;
    while(fr <= tl) {
        int f = que[fr]; fr++;
        int rest = 0;
        for(int i = head[f]; i; i = nxt[i]) {
            int v = to[i];
            if(vis[v] || v == fatt[f]) continue;
            rest = max(rest, siz[v]);
            que[++tl] = v;
        }
        rest = max(rest, remain - siz[f]);
        if(rest < maxpart) {
            maxpart = rest;
            root = f;
        }
    }
}

struct node {
    int val, bel, u;
} vec[N * L + 20];
int ssiz = 0;
int lll[N + 10], rr[N + 10];
int bel, maxn = 0, bl[N + 10];
void getdis(int u, int fat, int dep) {
    fr = 1, tl = 0;
    que[++tl] = u;
    siz[u] = 0;
    fatt[u] = fat;
    lll[u] = rr[u] = -1;
    while(fr <= tl) {
        int f = que[fr]; fr++;
        if(siz[f] > qr) break;
        if(siz[f]) {
            if(lll[u] == -1) lll[u] = ssiz;
            vec[ssiz++] = ((node){siz[f], bl[f], f});
            rr[u] = ssiz - 1;
        }
        maxn = siz[f];
        if(f == u) {
            for(int i = head[f]; i; i = nxt[i]) {
                int v = to[i];
                if(vis[v]) continue;
                bl[v] = v;
                fatt[v] = u;
                siz[v] = 1;
                que[++tl] = v;
            }
            continue;
        }
        for(int i = head[f]; i; i = nxt[i]) {
            int v = to[i];
            if(vis[v] || v == fatt[f]) continue;
            fatt[v] = f;
            siz[v] = siz[f] + 1;
            que[++tl] = v;
            bl[v] = bl[f];
        }
    }
}
void work(int u) {
    // cout << u << endl;
    vis[u] = 1;
    maxn = 0;
    getdis(u, 0, 0);
    for(int i = head[u]; i; i = nxt[i]) {
        int v = to[i];
        if(vis[v]) continue;
        getsiz(v, 0);
        remain = siz[v], root = 0, maxpart = N;
        getroot(v, 0);
        work(root);
    }
}

namespace lca {
    int fat[N + 10], dep[N + 10], mind[N + 10][21], C = 0, dfn[N + 10];
    void dfs(int u, int fa) {
        dfn[u] = ++C;
        fat[u] = fa;
        mind[C][0] = u;
        dep[u] = dep[fa] + 1;
        for(int i = head[u]; i; i = nxt[i]) {
            int v = to[i];
            if(v == fa) continue;
            dfs(v, u);
        } 
    }
    void prep() {
        dfs(1, 0);
        for(int j = 1; (1 << j) <= n; j++)
            for(int i = 1; i + (1 << j) - 1 <= n; i++)
                if(dep[mind[i][j - 1]] < dep[mind[i + (1 << (j - 1))][j - 1]])
                    mind[i][j] = mind[i][j - 1];
                else mind[i][j] = mind[i + (1 << (j - 1))][j - 1];
    }
    inline int lca(int u, int v) {
        if(u == v) return u;
        if(dfn[u] > dfn[v]) swap(u, v);
        int L = __lg(dfn[v] - dfn[u]);
        if(dep[mind[dfn[u] + 1][L]] < dep[mind[dfn[v] - (1 << L) + 1][L]])
            return fat[mind[dfn[u] + 1][L]];
        else return fat[mind[dfn[v] - (1 << L) + 1][L]];
    }
}
ll cf[N + 10];
inline void upd(int u, int v, int val) {
    if(!u || !v) return ;
    cf[u] += val;
    cf[v] += val;
    int l = lca::lca(u, v);
    cf[l] -= val;
    cf[lca::fat[l]] -= val;
    // cout << u << ' ' << v << ' ' << val << endl;
}
int tab[N + 10];
void calc(int x, int t) {
    for(int u = 1; u <= n; u++) {
        if(rr[u] < 0 || lll[u] < 0) continue;
        int l = lll[u], r = lll[u];
        for(r = rr[u]; r >= lll[u]; r--) {
            if(vec[r].val > x) continue;
            while(l > r && l > lll[u])
                l--, tab[vec[l].bel]--;
            while(l < r && vec[r].val + vec[l].val <= x)
                tab[vec[l].bel]++, l++;
            upd(vec[r].u, u, t * (l - tab[vec[r].bel] - lll[u]));
        }
        for(r = head[u]; r; r = nxt[r])
            tab[to[r]] = 0;
        for(r = lll[u]; r <= rr[u]; r++)
            tab[vec[r].bel]++;

        r = rr[u];
        for(l = lll[u]; l <= rr[u] - 1; l++) {
            if(vec[l].val > x) break;
            upd(vec[l].u, u, t * 1);
            tab[vec[l].bel]--;
            while(r >= lll[u] && vec[l].val + vec[r].val > x)
                tab[vec[r].bel]--, r--;
            int rest = (r - l) - tab[vec[l].bel]; 
            rest = max(rest, 0);
            upd(vec[l].u, u, t * rest);
            upd(u, u, -t * rest);
        } 
        if(vec[rr[u]].val <= x) upd(vec[rr[u]].u, u, t * 1);
        for(r = head[u]; r; r = nxt[r])
            tab[to[r]] = 0;
    }
}
void dp(int u, int fa) {
    for(int i = head[u]; i; i = nxt[i]) {
        int v = to[i];
        if(v == fa) continue;
        dp(v, u);
        cf[u] += cf[v];
    }
}
int main() {
    cin >> n >> ql >> qr;
    for(int i = 2, x; i <= n; i++)
        cin >> x, link(i, x), link(x, i);
    lca::prep();
    getsiz(1, 0);
    remain = n, maxpart = n, root = 0;
    getroot(1, 0);
    work(root);

    calc(qr, 1);
    if(ql > 1) calc(ql - 1, -1);
    dp(1, 0);
    for(int i = 1; i <= n; i++)
        cout << cf[i] << '\n';
}

P6329

建立出点分树结构。考虑查询。询问 \(x\) 那么统计就暴力跳 \(x\) 的祖先,往子树里面统计,这里面要容斥掉上一个子树内的信息。具体而言,令 \(x\) 到它祖先 \(u\) 的距离为 \(d_1\),到祖先 \(fat_u\)(都是点分树上的!)的距离为 \(d_2\),那么在 \(u\) 处会统计到 \(u\) 距离小于等于 \((k - d_1)\) 的,在 \(fat_u\) 处会多统计距离小于等于 \((k - d_2)\) 的,两个贡献要相减。

子树内的统计,暴力存储 \(u\) 子树内所有点按照距离排序,用一个 bit 维护前缀和即可。修改也可以暴力修改。

具体见代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5, L = 18;
int lowbit(int x) {
	return x & (-x);
}

int n, m;
vector <int> gra[N + 10];

struct bit {
	int lim;
	vector <int> sum;
	void build(int siz) {
		// sum.resize(siz + 1);
		for(int i = 0; i <= siz + 1; i++)
			sum.push_back(0);
		lim = siz + 1;
	}
	void upd(int x, int y) {
		x++;
		// x = min(x, lim);
		while(x <= lim)
			sum[x] += y, x += lowbit(x);
	}
	int qry(int x) {
		x++;
		x = min(x, lim);
		int rest = 0;
		while(x)
			rest += sum[x], x -= lowbit(x);
		return rest;
	}
} f1[N + 10], f2[N + 10];

namespace LCA {
	int dfn[N + 10], C = 0;
	int dep[N + 10], fat[N + 10], mind[N + 10][L + 3];
	void dfs(int u, int fa) {
		dep[u] = dep[fa] + 1;
		fat[u] = fa;
		dfn[u] = ++C;
		mind[dfn[u]][0] = u;
		for(int i = 0; i < gra[u].size(); i++) {
			int v = gra[u][i];
			if(v == fa) continue;
			dfs(v, u);
		}
	}
	void prep() {
		for(int j = 1; j <= L; j++) {
			for(int i = 1; i + (1 << j) - 1 <= n; i++) {
				if(dep[mind[i][j - 1]] < dep[mind[i + (1 << (j - 1))][j - 1]]) 
					mind[i][j] = mind[i][j - 1];
				else mind[i][j] = mind[i + (1 << (j - 1))][j - 1];
			}
		}
	}
	int getlca(int u, int v) {
		if(u == v) return u;
		if(dfn[u] > dfn[v]) swap(u, v);
		int L = __lg(dfn[v] - dfn[u]);
		if(dep[mind[dfn[u] + 1][L]] < dep[mind[dfn[v] - (1 << L) + 1][L]])
			return fat[mind[dfn[u] + 1][L]];
		else return fat[mind[dfn[v] - (1 << L) + 1][L]];
	}
}

bool vis[N + 10];
int val[N + 10], siz[N + 10];
vector <int> tr[N + 10];
int remain = 0, root = 0, maxpart = N;
void getrt(int u, int fa) {
	if(vis[u]) return ;
	int rest = 0;
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(vis[v] || v == fa) continue;
		getrt(v, u);
		rest = max(rest, siz[v]);
	}
	rest = max(rest, remain - siz[u]);
	if(rest < maxpart) {
		maxpart = rest;
		root = u;
	}
}
void getsiz(int u, int fa) {
	siz[u] = 1;
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(vis[v] || v == fa) continue;
		getsiz(v, u);
		siz[u] += siz[v];
	}
}

int fat[N + 10];
void building(int u) {//建立点分树
	vis[u] = 1;
	// if(u == 93267) {
		// cout << "QWQ" << endl;
	// }
	// cout << u << endl;

	getsiz(u, 0);
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(vis[v]) continue;
		// getsiz(v, 0);
		remain = siz[v];
		root = 0;
		maxpart = remain;
		getrt(v, u);

		// cout << "TYPE1" << endl;
		tr[u].push_back(root);
		fat[root] = u;
		f1[root].build(siz[v]);
		f2[root].build(siz[v]);
		// cout << "TYPE2" << endl;

		building(root);
	}
}
int dist(int u, int v) {
	return LCA::dep[u] + LCA::dep[v] - 2 * LCA::dep[LCA::getlca(u, v)];
}

int tval[N + 10];
void change(int x, int v) {
	int anc = x;
	f1[x].upd(0, v - val[x]);
	while(fat[anc]) {
		int tmp = dist(x, fat[anc]);
		f1[fat[anc]].upd(tmp, v - val[x]);
		f2[anc].upd(tmp, v - val[x]);
		anc = fat[anc];
	}
	val[x] = v;
}
int query(int x, int k) {
	int anc = x;
	int sum = f1[x].qry(k);
	while(fat[anc]) {
		int tmp = dist(x, fat[anc]);
		if(k >= tmp) {
			sum += (f1[fat[anc]].qry(k - tmp) - 
				f2[anc].qry(k - tmp));
		}
		anc = fat[anc];
	}
	return sum;
}

int main() {
	// freopen("read.in", "r", stdin);
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		cin >> tval[i];
	for(int i = 1, x, y; i < n; i++) {
		cin >> x >> y;
		gra[x].push_back(y);
		gra[y].push_back(x);
	}
	LCA::dfs(1, 0);
	LCA::prep();
	// cout << "QWQ" << endl;

	int alltheroot = 0;
	remain = n, root = 0, maxpart = n;
	getsiz(1, 0);
	getrt(1, 0);
	fat[root] = 0;
	f1[root].build(n);
	f2[root].build(n);
	alltheroot = root;
	building(root);
	// cout << "QWQ" << ' ' << alltheroot << ' ' << fat[alltheroot] << endl;

	for(int i = 1; i <= n; i++)
		change(i, tval[i]);

	// cout << "QWQ" << endl;

	int lastans = 0;
	while(m--) {
		int opt; int x, k;
		cin >> opt >> x >> k;
		x ^= lastans, k ^= lastans;
		if(opt) change(x, k);
		else cout << (lastans = query(x, k)) << '\n';
	}
}

P6626

几乎同理

P9260

建图缩点 DAG 可达,这里着重讨论建图的优化。因为是 \(\operatorname{dist}(u, v)\le d_u\) 作为充要条件,所以考虑用点分树的方式整体上刻画,具体而言对于 \(u\) 经过祖先 \(a\) 的连边,能连到 \(a\) 子树中距离 \(a\) 比较小的点的一个前缀,二分处位置后可以前缀优化建图,时间复杂度 \(O(n\log^2 n)\),练出的边数量 \(O(n\log n)\)

线段树合并进行可达性统计,我洛谷过了,但是 loj 过不去,这也太难了嘤嘤嘤

P6541

肃然起敬。

首先每次已知的点集必然是一个联通块。

首先考虑链的情况,大概的操作次数是 \(O(n+\log_2 n)\) 级别的。我们很容易想到这样一个链的做法,链上已知点集是一个区间,那么我们随机从左端点或者右端点中往任意一个未确定的点 \(y\) 进行一次探索。如果探索出来的 \(z\) 没有被访问,那么我们就可以一路走到 \(z\)。如果被访问过了,那么显然 \(y\) 就在另一侧,我们就从另一个端点一路走到 \(z\)。我们发现这个实际次数是 \((n+c)\)\(c\) 为第一次找到 \(z\) 被访问过的次数。由于这是随机的,所以我们可以认为 \(c\) 期望是 \(O(\log_2 n)\) 的。

然后考虑树的情况。直接暴力的话可以从根开始往任意一个点在链上探索。但是时间是 \(O(n^2)\) 的。考虑优化这个过程。分析一下 \(\operatorname{explore}(x, y) = z\)\(z\) 访问过时发生了什么事。把 \(z\) 作为根看那么 \(y\) 在以 \(z\) 为根的子树内。每次只跳一层太慢了,而且此时我们得到的信息是 \(y\)\(z\) 为根子树,并没有别的约束,在里面随便找个点都行,然后可以继续按照相似的手法划分到子树内。这样的过程很容易联想到点分治。我们选择 \(z\) 的重心进行下一次操作。放在点分树上就是,如果 \(z\) 被访问过,那么就跳到 \(z\) 在点分树上作为 \(x\) 儿子的祖先。这样的过程是 \(O(n\log_2 n)\) 的。

接下来我们要来维护一个动态加叶子的点分树。我不会 LCT,这篇题解太强啦!利用类似替罪羊树的方法,更新点分树子树大小时,如果 \(x\) 的子树大小超过父亲子树大小的 \(\alpha\) 那么就把点分树上父亲子树直接重构。\(\alpha\)\(0.7\) 左右就行。这个复杂度我不会算反正能过。

一些细节:

  • 点分树连边。
  • 如果所找到的叶子不是当前要找的点,那么最好一路找到目前要找到点,加入一条链进去。再从上到下判定重构,这样只要重构最上层的一次。
#include <bits/stdc++.h>
#include "rts.h"
#define il inline
const int N = 3e5;
using namespace std;
// int explore(int,int);

namespace chain {
	bool used[N + 10];
	int n, T;
	vector <int> ord;
	void init(int nn, int tt) {
		n = nn, T = tt;
		for(int i = 2; i <= n; i++)
			ord.push_back(i);
		for(int t = 1; t <= 10; t++)
			random_shuffle(ord.begin(), ord.end());
		used[1] = 1;
		int L = 1, R = 1;
		for(int i = 0; i < ord.size(); i++) {
			if(used[ord[i]]) continue;
			int o = rand() % 2;
			int w = explore(((o == 0) ? R : L), ord[i]);
			if(!used[w]) {
				used[w] = 1;
				int x = w;
				while(x != ord[i])
					x = explore(x, ord[i]), 
					used[x] = 1;
				if(o == 0) R = ord[i];
				else L = ord[i];
			}
			else {
				w = explore(((o == 0) ? L : R), ord[i]);
				used[w] = 1;
				int x = w;
				while(x != ord[i])
					x = explore(x, ord[i]), 
					used[x] = 1;

				if(o == 1) R = ord[i];
				else L = ord[i];
			}
		}
	}
}
namespace tree {
	unordered_map <int, unordered_map <int, int> > M;
	vector <int> gra[N + 10];
	int fat[N + 10];
	vector <int> T[N + 10];
	int root = 0;
	int sz[N + 10];

	bool used[N + 10];
	int pot = 0;

	int n;
	il int getson(int u, int aim) {
		while(fat[u] != aim)
			u = fat[u];
		return u;
	}
	vector <int> dot;
	bool isin[N + 10], vis[N + 10];
	int cur[N + 10], tc = 0;
	il void getdot(int u) {
		cur[++tc] = u;
		isin[u] = 1;
		vis[u] = 0;
		for(int i = 0; i < T[u].size(); i++) {
			int v = T[u][i];
			getdot(v);
		}
		T[u].clear();
	}
	int siz[N + 10], mxz[N + 10];//in gra[] the siz
	int rem = 0, maxpart = N, nowroot = 0;
	il void getsiz(int u, int fa) {
		siz[u] = 1;
		mxz[u] = 0;
		for(int i = 0; i < gra[u].size(); i++) {
			int v = gra[u][i];
			if(vis[v] || !isin[v] || v == fa) continue;
			getsiz(v, u);
			siz[u] += siz[v];
			mxz[u] = max(mxz[u], siz[v]);
		}
	} 
	il int getroot(int u, int fa) {
		if(max(mxz[u], rem - siz[u]) <= rem / 2) return u;
		for(int i = 0; i < gra[u].size(); i++) {
			int v = gra[u][i];
			if(vis[v] || !isin[v] || v == fa) continue;
			int t = getroot(v, u);
			if(t != -1) return t;
		}
		return -1;
	}
	il void rb(int u) {
		vis[u] = 1;
		sz[u] = 1;
		for(int i = 0; i < gra[u].size(); i++) {
			int v = gra[u][i];
			if(vis[v] || !isin[v]) continue;
			getsiz(v, 0);
			rem = siz[v], maxpart = N, nowroot = getroot(v, 0);
			int ww = nowroot;
			T[u].push_back(ww);
			fat[ww] = u;
			rb(ww);
			sz[u] += sz[ww];
		}
	}
	il void rebuild(int u) {
		getdot(u);
		getsiz(u, 0);
		rem = siz[u], maxpart = N, nowroot = getroot(u, 0);
		if(nowroot != u && fat[u]) {
			vector <int> tmp = T[fat[u]];
			T[fat[u]].clear();
			for(int i = 0; i < tmp.size(); i++)
				if(tmp[i] != u) T[fat[u]].push_back(tmp[i]);
			T[fat[u]].push_back(nowroot);
			fat[nowroot] = fat[u];
		}
		if(!fat[u]) {
			sz[nowroot] = sz[root];
			root = nowroot;
			fat[nowroot] = 0;
		}
		rb(nowroot);
		for(int i = 1; i <= tc; i++) 
			isin[cur[i]] = vis[cur[i]] = 0;
		tc = 0;
	}
	int chn[N + 10];
	int path[N + 10], tt = 0;
	il void find(int aim) {
		int x = root, bl = 0, cnt = 0;
		while(1) {
			int z;
			z = explore(x, aim);
			bl = z;
			if(!used[z]) { 
				do {
					chn[++cnt] = z;
					gra[x].push_back(z);
					gra[z].push_back(x);
					T[x].push_back(z);
					fat[z] = x;
					used[z] = 1;

					if(z == aim) break;
					x = z;
					z = explore(x, aim);
				} while(1);
				break;
			}
			else
				x = getson(z, x);
		}
		for(int i = cnt; i >= 1; i--)
			sz[chn[i]] = cnt - i + 1;
		tt = 0;
		x = aim;
		while(x) {
			path[++tt] = x;
			sz[x] += cnt;
			x = fat[x];
		}
		for(int i = tt; i >= 2; i--) {
			int a = path[i], b = path[i - 1];
			if((double)(0.75 * sz[a]) < (double)(1.0 * sz[b])) {
				rebuild(a);
				return ;
			}
		}
	}
	void init(int nn) {
		n = nn;
		used[1] = 1;
		root = 1;
		sz[1] = 1;
		
		vector <int> ord;
		for(int i = 2; i <= n; i++)
			ord.push_back(i);
		random_shuffle(ord.begin(), ord.end());
		random_shuffle(ord.begin(), ord.end());

		for(int i = 0; i < ord.size(); i++) {
			int v = ord[i];
			if(!used[v]) 
				find(v);
		}
	}
}
void play(int n, int T, int dataType) {
	srand(20090725);
	if(dataType == 3) chain::init(n, T);
	else tree::init(n);
}

轻重链剖分

树上链信息还是很难维护,怎么办?把一个树上路径剖分成 \(O(\log n)\) 个链,然后直接在链上维护信息!拥有各种板子。

其实轻重链剖分的本质仍然是:将链剖分使得 \(u\) 到根的链数量为 \(O(\log n)\) 级别。部分题目利用此性质解题。

P5305

有个人写了一个块套树状数组,糖丸了啊,是谁啊不认识

如何计算答案?或者说如何计算贡献?

对于 \(y\) 使 \(x\)\(y\) 的 LCA 为 \(t\),要求 \(x\)\(y\)\(t\) 不同子树内。不同子树不好办,考虑容斥,容斥掉在同一个子树内的情况然后把贡献放到这一层来(其实和上面的点分树的做法很相似)

\(c_u\)\(u\) 子树内有多少点编号小于等于 \(x\),那么总贡献是实际上是 \(\sum\limits (c_{fat_u} - c_u)\operatorname{dep}(fat_u)^k = \sum\limits c_u{[\operatorname{dep}(u)^k - (\operatorname{dep}(u) - 1)^k]}\)。记 \(v_u = \operatorname{dep}(u)^k - (\operatorname{dep}(u) - 1) ^ k\)

询问本质上就变成了求 \(x\) 祖先中 \(c_u\) 并加权。

搞笑歪解:放到平面上用二维数点刻画,对于 \((dfn_u, u)\) 产生贡献,这就相当于一个在 \(y\) 轴上无限延伸的矩形加,查询相当于一个贴着 \(y\) 轴的矩形查询。然后就可以写一个分块套树状数组了还被卡常了呜呜呜

正经解法:仍然从二维数点角度出发,根据询问贴着 \(y\) 轴所以按照点从小到大离线,一个点会给它祖先 \(anc\) 提供 \(v_{anc}\) 的贡献,树剖维护一下即可。

从这道题中我们也可以了解到有的时候不一定要局限用“矩形”来刻画二维数点,树上这种角度来刻画二维数点一样可行。

P2486

稍微注意点线段树写法就做完了。线段树写法为了合并要记录左端的颜色,右端的颜色,总颜色段数。

P2146

P3313

这俩我都没写,咕咕咕

若信息满足可减性

以上四种特定的算法或者思想都是根据比较通用信息的。

如果此信息满足可减性(可以用群定义一下)。对于链,我们可以直接减去 LCA 相关信息。对于子树,可以利用类似于欧拉序上差分的方式。这里分别给出几个例子。

P3302

首先考虑没有合并情况下如何解决 \(x, y\) 路径上第 \(k\) 小点值问题。为了维护 \(x\)\(y\) 每个权值出现的桶,这个东西满足可减性,我们只要维护一个根到 \(x\) 的信息,就可以用 \(x\) 上的桶加上 \(y\) 上的桶减去 \(2\)\(fa[LCA(x, y)]\) 上的桶的信息即可。合并就启发式合并即可。

P4592

故事会时间~这玩意是我和 yzy 省选之前随便组的题目里面抽到的,当时我好像搓了一个 trie,trie 合并,树剖和一个莫队(我有点记不清了)。当时我没调出来,后来调出来了,更搞笑的事情在于它能过。

两个询问相当独立,但是本质差不多。第一个询问可以离线下来做 trie 树合并得到一个子树内的 01trie,或者也可以欧拉序差分。第二个询问可以直接用上面类似的方法取出路径上的 01trie。

P1600

经典老番,常看常新。

明显的一个路径可以拆成上行链下行链,然后考虑贡献到的点。对于 \(x\)\(s\)\(LCA(s, t)\) 的上行处,能观察到当且仅当 \(d_s - d_x = w_x\) 也就是 \(d_x + w_x = d_s\)。对于 \(x\)\(LCA(s, t)\)\(t\) 的下行处能观察到当且仅当 \(d_s + d_x - 2d_{LCA} = w_x\) 也就是 \(d_x - w_x = 2d_{LCA} - d_s\)。对于两类点可以分别打一个开始和结束标记,分别统计。对于第一类就是子树内大过标记的 \(d_s\) 等于 \(d_x + w_x\) 数量。这个东西具有可减性,进入子树的时候记录一下,出子树的时候剪掉。本质上是欧拉序上面做前缀和后差分掉。

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5;
vector <int> gra[N + 10];
int dep[N + 10], jp[N + 10][25];
void dfs(int u, int fa) {
	dep[u] = dep[fa] + 1;
	jp[u][0] = fa;
	for(int i = 1; i <= 20; i++)
		jp[u][i] = jp[jp[u][i - 1]][i - 1];
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa) continue;
		dfs(v, u);
	}
}

int st[N + 10], ed[N + 10], lc[N + 10];
int lca(int x, int y) {
	if(dep[x] < dep[y]) swap(x, y);
	for(int i = 20; i >= 0; i--)
		if(dep[jp[x][i]] >= dep[y])
			x = jp[x][i];
	
	if(x == y) return x;
	
	for(int i = 20; i >= 0; i--)
		if(jp[x][i] != jp[y][i])
			x = jp[x][i], y = jp[y][i];
	return jp[x][0];
}
int n, m, w[N + 10];

struct node {
	int t, v;
};
int ans0[N + 10];
int tab1[2 * N + 10], ans1[N + 10];
vector <node> obj1[N + 10];
void count1(int u, int fa) {
	int rt = tab1[dep[u] + w[u]];
	
	for(int i = 0; i < obj1[u].size(); i++) {
		int t = obj1[u][i].t, v = obj1[u][i].v;
		tab1[v] += t;
	}
	
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa) continue;
		count1(v, u);
	}
	
	ans1[u] = tab1[dep[u] + w[u]] - rt;
}

int tab2[2 * N + 10], ans2[N + 10];
vector <node> obj2[N + 10];
void count2(int u, int fa) {
	int rt = tab2[dep[u] - w[u] + n];
	for(int i = 0; i < obj2[u].size(); i++) {
		int t = obj2[u][i].t, v = obj2[u][i].v;
		tab2[v] += t;
	}
	
	for(int i = 0; i < gra[u].size(); i++) {
		int v = gra[u][i];
		if(v == fa) continue;
		count2(v, u);
	}
	ans2[u] = tab2[dep[u] - w[u] + n] - rt;
}

int main() {
	cin >> n >> m;
	for(int i = 1, x, y; i < n; i++) {
		cin >> x >> y;
		gra[x].push_back(y);
		gra[y].push_back(x);
	}
	
	dfs(1, 0);
	for(int i = 1; i <= n; i++)
		cin >> w[i];
		
	for(int i = 1; i <= m; i++) {
		cin >> st[i] >> ed[i];
		lc[i] = lca(st[i], ed[i]);
		
		if(dep[st[i]] - dep[lc[i]] == w[lc[i]])
			ans0[lc[i]]++;
	}
	
	for(int i = 1; i <= m; i++) {
		obj1[st[i]].push_back((node){1, dep[st[i]]});
		obj1[lc[i]].push_back((node){-1, dep[st[i]]});
		
		obj2[ed[i]].push_back((node){1, 2 * dep[lc[i]] - dep[st[i]] + n});
		obj2[lc[i]].push_back((node){-1, 2 * dep[lc[i]] - dep[st[i]] + n});
	}
	count1(1, 0); 
	count2(1, 0);
	
	for(int i = 1; i <= n; i++)
	    cout << ans1[i] + ans2[i] + ans0[i] << ' ';
}

P5666

考虑点 \(u\) 被贡献的次数。

做一点基础观察:以原树的重心为根,非重心那么其它点的最大联通块一定是砍掉这个子树后得到的。

对于非重心的点 \(u\),令其最大子树为 \(g_u\)。它的最大联通块大小为 \((n - siz_u)\),砍断子树 \(t\) 那么 \(2(n - siz_u - siz_t) \le n - siz_t, 2g_u \le n - siz_t\) 于是就有:

  1. \(t\not\in \operatorname{subtree}(u)\)
  2. \(n - 2g_u \ge siz_t \ge n - 2siz_u\)

为了维护第一个条件就拿全局减去子树,子树统计就用上面的手法即可。

对于 \(u\) 为重心的情况:就两个重心作为根扫描一下即可。


子树问题中数据结构的合并

平衡树一般没人用它来合并把 QAQ

线段树合并

详见 here

trie 合并

在上面 P4592 代码里面贴了

可并堆

P3261

注意到,一个骑士只会死一次,同时死的一定是值最小的那些骑士。再注意到,当 \(a_i = 1\)\(v_i > 0\),也就是攻占后骑士们的相对实力不变。维护小根堆,里面保存骑士实力。到 \(u\) 之后首先把子树中的骑士合并起来,然后每次用最小的骑士攻打,失败了就扔掉代表骑士死了。剩下保留的骑士在根部打一个 tag,形如 \((k, b)\) 代表里面所有元素 \(x\) 应当为 \(x' = kx + b\)

P1552

极为类似。

树上倍增

倍增的好处都有啥?

  • 可以方便的维护满足结合律的信息,在静态情况可以平替树剖。
  • 可以方便的添加叶子,静态情况下可以平替 LCT。

比较战神。

CF1535E

贪心的自然是每次考虑取根到 \(u\) 一个后缀,所以仍然存在的显然是以 \(1\) 为根的一个联通块。每次倍增找到最靠下没有被取完的点即可。因为一个点被去光不会再增加,所以时间复杂度均摊是 \(O((n+q)\log n)\) 的。

P5024

特殊性质使得这题可以不用 ddp。

先考虑不带修的做法,操,忘了,咕咕咕。

P7518

二分+倍增,做完了。

upd:撸起来还是有点细节的。

首先,容易想到如果是一个从后代到祖先的过程,那么每个点的后继是唯一的,但是从祖先到后代不行,所以我们二分,变成两头都向祖先跑。向祖先可以用倍增刻画,问题主要存在于如何找到 \(u\) 上方第一个颜色的点。对于每个颜色所支配子树拿 set 维护连续段即可。

posted @ 2025-10-30 17:21  CatFromMars  阅读(27)  评论(0)    收藏  举报