二次扫描与换根法总结

二次扫描与换根法

咕咕得有些久了,再不写就废了。

久了不写题目都记不住了,不过也权当复习

直接看理论吧。

下面直接用 \(v\) 表示 \(u\) 的子节点。

问题相关

一般二扫的题目很明显。

  1. 求当每个节点为根时的答案
  2. 求选出一个根节点,使得xxxx最优
  3. 求子节点xxx状态的期望个数/概率
  4. 选出一条xxx路径(用换根简化为根到叶子的路径),使得xxx
  5. 有哪些点可以通过xxx,成为xxxx

解决此类问题,我们需要知道每个节点为根时候的答案,最后择优。而二扫就是用来统计这个东西的。

理论做法

设:

  1. \(g_u\) 为以1为根时,子树 \(u\) 所得答案
  2. \(h_u\) 为以 \(fa_u\) 为根时,子树 \(u\) 不参与答案统计时的答案
  3. \(f_u\) 为以 \(u\) 为整棵树的根时的答案

我们在第一次dfs的过程中求出 \(g\)

然后更新出 \(f_1=g_1\),接着进行第二次dfs,先排除影响,求出 \(h\),再合并为 \(f\)

这个排除影响,通用的方法是重新求一边 \(f_{fa_u}\),不统计 \(u\)。但如果可以直接消去影响也是可以的。

下面放题。

例题

简单题部分

sta

给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大。

我们考虑设 \(g_x\) 表示以 \(x\) 为根的子树内的深度之和。则显然有 \(g_u=dep_u+\sum g_v\)

然后考虑设 \(f_x\) 表示以 \(x\) 作为全树根时的深度之和,有 \(f_1=g_1\)

现在我们一直 \(f_{fa}\) 的值,求 \(f_u\)

显然,将根节点下移,会使得 \(u\) 原子树内所有节点深度减一,而其他节点深度加一。故有 \(f_u=f_{fa}+n-2siz_u\)

最大疯子树

转化一下题意,即为给一棵树,求一个极大连通子图,满足以点权最小的节点为根且根到子节点的路径上点权不严格单增。

考虑设 \(g_u\) 表示以 \(u\) 为子树根时,在子树内的疯子树大小。显然有 \(g_u=1+\sum_{val_v\ge val_u}g_v\)

再考虑换根。设 \(f_u=g_u+w\),而 \(w\)

  1. \(val_{fa}<val_u\implies w=0\)
  2. \(val_{fa}> val_u \implies w=f_{fa}\),因为当 \(fa\) 为根时,\(u\) 根本不会被统计。
  3. \(val_{fa}= val_u\implies w=f_{fa}-g_u\),此时实际上 \(fa,u\) 等价。

本题保证了 \(val\) 相异,所以不用管情况3

应用拔高

叶子的染色

题意:给定 \(m\) 个点, \(1\sim n\) 号点为叶子,度数为1,其余点度数大于1。每个点可以染为白色、黑色或者不染色,给定 \(c_u\) 表示 叶子节点 \(u\) 到根的路径上(自底向上)的第一个有色节点的颜色。

求所有方案中,染色数最少的方案需要染多少色。

引理:选择任意一个节点为根即可。

证明:设原树根为 \(x\),要转移到子节点 \(y\)

显然,在以 \(x\) 为根时,染色方案中,\(x,y\) 颜色必定不同。

那么换根后,最优方案仍然不会改变 \(x\) 的颜色,因为与 \(y\) 无关,所以方案等价。

那么考虑设 \(f_{u,0/1/2}\) 分别表示不染色,染黑色,染白色的答案。

对于叶子节点而言,其他都是极大值,将自己染为自己所需的颜色,为1.

显然有转移方程式:

		f[u][1]+=min(f[v][1]-1,f[v][2]);//1黑2白0无色
		f[u][2]+=min(f[v][1],f[v][2]-1);
		f[u][0]+=min(f[v][0],min(f[v][1],f[v][2]));		
