Loading

【笔记】虚树/长链剖分

来自\(\texttt{SharpnessV}\)省选复习计划中的虚树/长链剖分


虚树和长链剖分都可用于一类 树形\(\rm DP\) 的优化。


Part 1:虚树

特点,多次询问,每次询问包括树上的\(k\)个点,$\sum k \le 2\times 10^6 $ 在可承受范围内。

对于每个询问,只包括树上的部分点,如果我们对整棵树跑 \(\rm DP\) ,无疑是对资源的巨大浪费。

观察一下不难发现 DP 的结果只和这给定的 \(k\) 个点和这 \(k\) 个点两两之间的 \(\rm LCA\) 有关。

\(\rm LCA(x,y)\)\(\rm LCA(x,z)\)一定是祖先后代关系,所以两两之间\(\rm LCA\)不超过\(k\)个,虚树建出来点数是\(\rm O(k)\)级别的。

我们对\(k\)个节点按照 \(\rm DFS\) 序排序,然后用栈模拟 \(\rm DFS\) 的过程,顺便将虚树建出来。

虚树主要用于 \(\rm DP\) 优化,难点仍在于 \(\rm DP\) 的设计。

时间复杂度一般为 \(\rm O((n+\sum k)log)\)


P3233 [HNOI2014]世界树

模板题,每次给定\(k\)个关键点,每个树上的点会移动到离他最近的节点,问每个关键点会收纳多少个节点。

首先我们将虚树建立出来,答案分为两种情况。

第一种是虚树叶子节点的子树,直接归为叶子即可。

第二种是虚树边上的贡献,不难看出以边中点为分界线,两边分别归为虚树边的两段。由于边权是\(1\),这个过程被极大的简化了。

甚至不需要\(\rm DP\)

代码


P2495 [SDOI2011]消耗战

先考虑直接 \(\rm DP\) ,我们用\(f[i]\)表示解决子树内所有点的最小代价,再记录\(g[i]\)表示到根的最小边权,随便转移一下即可。

考虑到查询次数会很多,我们直接上虚树。

代码


P5680 [GZOI2017]共享单车

这道题最大的难点是出题人用又臭又长的文字表述一个非常简单的问题

我们先建立最短路树。

最短路树也是树,所以我们可以在最短路树上建立虚树,然后跑\(\rm DP\)

这里定义 \(f[i]\) 表示 \(i\) 和子树中所有有颜色的点被处理了的最小代价,\(g[i]\)表示没有被处理的最小代价,转移一下最终答案就是 \(f[k]\)

代码


CF613D Kingdom and its Cities

很模板的虚树题,留白供思考。

代码


CF1073G Yet Another LCP Problem

P7409 SvT

虚树与后缀树的结合。

后缀树就是反串 \(\rm SAM\)\(\rm parent\) 树,我们先跑\(\rm SAM\)建出后缀树。

不难发现两个串的\(\rm LCA\)就是后缀树上最近公共祖先的深度。

我们我们可以对每个点计算它作为\(\rm LCA\)时的贡献。

由于多次查询,我么依然可以套用虚树模板。

代码


CF1111E Tree

并不是所有形如\(\sum k\)的树上问题都是虚树,比如这道例题。

如果使用虚树,我们还需要在虚树上换根等,将原本简洁的方法繁复了。

这道题我们直接对每个点按与\(r\)的距离排序,则节点\(i\)的祖先一定出现在\(i\)前面。

这我们就可以直接\(\rm DP\),令\(f[i][j]\)表示前\(i\)个点分为\(j\)组的方案数。

\[f[i][j]=f[i-1][j-1]+\max\{0,j-anc\}\times f[i-1][j] \]

非常类似第二类斯特林树的转移,只不过这里我们要去除与祖先相交的方案。这里\(anc\)表示祖先个数,就是 \(i\)\(r\) 路径上点的个数。树剖树状数组维护即可。

代码


Part 2:长链剖分

特点:状态与节点深度有关的\(\rm DP\)

P5903 【模板】树上 k 级祖先

不能算作模板的模板,这道题只能告诉你什么是长链剖分,而并没有长链剖分的内涵。

