点分治

最近发现各位大佬们(\(MikeDuke\), \(lhm\))都早早会了点分治了,我也打算学一下。

讲解博客链接: 点分治略解

博客里面讲得很详细,但我还是觉得\(MikeDuke\)大佬讲得更易懂一些。

P3806 【模板】点分治1

给定一棵有n个点的树(有边权)

询问树上距离为k的点对是否存在。

运用容斥原理,我们每次找从一个点出发往下找两条路径(可以为空),计入答案,然后进入这个点的每个子树,把算重的部分消去。

消去也比较简单,把每个子树的所有路径找出来(方法同上),把这些路径长度从答案中删去。

为了降低复杂度,我们每次找重心来搞。

\(Code:\)

void find_root(int cur, int faa) {
	f[cur] = 0;
	int to;
	siz[cur] = 1;
	for (register int i = head[cur]; i; i = e[i].nxt) {
		to = e[i].to;
		if (to == faa || vis[to])	continue;
		find_root(to, cur);
		siz[cur] += siz[to];
		if (siz[to] > f[cur]) {
			f[cur] = siz[to];
		}
	}
	if (Siz - siz[cur] > f[cur])	f[cur] = Siz - siz[cur];
	if (f[cur] < f[root]) root = cur;
}
void get_dis(int cur, int faa) {
	int to;
	known_dis[++cnt] = dis[cur];
	for (register int i = head[cur]; i; i = e[i].nxt) {
		to = e[i].to;
		if (vis[to] || to == faa)	continue;
		dis[to] = dis[cur] + e[i].val;
		get_dis(to, cur);
	}
}
inline void sol(int cur, int len, int flag) {
	cnt = 0;
	dis[cur] = len;
	get_dis(cur, 0);
	for (register int i = 1; i <= cnt; ++i) {
		for (register int j = i + 1; j <= cnt; ++j) {
			ans[known_dis[i] + known_dis[j]] += flag;
		}
	}
}
void work(int cur) {
	sol(cur, 0, 1); vis[cur] = true;
	int to;
	for (register int i = head[cur]; i; i = e[i].nxt) {
		to = e[i].to;
		if (vis[to])	continue;
		sol(to, e[i].val, -1);
		Siz = siz[to], f[0] = n, root = 0;
		find_root(to, cur);
		work(root);
	}
}
int main() {
	//get the data...
	Siz = n, f[0] = n, root = 0;
	find_root(1, 0);
	work(root);
    //ans[i]:长度为i的路径条数
	return 0;
}

一些习题

P4149 [IOI2011]Race

CF161D Distance in Tree

P4178 Tree

P2634 [国家集训队]聪聪可可

T119278 点对游戏(团队题目)

扩展与延伸:动态点分治(点分树)

例题:P2056 [ZJOI2007]捉迷藏

给一棵无根树,一开始全是关键点,然后有若干操作,每次将某关键点改成非关键点,或将某非关键点改成关键点,操作中掺杂着询问。

\(n <= 1e5, q <= 5e5\)


相当不错的板子题。

双倍经验:P4115 Qtree4

三倍经验:SP2666 QTREE4 - Query on a tree IV

卡常卡的一个比一个厉害

感谢lhm大佬的博客,是这篇博客带领我走入动态点分治的大门(雾 其实是一位山西学长)至少博客里面的板子通俗易懂,十分亲切
  • 点分树:点分治时的重心所构成的树

维护三个堆:

q1(维护全局q2各点所统计的答案的最大值),

q2[cur](维护点分树上cur的所有子树的q3[son].top()的值(含一个0(自己连自己)),用时找最大值和次大值相加),

q3[cur](维护点分树上cur所带领的子树连向 fa 的所有边)。

有: \(q1=max(q2[].max+q2[].second~max)\)\(q2[cur] = max(q3[son].max)\),$q3[cur] = ${ \(dis(node, fa)\)}(node位于\(cur\)所带领的连通块)

修改:在点分树上往上跳;对于每个点分树上的点,一级一级地改就行(删掉,更新下一级,加上)


Code

Part Ⅰ:支持删除,查询最大值,次大值的堆:

经常清空更保险。

struct heap{
	priority_queue<int> que, wst;//waste
	inline void add(int x) {
		que.push(x);
	}
	inline void del(int x) {
		wst.push(x);
	}
	inline void clear() {//attention!!
		while (!wst.empty() && que.top() == wst.top())	que.pop(), wst.pop();//attention!!
	}
	inline int top() {
		clear();
		if (que.empty())	return -inf;//have problems
		return que.top();
	}
	inline int get_siz() {
		clear();
		return que.size() - wst.size();
	}
	inline int get_res() {
		clear();
		int sz = get_siz();
		if (sz == 0)	return -1;
		if (sz == 1)	return 0;
		int mx = top();
		del(mx);
		int res = mx + top();
		add(mx);
		return res;
	}
}q1, q2[N], q3[N];//q3[son]:son -> fa(put res); q2[fa]:fa -> son(get res); q1:ans