int main(){
	cin>>m>>n;
	for(int i=1;i<=n;i++)cin>>c[i];
	for(int i=2;i<=m;i++){
		int u,v;cin>>u>>v;add(u,v);add(v,u);
	}
	dfs(n+1,0);
	cout<<min(f[n+1][1],min(f[n+1][0],f[n+1][2]))<<"\n";
}

计算机

题意:给定一颗边有权的树,求出分别以 \(1\sim n\) 为根时,节点的 \(dis\) 最大值。

根据树的直径的性质,处理出直径两端点,并倒着计算一次距离,记为 \(dis1,dis2\),则答案显然是 \(\max(dis1_i,dis2_i)\)

我们还是老实换根吧。

这里有套路,换根维护最值的套路:记录最大值和次大值,一般节点用最大值转移,本身是最大值的节点用次大值转移。

在本题中,我们先计算出树内最长路,树内次长路。注意最长路和次长路必须是由不同子节点转移而来,因为之所以要次长路就是用来换根时更新转移最长路的这个点。

int dp(int u,int fa){
    if(f[u][0]>=0)return f[u][0];
    f[u][0]=f[u][1]=f[u][2]=lst[u]=0;
    for(int i=head[u];i;i=nxt[i]){
        int v=ver[i],w=cost[i];
        if(v==fa)continue;
        if(f[u][0]<dp(v,u)+w){
            f[u][1]=f[u][0];
            f[u][0]=dp(v,u)+w;
            lst[u]=v;
        }
        else if(f[u][1]<dp(v,u)+w)
            f[u][1]=dp(v,u)+w;
    }
    return f[u][0];
}

然后我们考虑换根。这其实也简单,无非是树外最长路,树内最长路取一个最大值而已。

树外最长路怎么求?设 \(v\) 为一个普通节点,则最长路就是 \(u\) 的树内最长路,亦或者 \(u\) 的树外最长路。二者再加上 \(w(u,v)\)。而如果 \(v\) 就是转移最长路的节点,用树内次长路代替即可。

void dfs(int u,int fa){
    for(int i=head[u];i;i=nxt[i]){
        int v=ver[i],w=cost[i];
        if(v==fa) continue;
        if(v==lst[u])f[v][2]=max(f[u][1],f[u][2])+w;
        else f[v][2]=max(f[u][0],f[u][2])+w;
        dfs(v,u);
    }
}

Chase

这个题,怎么说,其实并不是一道换根的题,但它揭示了一个树上路径问题的新套路。

题意:
在逃亡者的面前有一个迷宫,这个迷宫由 \(n\) 个房间和 \(n-1\) 条双向走廊构成,每条走廊会链接不同的两个房间,所有的房间都可以通过走廊互相到达。换句话说,这是一棵树。

逃亡者会选择一个房间进入迷宫,走过若干条走廊并走出迷宫,但他永远不会走重复的走廊。

在第 \(i\) 个房间里,有 \(F_i\)​​ 个铁球,每当一个人经过这个房间时,他就会受到铁球的阻挡。逃亡者手里有 \(V\) 个磁铁,当他到达一个房间时,他可以选择丢下一个磁铁(也可以不丢),将与这个房间相邻的所有房间里的铁球吸引到这个房间。这个过程如下:

  1. 逃亡者进入房间。
  2. 逃亡者丢下磁铁。
  3. 逃亡者走出房间。
  4. 铁球被吸引到这个房间。

注意逃亡者只会受到这个房间原有的铁球的阻拦,而不会受到被吸引的铁球的阻挡。

在逃亡者走出迷宫后,追逐者将会沿着逃亡者走过的路径穿过迷宫,他会碰到这条路径上所有的铁球。

请帮助逃亡者选择一条路径,使得追逐者遇到的铁球数量减去逃亡者遇到的铁球数量最大化。

题解:

本质上,是要求一条路径,对于寻找树上最优路径问题,用动态规划算法解决,一般是有两种处理方式:

  1. 换根DP法,这样只需要考虑根节点到子树的路径,需要维护上述的 \(g,h,f\) 三个函数。
  2. 枚举LCA法,这样可以在一次DFS(可能统计信息需要多次)解决,但需要维护子树内走向根,和根走向子树两个方向。

第二种做法其实和树链剖分有些相似。

此题,第二种做法应该要简单一些。

\(w_u=\sum val_v\)。设 \(g_{i,j,0/1}\) 表示从子树内走到 \(i\),用 \(j\) 块磁铁,是否在的当前节点放磁铁的最优解,设 \(f_{i,j,0/1}\) 为从 \(i\) 走到子树内,用 \(j\) 块磁铁,是否在当前节点放的最优解。

显然 \(g_{u,1,1}=w_u+val_{fa},f_{u,0,1}=g_{u,0,1}=-\infty\)

下面我们考虑计算。先考虑更新答案(类比树的直径)。我们枚举断点,设枚举前半路径用了 \(j\) 块磁铁,共有 \(num\) 块磁铁。

需要注意的是,这个磁铁吸引,除了当前链顶端(LCA),不能计算 \(val_{fa}\)。因为会算重,这个位置本就被计算过(v的链顶)。

为了方便转移,这里可以用一点trick。我们注意到合并两条链时会出现LCA处的统计冲突,如 \(val_{LCA},val_{fa(LCA)}\) 等。我们可以一条链维护这个信息,另一条链不维护。

在实现中我采用用 \(g\) 维护这个信息。

显然有:

		for(int j=num;j>=0;--j){
			mx1=max(mx1,max(g[u][num-j][0],g[u][num-j][1]));//已经统计了val[fa],w[u]
			mx2=max(mx2,max(f[u][num-j][0],f[u][num-j][1]+val[fa]-val[v]));//注意方向带来的差异
			ans=max(ans,max(f[v][j][1],f[v][j][0])+mx1);//算了兄弟节点
			ans=max(ans,max(g[v][j][1],g[v][j][0])+mx2);//里面计算了val[u]
		}

然后考虑更新DP数组。

        for(int j=1;j<=num;j++){
			f[u][j][0]=max(f[u][j][0],max(f[v][j][0],f[v][j][1]));
			f[u][j][1]=max(f[u][j][1],max(f[v][j-1][0],f[v][j-1][1])+w[u]);
			
		}
		for(int j=1;j<=num;j++){
			g[u][j][0]=max(g[u][j][0],max(g[v][j][0],g[v][j][1]));
			g[u][j][1]=max(g[u][j][1],max(g[v][j-1][0],g[v][j-1][1])+w[u]+val[fa]-val[v]);//-val[v] 去重
		}

Centroids

给定一颗树,你有一次将树改造的机会,改造的意思是删去一条边,再加入一条边,保证改造后还是一棵树。

请问有多少点可以通过改造,成为这颗树的重心?(如果以某个点为根,每个子树的大小都不大于\(\dfrac{n}{2}\),则称某个点为重心)

其实是较为简单的问题。

我们考虑怎么改。一个点本身为重心,则直接任删一条边然后再加回来即可。

如果本身不是重心,则必定有一颗子树大小大于 \(\frac{n}{2}\),此时考虑搞事情。

我们可以分离出这个子树里的一颗子树,然后成为当前节点的新儿子,这样就达到了拆开最大子树的目的。

那么,我们拆掉一颗不大于 \(\frac{n}{2}\) 的最大的子树,显然是最优的。

问题化为维护以每个点为根时,节点数不超过 \(\frac{n}{2}\) 的最大子树的大小。

这是容易的,我们只需要判断各个子树是否合法即可。设 \(g_i\) 为在子树 \(i\) 中节点数不超过 \(\frac{n}{2}\) 的最大子树的大小。