对于这道题,我们对每条长链预处理它长度级别的祖先,并预处理每个节点的 \(2^k\) 级祖先。由于先跳\(2^k\)步后上面的深度一定\(<2^k\),而根据长链的定义预处理的深度一定\(>2^k\),所以可以支持\(\rm O(1)\)查询,总的时间复杂度为\(\rm O(N\log N+M)\)

代码


CF1009F Dominant Indices

长链剖分模板题。

我们定义状态 \(f[u][x]\) 表示节点 \(u\) 的子树中,距离 \(u\)\(x\) 的节点个数。枚举一下子树转移即可。

但是状态就是\(N^2\)级别的,看上去是最不可行的方法。

所以我们需要运用黑科技长链剖分,对于每个节点,直接继承它长儿子的\(f\)值,然后将其他儿子的\(f\)合并过来。

如何继承?我们发现长儿子的 \(f[son[u]][i]\) 对应 \(u\)\(f[u][i+1]\),我们直接令指针 \(f[son[u]]=f[u]+1\) 即可,使得两个数组恰好错开 \(1\) 位,使得在更新 \(f[son[u]]\) 时同时更新 \(f[u]\)

长链剖分存在定式,在\(\rm DP\)写出来后直接把\(f[]\)数组修改为指针即可。

这里贴出代码。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1000005
using namespace std;
int n,h[N],tot;
struct edge{
	int to,nxt;
}e[N<<1];
void add(int x,int y){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;
}
int son[N],d[N];
void init(int x,int fa){
	for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa){
		init(e[i].to,x);
		if(d[e[i].to]+1>d[x])son[x]=e[i].to,d[x]=d[e[i].to]+1;
	}
}
vector<int>f[N];int ans[N];
void dfs(int x,int fa){
	if(!son[x]){
		f[x].push_back(1);
		ans[x]=0;
		return ;
	}
	dfs(son[x],x);ans[x]=ans[son[x]]+1;
	swap(f[x],f[son[x]]);
	if(f[x][d[son[x]]-ans[son[x]]]==1)ans[x]=0;
	f[x].push_back(1);
	for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa&&e[i].to!=son[x]){
		dfs(e[i].to,x);
		rep(j,0,d[e[i].to]){
			f[x][d[x]-j-1]+=f[e[i].to][d[e[i].to]-j];
			if(f[x][d[x]-j-1]>f[x][d[x]-ans[x]]||(f[x][d[x]-j-1]==f[x][d[x]-ans[x]]&&j+1<=ans[x]))ans[x]=j+1;
		}
	}
}
int main(){
	scanf("%d",&n);
	rep(i,2,n){
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);add(y,x);
	}
	init(1,0);dfs(1,0);
	rep(i,1,n)printf("%d\n",ans[i]);
	return 0;
}

P3565 [POI2014]HOT-Hotels

P5904 [POI2014]HOT-Hotels 加强版

转移方程这里不再赘述。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5005
using namespace std;
int n,h[N],tot;
struct edge{
	int to,nxt;
}e[N<<1];
void add(int x,int y){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;
}
int g[N][N];long long ans;short f[N][N];
void dfs(int x,int fa){
	for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa){
		dfs(e[i].to,x);
		rep(j,1,n)ans+=f[x][j-1]*g[e[i].to][j]+g[x][j]*f[e[i].to][j-1];
		rep(j,1,n)g[x][j]+=g[e[i].to][j+1]+f[x][j]*f[e[i].to][j-1];
		g[x][0]+=g[e[i].to][1];
		rep(j,1,n)f[x][j]+=f[e[i].to][j-1];
	}
	f[x][0]=1;
	ans+=g[x][0];
}
int main(){
	scanf("%d",&n);
	rep(i,1,n-1){
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);add(y,x);
	}
	dfs(1,0);
	printf("%lld\n",ans);
	return 0;
}

这里是弱化版的代码,中规中矩的树形\(\rm DP\)