Part Ⅱ:动态点分治:

//work前的find_root已经把当前连通块内的点都压入h数组
inline void work(int cur) {
	int to; vis[cur] = true; q2[cur].add(0);//收集子树(含自身)的碎链
	for (register int i = 1; i <= tot; ++i) {
		q3[cur].add(get_dis(h[i], dfzfa[cur]));//给fa当前连通块内的碎链
	}
	for (register int i = head[cur]; i; i = e[i].nxt) {
		to = e[i].to;
		if (vis[to])	continue;
		tot = 0; Siz = siz[to]; root = 0;//注意tot=0,即 更新h数组
		find_root(to, cur);
		dfzfa[root] = cur; int rt = root;//注意设定父亲fa(dfzfa)
		work(root);
		q2[cur].add(q3[rt].top());//收集子树(含自身)的碎链
	}
	q1.add(q2[cur].get_res());//将该点信息汇总到q1
}

inline void modify(int cur, int type) {//type: 0:add; 1:del 
	q1.del(q2[cur].get_res());
	type ? q2[cur].del(0) : q2[cur].add(0);//change itself
	q1.add(q2[cur].get_res());
	int np = cur;
	while (dfzfa[np]) {
		int faa = dfzfa[np]; q1.del(q2[faa].get_res());//删q1
		int tmp = q3[np].top(); if (tmp != -inf)	q2[faa].del(tmp);//删q2
		type ? q3[np].del(get_dis(cur, faa)) : q3[np].add(get_dis(cur, faa));//更新q1
		tmp = q3[np].top(); if (tmp != -inf)	q2[faa].add(tmp);//更新q2
		q1.add(q2[faa].get_res());//更新q1
		np = dfzfa[np];
	}
}

光为了查个距离还写个树剖?这里有一个不用树剖的方法:把每个点的(点分树上)各级祖先都记录到一个 vector 里面。具体看代码:

int Siz, mxsiz[N], root, h[N], cnt, dfzfa[N], siz[N];
bool vis[N];
vector<int> dis[N];
inline void find_root(int cur, int faa, int nwdis) {//nwdis
	dis[cur].push_back(nwdis); h[++cnt] = nwdis;//attention
	siz[cur] = 1, mxsiz[cur] = 0;
	int to;
	for (register int i = head[cur]; i; i = e[i].nxt) {
		to = e[i].to;
		if (to == faa || vis[to])	continue;
		find_root(to, cur, nwdis + e[i].val);
		siz[cur] += siz[to];
		if (siz[to] > mxsiz[cur])	mxsiz[cur] = siz[to];
	}
	if (Siz - siz[cur] > mxsiz[cur])	mxsiz[cur] = Siz - siz[cur];
	if (mxsiz[cur] < mxsiz[root])	root = cur;
}

void work(int cur) {
	int to; vis[cur] = true; q2[cur].add(0);
	for (register int i = 1; i <= cnt; ++i) {
		q3[cur].add(h[i]); //attention
	}
	for (register int i = head[cur]; i; i = e[i].nxt) {
		to = e[i].to;
		if (vis[to])	continue;
		cnt = 0; root = 0; Siz = siz[to];
		find_root(to, cur, e[i].val);
		int rt = root;
		dfzfa[rt] = cur;
		work(rt);
		q2[cur].add(q3[rt].top());
	}
	q1.add(q2[cur].get_res());
}


bool isbl[N];
inline void modify(int cur, int type) {//type: 0:add; 1:del
	q1.del(q2[cur].get_res());
	type ? q2[cur].del(0) : q2[cur].add(0);
	q1.add(q2[cur].get_res());
	int np = cur, id = dis[cur].size() - 1;//id:attention
	while (dfzfa[np]) {
		int faa = dfzfa[np]; q1.del(q2[faa].get_res());
		int tmp = q3[np].top(); if (tmp != -inf)	q2[faa].del(tmp);
		type ? q3[np].del(dis[cur][id]) : q3[np].add(dis[cur][id]);//dis[][]!! 
		tmp = q3[np].top(); if (tmp != -inf)	q2[faa].add(tmp);
		q1.add(q2[faa].get_res());
		np = dfzfa[np]; --id; //id-- !!!
	}
}

