长链剖分

\(O(n)\) 时空解决部分 dsu on tree / 线段树合并 / 倍增 \(O(n\log n)\) 时空的问题。

长链剖分的机制是选子树内深度最大的儿子作为长儿子(类比重链剖分的重儿子)。

然后有几个性质:

  1. 一条链向上跳一条链,则链上最大深度不减。

  2. 由 1 可知任何一个点向上跳链次数不会超过 \(\sqrt n\) 次。(但是这个性质不是很有用)。

  3. 任意一个点 \(x\)\(k\) 次祖先 \(y\) 所在的长链的长度大于等于 \(k\),因为 \(x\to y\) 长度为 \(k\),长儿子必然更长。

P5903 【模板】树上 k 级祖先

对树进行长链剖分,记录每个点所在链的顶点和深度。

树上倍增求出每个点的 \(2^c\) 级祖先。

对于每条链,如果其长度为 \(len\),那么在顶点处记录顶点向上的 \(len\) 个祖先和向下的 \(len\) 个链上的儿子。

预处理出每个数的二进制最高位。

对于每次询问 \(x\)\(k\) 级祖先:

利用倍增数组先将 \(x\) 跳到最大的 \(2^c\) 级祖先。

设剩下还有 \(k'\)

由刚刚的性质,此时 \(x\) 所在长链长度 \(\ge 2^c>k'\)

因此可以先将 \(x\) 跳到 \(x\) 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。

复杂度为 \(O(n\log n)−O(1)\)

点击查看代码
/*
* Author: ShaoJia
* Create Time:        2022-09-01 21:12:55
* Last Modified time: 2022-09-01 21:53:13
* Motto: We'll be counting stars.
*/
//#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;
#define fir first
#define sec second
#define mkp make_pair
#define pb emplace_back
#define For(i,j,k) for(int i=(j),i##_=(k);i<=i##_;i++)
#define Rof(i,j,k) for(int i=(j),i##_=(k);i>=i##_;i--)
#define ckmx(a,b) a=max(a,b)
#define ckmn(a,b) a=min(a,b)
#define debug(...) cerr<<"#"<<__LINE__<<": "<<__VA_ARGS__<<endl
#define ui unsigned int
ui S;
inline ui get(ui x) {
	x ^= x << 13;
	x ^= x >> 17;
	x ^= x << 5;
	return S = x; 
}
//-------------------------
#define C 18
#define N 500010
int n,q,root,dep[N],md[N],wson[N],top[N],f[N][C+1],lg[N];
vector<int> e[N],up[N],dn[N];
void dfs(int rt,int fa){
	f[rt][0]=fa;
	For(i,1,C) f[rt][i]=f[f[rt][i-1]][i-1];
	md[rt]=dep[rt]=dep[fa]+1;
	for(int i:e[rt]){
		dfs(i,rt);
		ckmx(md[rt],md[i]);
		if(md[wson[rt]]<md[i]) wson[rt]=i;
	}
}
void dfs2(int rt,int tp){
	top[rt]=tp;
	if(rt==tp){
		int x=rt;
		For(i,0,md[rt]-dep[rt]){
			up[rt].pb(x);
			x=f[x][0];
		}
		x=rt;
		For(i,0,md[rt]-dep[rt]){
			dn[rt].pb(x);
			x=wson[x];
		}
	}
	if(wson[rt]) dfs2(wson[rt],tp);
	for(int i:e[rt]) if(i!=wson[rt]) dfs2(i,i);
}
int que(int x,int y){
	if(!y) return x;//0 dont have lg
	x=f[x][lg[y]];
	y-=1<<lg[y];
	y-=dep[x]-dep[top[x]];
	x=top[x];
	return y>0?up[x][y]:dn[x][-y];
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr);
	cin>>n>>q>>S;
	For(i,2,n) lg[i]=lg[i>>1]+1;
	int x,k,ans=0;
	long long out=0;
	For(i,1,n){
		cin>>x;
		if(x) e[x].pb(i);
		else root=i;
	}
	dfs(root,0);
	dfs2(root,root);
	For(i,1,q){
		x=(get(S)^ans)%n+1;
		k=(get(S)^ans)%dep[x];
		out^=(long long)i*(ans=que(x,k));
	}
	cout<<out<<endl;
return 0;}

P3899 [湖南集训]更为厉害

长链剖分优化 DP。

时空 \(O(n)\)

相当于按 \(mxdep\) 启发式合并(?)

点击查看代码
/*
* Author: ShaoJia
* Last Modified time: 2022-09-02 10:54:28
* Motto: We'll be counting stars.
*/
#include<bits/stdc++.h>
using namespace std;
#define pb emplace_back
#define For(i,j,k) for(int i=(j),i##_=(k);i<=i##_;i++)
#define Rof(i,j,k) for(int i=(j),i##_=(k);i>=i##_;i--)
#define ll long long
#define pi pair<int,int>
const int N=300010;
int n,q,dep[N],wson[N],md[N],sz[N];
ll ans[N],*f[N];//f: dp's sufsum
vector<int> e[N];
vector<pi> g[N];
void dfs(int rt,int fa){
	dep[rt]=dep[fa]+1;
	sz[rt]=1;
	for(int i:e[rt]) if(i!=fa){
		dfs(i,rt);
		sz[rt]+=sz[i];
		if(md[i]>md[wson[rt]]) wson[rt]=i;
	}
	if(wson[rt]) md[rt]=md[wson[rt]];
	else md[rt]=dep[rt];
}
void work(int rt,int fa,int tp){
	if(rt==tp) f[rt]=new ll[md[rt]-dep[rt]+1];//top
	f[rt][0]=sz[rt]-1;
	if(wson[rt]){
		f[wson[rt]]=f[rt]+1;
		work(wson[rt],rt,tp);
		f[rt][0]+=f[wson[rt]][0];
	}
	for(int i:e[rt]) if(i!=fa && i!=wson[rt]){
		work(i,rt,i);
		For(j,0,md[i]-dep[i]) f[rt][j+1]+=f[i][j];
		f[rt][0]+=f[i][0];
	}
	for(auto [k,x]:g[rt]){
		ans[x]=(ll)(sz[rt]-1)*min(dep[rt]-1,k);//b is anc of a
		ans[x]+=f[rt][0]-(sz[rt]-1);
		if(k<md[rt]-dep[rt]) ans[x]-=f[rt][k+1];//sufsum get range sum
	}
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr);
	cin>>n>>q;
	int x,y;
	For(i,1,n-1){
		cin>>x>>y;
		e[x].pb(y);
		e[y].pb(x);
	}
	dfs(1,0);
	For(i,1,q){
		cin>>x>>y;
		g[x].pb(y,i);
	}
	work(1,0,1);
	For(i,1,q) cout<<ans[i]<<endl;
return 0;}