长链剖分,只用套用模板即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
typedef long long ll;
using namespace std;
struct edge{
	int to,nxt;
}e[N<<1];
int h[N],tot,n;
void add(int x,int y){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;
}
int d[N],son[N];
void calc(int x,int fa){
	for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa){
		calc(e[i].to,x);if(d[e[i].to]>d[son[x]])son[x]=e[i].to;
	}
	d[x]=d[son[x]]+1;
}
ll *f[N],*g[N],cur[N<<3],*o,ans;
void New(int x){
	f[x]=o;o+=((d[x]+1)<<1);g[x]=o;o+=((d[x]+1)<<1);
}
void dfs(int x,int fa){
	if(!son[x]){
		g[x][0]=0;f[x][0]=1;return ;
	}
	f[son[x]]=f[x]+1;g[son[x]]=g[x]-1;
	dfs(son[x],x);
	for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa&&e[i].to!=son[x]){
		int y=e[i].to;
		New(y);dfs(y,x);
		rep(j,1,d[y]+1)ans+=f[x][j-1]*g[y][j]+g[x][j]*f[y][j-1];
		rep(j,1,d[y]+1)g[x][j]+=g[y][j+1]+f[x][j]*f[y][j-1];
		g[x][0]+=g[e[i].to][1];
		rep(j,1,d[y]+1)f[x][j]+=f[y][j-1];
	}
	f[x][0]=1;
	ans+=g[x][0];
}
int main(){
	scanf("%d",&n);
	rep(i,2,n){
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);add(y,x);
	}
	calc(1,0);
	o=cur;New(1);
	dfs(1,0);
	printf("%lld\n",ans);
	return 0;
}

观察后不难发现,两份代码在\(\rm DP\)转移的时候没有任何变化,唯一变化的是预处理长儿子和用指针数组\(f[]\)代替原来的数组\(f[][]\)


简述:给定一颗边权为 \(1\) 的树,求长度\(\le K\)的路径条数。

点分治板子题,但是我们可以做到线性。

有不使用长剖的线性做法,这里不再赘述。

我们可以定义状态\(f[i][j]\)为子树\(i\)中,距离\(\le j\)的点数。

但是我们发现,这个状态是一个前缀的定义,合并/继承的时候,是对整个后缀进行加减。

我们可以在每个节点\(i\)同时记录一个\(tag\),表示对当前\(f[i]\)进行整体加,在继承的时候同步继承 \(tag\) 即可。

最后统计以当前节点为\(\rm LCA\)时的点对数。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1000005
using namespace std;
int n,k,d[N],son[N],c[N],h[N],tot,nxt[N],to[N];long long ans;
int txt[N<<1],*f[N],*o=txt,y;
void dfs(int x){
    if(!son[x]){c[x]=1;return;}
    f[son[x]]=f[x]+1;dfs(son[x]);c[x]=c[son[x]];f[x][0]=-c[x];
    for(int i=h[x];i;i=nxt[i])if(to[i]!=son[x]){
        f[to[i]]=o;o+=d[to[i]]+2;dfs(to[i]);y=to[i];
        rep(j,0,min(d[y],k-1))f[y][j]+=c[y],ans+=1LL*(f[y][j]-f[y][j-1])*(f[x][min(k-j-1,d[x])]+c[x]);
        c[x]+=f[y][d[y]];f[x][0]-=f[y][d[y]];
        rep(j,0,d[y])f[x][j+1]+=f[y][j]-f[y][d[y]];
    }
    ans+=c[x]+f[x][min(k,d[x])];c[x]++;
}
int main(){
    //freopen("INPUT","r",stdin);
    scanf("%d%d",&n,&k);
    int x,i;
    rep(i,2,n)scanf("%d",&x),nxt[++tot]=h[x],h[x]=tot,to[tot]=i;
    for(x=n;x;x--){
        d[x]=-1;    
        for(i=h[x];i;i=nxt[i])if(d[to[i]]>d[x])d[x]=d[to[i]],son[x]=to[i];
        d[x]++;
    }
    f[1]=o;o+=d[1]+2;dfs(1);
    printf("%lld\n",ans);
    return 0;
}
posted @ 2021-12-16 22:34  7KByte  阅读(121)  评论(0编辑  收藏  举报