双倍经验?SP2666 QTREE4 - Query on a tree IV

可能是吧,但我TLE了。题解 题解2

附博客:点分治&&动态点分治学习笔记

例题:P6329 【模板】点分树 | 震波

支持查询与 \(x\) 相距 \(k\) 以内的点权和(边权均为1),还需支持单点修改点权。

并不是树上路径问题,只有个“相距”和点分治有点关系。

先考虑静态问题:

对于关于 \(x\) 的查询,我们可以在 \(x\) 为分治重心的区域里面查询距离 \(x\) 小于等于 \(k\) 的点权和。但是有些合法点超出了 \(x\) 的管辖区域,那么我们就需要去 \(x\) 的点分数父亲(以及父亲的父亲...)的分治重心求相距那个重心不超过 \(k - dis(fa[x], x)\) 的点权和,不过可能会多减去原来就已经算过的点。发现这些点一定只会在上一个重心的管辖范围内,直接减去即可。

我们用一个线段树维护 \(rt\) 管辖区域中与 \(rt\) 相距为 \(d\) 的点权和;再开个线段树维护一下 \(rt\) 管辖区域中与 \(fa[rt]\) 相距为 \(d\) 的点权和。

动态的话,修改查询容斥即可。

距离可以预处理,点分树只用维护 \(fa[]\) 即可,因为我们只会向上跳。

震波

注意查询的时候当前的 \(dis\)\(k\) 大,不一定其点分数的祖先都比 \(k\) 大,或许祖先是在相反方向,反而更近一些。

关键代码:

void dfs(int cur) {
	
	dtot = 0;
	fg = true;//打了fg将会把 dfs 到的点的 dis 都存到其 vec 里
	dfs_dis(cur, 0, 0);
	fg = false;
	lim1[cur] = dtot;
	build(0, dtot, rt1[cur]);
	
	vis[cur] = true;
	for (register int i = head[cur]; i; i = e[i].nxt) {
		int to = e[i].to; if (vis[to])	continue;
		Siz = siz[to], mxSiz = n;
		find_root(to, 0);
		int nwroot = root;
		fa[root] = cur;
		
		dtot = 0;
		dfs_dis(to, 0, 1);
		lim2[nwroot] = dtot;
		build(0, dtot, rt2[nwroot]);
		
		dfs(nwroot);
	}
}

inline void Modify(int p, int v) {
	int cha = v - h[p];//Attention!!
	h[p] = v;
	modify(0, lim1[p], 0, cha, rt1[p]);//Attention!!!!
	int lst = p, nw = fa[p];
	int nwt = vec[p].size() - 2;
	while (nw) {
		modify(0, lim1[nw], vec[p][nwt], cha, rt1[nw]);
		modify(0, lim2[lst], vec[p][nwt], cha, rt2[lst]);
		
		lst = nw, nw = fa[nw]; --nwt;
	}
}

inline int Query(int p, int k) {
	int res = query(0, lim1[p], 0, k, rt1[p]);
	int lst = p, nw = fa[p];
	int nwt = vec[p].size() - 2;
	while (nw) {
		if (k - vec[p][nwt] >= 0) {//Attention!!!
			res += query(0, lim1[nw], 0, k - vec[p][nwt], rt1[nw]);
			res -= query(0, lim2[lst], 0, k - vec[p][nwt], rt2[lst]);
		}
		
		lst = nw, nw = fa[nw], --nwt;
	}
	return res;
}

习题

后续学习:

【学习笔记】树论—点分树(动态点分治)

注意!!!

  • 为了防止将子树内部的路径给加上,要么运用容斥,先加后减,要么先一个子树一个子树地计算贡献,计算完一个子树后合并,然后再对子树进行dfs,求子树内部的答案,不要先dfs,再一个子树一个子树地算,很不方便!

  • 在求子树的重心时,子树的大小可以暂时按 \(siz[to]\) 来计算,而不用dfs一遍算它真的siz。至于为什么,见:一种基于错误的寻找重心方法的点分治的复杂度分析

  • 计算出子树的重心后,要传重心!!! 不要传 \(to\) !!!不要dfs(to)!!!!否则就白计算重心了,复杂度会退化成(n^2)!!!

  • 注意 \(cur\)\(to\) 的区别啊!!!

  • find_root 的时候,注意要初始化 \(f[](mxsiz[])\)\(siz[]\)!! 即:$ siz[cur] = 1; ~ mxson[cur] = 0;$

  • 分清to和rt!!!

posted @ 2020-07-22 22:00  JiaZP  阅读(204)  评论(0编辑  收藏  举报