二次扫描与换根法总结
二次扫描与换根法
咕咕得有些久了,再不写就废了。
久了不写题目都记不住了,不过也权当复习
直接看理论吧。
下面直接用 \(v\) 表示 \(u\) 的子节点。
问题相关
一般二扫的题目很明显。
- 求当每个节点为根时的答案
- 求选出一个根节点,使得xxxx最优
- 求子节点xxx状态的期望个数/概率
- 选出一条xxx路径(用换根简化为根到叶子的路径),使得xxx
- 有哪些点可以通过xxx,成为xxxx
解决此类问题,我们需要知道每个节点为根时候的答案,最后择优。而二扫就是用来统计这个东西的。
理论做法
设:
- \(g_u\) 为以1为根时,子树 \(u\) 所得答案
- \(h_u\) 为以 \(fa_u\) 为根时,子树 \(u\) 不参与答案统计时的答案
- \(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\) ?
- \(val_{fa}<val_u\implies w=0\)
- \(val_{fa}> val_u \implies w=f_{fa}\),因为当 \(fa\) 为根时,\(u\) 根本不会被统计。
- \(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\) 个磁铁,当他到达一个房间时,他可以选择丢下一个磁铁(也可以不丢),将与这个房间相邻的所有房间里的铁球吸引到这个房间。这个过程如下:
- 逃亡者进入房间。
- 逃亡者丢下磁铁。
- 逃亡者走出房间。
- 铁球被吸引到这个房间。
注意逃亡者只会受到这个房间原有的铁球的阻拦,而不会受到被吸引的铁球的阻挡。
在逃亡者走出迷宫后,追逐者将会沿着逃亡者走过的路径穿过迷宫,他会碰到这条路径上所有的铁球。
请帮助逃亡者选择一条路径,使得追逐者遇到的铁球数量减去逃亡者遇到的铁球数量最大化。
题解:
本质上,是要求一条路径,对于寻找树上最优路径问题,用动态规划算法解决,一般是有两种处理方式:
- 换根DP法,这样只需要考虑根节点到子树的路径,需要维护上述的 \(g,h,f\) 三个函数。
- 枚举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();
}