二次扫描与换根法总结
二次扫描与换根法
咕咕得有些久了,再不写就废了。
久了不写题目都记不住了,不过也权当复习
直接看理论吧。
下面直接用
问题相关
一般二扫的题目很明显。
- 求当每个节点为根时的答案
- 求选出一个根节点,使得xxxx最优
- 求子节点xxx状态的期望个数/概率
- 选出一条xxx路径(用换根简化为根到叶子的路径),使得xxx
- 有哪些点可以通过xxx,成为xxxx
解决此类问题,我们需要知道每个节点为根时候的答案,最后择优。而二扫就是用来统计这个东西的。
理论做法
设:
为以1为根时,子树 所得答案 为以 为根时,子树 不参与答案统计时的答案 为以 为整棵树的根时的答案
我们在第一次dfs的过程中求出
然后更新出
这个排除影响,通用的方法是重新求一边
下面放题。
例题
简单题部分
sta
给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大。
我们考虑设
然后考虑设
现在我们一直
显然,将根节点下移,会使得
最大疯子树
转化一下题意,即为给一棵树,求一个极大连通子图,满足以点权最小的节点为根且根到子节点的路径上点权不严格单增。
考虑设
再考虑换根。设
,因为当 为根时, 根本不会被统计。 ,此时实际上 等价。
本题保证了
应用拔高
叶子的染色
题意:给定
求所有方案中,染色数最少的方案需要染多少色。
引理:选择任意一个节点为根即可。
证明:设原树根为
显然,在以
那么换根后,最优方案仍然不会改变
那么考虑设
对于叶子节点而言,其他都是极大值,将自己染为自己所需的颜色,为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";
}
计算机
题意:给定一颗边有权的树,求出分别以
根据树的直径的性质,处理出直径两端点,并倒着计算一次距离,记为
,则答案显然是
我们还是老实换根吧。
这里有套路,换根维护最值的套路:记录最大值和次大值,一般节点用最大值转移,本身是最大值的节点用次大值转移。
在本题中,我们先计算出树内最长路,树内次长路。注意最长路和次长路必须是由不同子节点转移而来,因为之所以要次长路就是用来换根时更新转移最长路的这个点。
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];
}
然后我们考虑换根。这其实也简单,无非是树外最长路,树内最长路取一个最大值而已。
树外最长路怎么求?设
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
这个题,怎么说,其实并不是一道换根的题,但它揭示了一个树上路径问题的新套路。
题意:
在逃亡者的面前有一个迷宫,这个迷宫由
逃亡者会选择一个房间进入迷宫,走过若干条走廊并走出迷宫,但他永远不会走重复的走廊。
在第
- 逃亡者进入房间。
- 逃亡者丢下磁铁。
- 逃亡者走出房间。
- 铁球被吸引到这个房间。
注意逃亡者只会受到这个房间原有的铁球的阻拦,而不会受到被吸引的铁球的阻挡。
在逃亡者走出迷宫后,追逐者将会沿着逃亡者走过的路径穿过迷宫,他会碰到这条路径上所有的铁球。
请帮助逃亡者选择一条路径,使得追逐者遇到的铁球数量减去逃亡者遇到的铁球数量最大化。
题解:
本质上,是要求一条路径,对于寻找树上最优路径问题,用动态规划算法解决,一般是有两种处理方式:
- 换根DP法,这样只需要考虑根节点到子树的路径,需要维护上述的
三个函数。 - 枚举LCA法,这样可以在一次DFS(可能统计信息需要多次)解决,但需要维护子树内走向根,和根走向子树两个方向。
第二种做法其实和树链剖分有些相似。
此题,第二种做法应该要简单一些。
设
显然
下面我们考虑计算。先考虑更新答案(类比树的直径)。我们枚举断点,设枚举前半路径用了
需要注意的是,这个磁铁吸引,除了当前链顶端(LCA),不能计算
为了方便转移,这里可以用一点trick。我们注意到合并两条链时会出现LCA处的统计冲突,如
在实现中我采用用
显然有:
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
给定一颗树,你有一次将树改造的机会,改造的意思是删去一条边,再加入一条边,保证改造后还是一棵树。
请问有多少点可以通过改造,成为这颗树的重心?(如果以某个点为根,每个子树的大小都不大于
其实是较为简单的问题。
我们考虑怎么改。一个点本身为重心,则直接任删一条边然后再加回来即可。
如果本身不是重心,则必定有一颗子树大小大于
我们可以分离出这个子树里的一颗子树,然后成为当前节点的新儿子,这样就达到了拆开最大子树的目的。
那么,我们拆掉一颗不大于
问题化为维护以每个点为根时,节点数不超过
这是容易的,我们只需要判断各个子树是否合法即可。设
则显然有
然后我们只需要判断那个最大子树
但是需换根。
这里怎么换?只是父子关系的调换而已,其他的没什么用。
我们同样维护合法
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();
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!