则显然有 \(g_u=\max(siz_u[siz_u\le \frac{n}{2}],g_v)\)

然后我们只需要判断那个最大子树 \(v\) 是否满足 \(siz_v-g_v\le \frac{n}{2}\) 即可。

但是需换根。

这里怎么换?只是父子关系的调换而已,其他的没什么用。

我们同样维护合法 \(g\) 的最大值次大值。按常更新即可。

void dfs1(int u,int fa){
	siz[u]=1;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		dfs1(v,u);
		mx[u]=max(mx[u],siz[v]);
		if(mx[u]==siz[v])mxi[u]=v;
		siz[u]+=siz[v];
		if(siz[v]<=n/2){
			if(g[u]==0||siz[v]>siz[g[u]])gg[u]=g[u],g[u]=v,fr[u]=v;
			else if(gg[u]==0||siz[v]>siz[gg[u]])gg[u]=v;
		}
		else {
			if(g[u]==0||siz[g[v]]>siz[g[u]])gg[u]=g[u],g[u]=g[v],fr[u]=v;
			else if(gg[u]==0||siz[g[v]]>siz[gg[u]])gg[u]=g[v];
		}
	}
}
void dfs2(int u,int fa){
	if(mx[u]>n/2)f[u]|=(mx[u]-siz[g[mxi[u]]]<=n/2);
	else f[u]|=(n-siz[u]-sh[u]<=n/2);
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		if(v==fr[u]){ 
			if(n-siz[v]<=n/2&&n-siz[v]>=siz[gg[u]])h[v]=u,sh[v]=n-siz[v];
			else if(siz[gg[u]]>sh[u])h[v]=gg[u],sh[v]=siz[gg[u]];
			else if(n-siz[v]<=n/2)h[v]=u,sh[v]=n-siz[v];
			else h[v]=h[u],sh[v]=sh[u];
		}
		else {
			if(n-siz[v]<=n/2&&n-siz[v]>=siz[g[u]])h[v]=u,sh[v]=n-siz[v];
			else if(siz[g[u]]>sh[u])h[v]=g[u],sh[v]=siz[g[u]];
			else if(n-siz[v]<=n/2)h[v]=u,sh[v]=n-siz[v];
			else h[v]=h[u],sh[v]=sh[u];
		}
		dfs2(v,u);
	}
}

Tow Paths

void dfs(int u,int in){
	f[u][0]=ver[in^1],dep[u]=dep[ver[in^1]]+1;
	for(int i=1;i<=18;i++)f[u][i]=f[f[u][i-1]][i-1];
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(i==(in^1))continue;
		d1[v]=d1[u]+cost[i],d2[v]=d2[u]+cost[i^1];
		dfs(v,i);
		g[u]+=max(0,g[v]-cost[i]-cost[i^1]);
		w[v]=max(0,g[v]-cost[i]-cost[i^1]);
	}
	g[u]+=a[u];
}
void dfs2(int u,int in){
	if(u!=1)h[u]=max(0,h[f[u][0]]+g[f[u][0]]-cost[in]-cost[in^1]-w[u]-a[f[u][0]])+a[u];
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(i==(in^1))continue;
		r[v]=r[u]+g[v]-w[v]; 
		dfs2(v,i);
	}
}
int lca(int u,int v){
	if(dep[u]>dep[v])swap(u,v);
	for(int i=18;i>=0;--i)if(dep[f[v][i]]>=dep[u])v=f[v][i];
	if(u==v)return u;
	for(int i=18;i>=0;--i)if(f[u][i]!=f[v][i])u=f[u][i],v=f[v][i];
	return f[u][0];
}
int main(){
	ios::sync_with_stdio(false);
	read(n),read(q);
	for(int i=1;i<=n;i++)read(a[i]);
	for(int i=1;i<n;i++){
		int u,v,w1,w2;read(u),read(v),read(w1),read(w2); 
		add(u,v,w1);add(v,u,w2);
	} 
	dfs(1,0);h[1]=a[1];r[1]=g[1]-w[1];dfs2(1,0);
	while(q--){
		int u,v;read(u),read(v);int s=lca(u,v);
		int w1=d2[s]-d2[u]+r[u]-r[f[s][0]]+w[s];
		int w2=d1[s]-d1[v]+r[v]-r[f[s][0]]+w[s];
		int w3=h[s]-g[s]-a[s];
		print(w1+w2+w3);puts("");
	}
}