CF570D Tree Requests

跳坑:传 wson 时忘记 f[wson[rt]]=f[rt]+1;,求答案时忘记判 k>md[rt]-dep[rt]

CF526G Spiders Evil Plan

复合题。

题意简述

给定一棵 \(n\) 个节点的无根树,每条边有边权。

\(q\) 次询问,每次询问给出 \(x,y\),你需要选择 \(y\) 条树上的路径,使这些路径形成一个包含 \(x\) 的连通块,且连通块中包含的边 权和最大。

强制在线。

  • \(1 \le n, q \le 10^5\)

题解

可以证明:使用 \(k\) 条路径就可以覆盖一棵有 \(2 k\) 的叶子的树。

先以任意方式匹配叶子。如果有两条路径不相交,可以调整成相交的情况。

不断调整就可以让任意两条路径都相交,于是显然覆盖了整棵树。

(证明不严谨,因为没有说明调整能在有限步内结束,不过这不重要)

所以当询问 \(y\) 的时候,就是要在原树中选取不超过 \(2 y\) 个叶子,让这些叶子组成的极小连通块的边权和尽量大。

再考虑:每次询问中,一定存在一种方案使得直径的两端中至少有一端被选取。

那么我们以两个直径端点为根,每次询问在两棵树中分别查询即可。

那么,现在根是一个叶子(直径端点必然是叶子),且根必选。

也就是说,需要选其它至多 \(2 y - 1\) 个叶子,打通他们到根的链,并且最大化边权和。

考虑带边权的长链剖分,发现这和选取过程是等价的,也就是贪心地选取前 \(2 y - 1\) 个最长链即可。

但是选完之后不一定经过 \(x\),所以需要做一下调整。

首先打通 \(x\) 子树中最深的点到根的路径,然后需要去掉另一个叶子,使得减小量尽量小。

可以发现,要不然是删掉第 \(2 y - 1\) 个最长链,也就是仅选取前 \(2 y - 2\) 个最长链,然后把 \(x\) 接上。

要不然就是在选取前 \(2 y - 1\) 个最长链的基础上,先把 \(x\) 接上,然后删去第一个碰到的点的其它子树。

最优解,一定符合这两种情况之一,且不会统计到错误的情况(叶子数都不超过 \(2 y - 1\)),所以是正确的。

向上跳的过程可以倍增,但是我喜欢省空间,写了一个重剖。

时间复杂度为 \(O ((n + q) \log n)\),空间倍增 \(O(n\log n)\),树剖 \(O(n)\)

倍增

树剖

再卡空间的树剖

CF1009F Dominant Indices

posted @ 2022-09-01 21:55  ShaoJia  阅读(72)  评论(0编辑  收藏  举报