星际迷航

void dfs_g(int u,int fa){
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		dfs_g(v,u);
		s[u]+=(g[v]==0);
		c[u][g[v]]+=h[v];
	}
	g[u]=(s[u]!=0);
	if(s[u]==0)h[u]=c[u][1]+1;
	else if(s[u]==1)h[u]=c[u][0];
}
void dfs_f(int u,int fa){
	if(u!=1){
		int sf=s[fa]-(g[u]==0);
		int c1=c[fa][1],c0=c[fa][0];
		if(g[u])c1-=h[u];
		else c0-=h[u];
		int hf,gf=(sf!=0);
		if(sf==0)hf=c1+1;
		else if(sf==1)hf=c0;
		else hf=0;
		f[u]=g[u]|(gf==0);c[u][gf]+=hf,s[u]+=(gf==0);
		if(s[u]==0)w[u]=c[u][1]+1;
		else if(s[u]==1)w[u]=c[u][0];
	}
	else f[u]=g[u],w[u]=h[u];
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		dfs_f(v,u);  
	}
}
const int p=1e9+7;
struct node{
	int a[2][2];
	node operator*(const node b)const {
		node c;c.a[0][0]=c.a[1][1]=c.a[0][1]=c.a[1][0]=0;
		for(int i=0;i<2;i++)for(int j=0;j<2;j++)for(int k=0;k<2;k++)c.a[i][j]=(c.a[i][j]+a[i][k]*b.a[k][j]%p)%p;
		return c;
	}
};
node power(node a,int b){
	node ans;ans.a[0][0]=ans.a[1][1]=1;ans.a[0][1]=ans.a[1][0]=0;
	while(b){
		if(b&1)ans=ans*a;
		a=a*a;
		b>>=1;
	}
	return ans;
}
void read(int &x){
	x=0;char ch=getchar();
	while(ch>'9'||ch<'0')ch=getchar();
	while('0'<=ch&&ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
void init(){
	read(n);read(m);
	for(int i=1;i<n;i++){
		int u,v;read(u),read(v);add(u,v);
	}
	dfs_g(1,0);dfs_f(1,0);int cnt=0;node a;a.a[0][0]=a.a[0][1]=a.a[1][0]=a.a[1][1]=0;
	for(int i=1;i<=n;i++){
		cnt+=(f[i]==0);
		a.a[1][0]=(a.a[1][0]+(f[i]==0)*n)%p;
		a.a[1][1]=(a.a[1][1]+(f[i]==1)*n)%p;
		a.a[0][0]=(a.a[0][0]+(f[i]==0)*(n-w[i])+(f[i]==1)*w[i])%p;
		a.a[0][1]=(a.a[0][1]+(f[i]==1)*(n-w[i])+(f[i]==0)*w[i])%p; 
	}
	a=power(a,m-1);
	node b;b.a[0][0]=cnt,b.a[0][1]=n-cnt;b.a[1][0]=0,b.a[1][1]=1;
	b=b*a;int s=b.a[0][0],t=b.a[0][1];s%=p,t%=p;
	if(f[1])cout<<(t*n+s*(n-w[1]))%p;
	else cout<<s*w[1]%p<<"\n";
}
signed main(){
	init();
}

\(Trick\) 总结

posted @ 2023-08-04 19:26  spdarkle  阅读(25)  评论(0编辑  收藏  举报