[浅谈] 虚树

P2495 [SDOI2011] 消耗战

所谓虚树,就是对树上 dp 的优化,建立一棵新树,上面包含原树的部分节点,且父子关系不变。套路是他会有很多个询问,每个询问涉及一些关键点,但是总关键点数 105 之类。
建树复杂度 O(nlogn)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2.5e5+110,M=1e6+110;
int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0' && c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
	return x*f;
} 
struct Tree_edge{
	int head[N],last[M],to[M],w[M],tot;
	void add(int u,int v,int t){to[++tot]=v,w[tot]=t,last[tot]=head[u],head[u]=tot;return;}
}E1,E2;
int n,dep[N],dfn[N],CntDfn,f[N][21],mw[N][21];//2^18>2.5e5 
void dfs(int u,int h){
	dfn[u]=++CntDfn;dep[u]=h;
	for(int i=E1.head[u];i;i=E1.last[i]){
		int v=E1.to[i];if(v==f[u][0])continue;
		f[v][0]=u,mw[v][0]=E1.w[i],dfs(v,h+1);
	}
	return;
}

int LCApos,LCAva;
void LCA(int x,int y){
	LCAva=2e9;
	if(dep[x]<dep[y])swap(x,y);
	int de=dep[x]-dep[y];
	for(int i=18;i>=0;i--)
		if(de&(1<<i))LCAva=min(LCAva,mw[x][i]),x=f[x][i];
	for(int i=18;i>=0;i--)
		if(f[x][i]!=f[y][i]){
			LCAva=min(LCAva,mw[x][i]),x=f[x][i];
			LCAva=min(LCAva,mw[y][i]),y=f[y][i];
		}
	if(x!=y){
		LCAva=min(LCAva,mw[x][0]),x=f[x][0];
		LCAva=min(LCAva,mw[y][0]),y=f[y][0];
	}
	LCApos=x;return;
}

int m,keypos[N],st[M],top;
int rcd[N],lenrcd;int dp[N],vis[N];
bool cmp(int A,int B){return dfn[A]<dfn[B];}
void DP(int u){
	if(vis[u]){dp[u]=2e17;return;} 
	dp[u]=0;
	for(int i=E2.head[u];i;i=E2.last[i]){
		int v=E2.to[i];DP(v);
		dp[u]+=min(dp[v],E2.w[i]);
	}
	return;
}
void solve(){
	top=0;for(int i=1;i<=lenrcd;i++)E2.head[rcd[i]]=0;E2.tot=0;lenrcd=0;
	
	int k=read();for(int i=1;i<=k;i++)keypos[i]=read(),vis[keypos[i]]=1;
	sort(keypos+1,keypos+k+1,cmp);
	for(int i=1;i<k;i++){
		st[++top]=keypos[i];
		LCA(keypos[i],keypos[i+1]);st[++top]=LCApos;
	}st[++top]=keypos[k];
	sort(st+1,st+top+1,cmp);
	top=unique(st+1,st+top+1)-st-1
	for(int i=1;i<top;i++){
		LCA(st[i],st[i+1]);int tmp=LCApos;
		LCA(tmp,st[i+1]);
		E2.add(tmp,st[i+1],LCAva);rcd[++lenrcd]=tmp;
	}
	if(st[1]!=1){
		LCA(1,st[1]);
		E2.add(1,st[1],LCAva),rcd[++lenrcd]=1;
	}
	DP(1);printf("%lld\n",dp[1]);
	
	for(int i=1;i<=k;i++)vis[keypos[i]]=0;
	return;
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read(),t=read();
		E1.add(u,v,t),E1.add(v,u,t);
	}
	dfs(1,1);
	for(int i=1;i<=18;i++)
		for(int j=1;j<=n;j++)
			f[j][i]=f[f[j][i-1]][i-1],
			mw[j][i]=min(mw[j][i-1],mw[f[j][i-1]][i-1]);
	m=read();for(int i=1;i<=m;i++)solve();
	return 0;
}

P4103 [HEOI2014] 大工程

首先看到 k2n ,直接先建虚树,然后发现第一问很好求,求每条边的贡献即可。第二三问第一反应应该是类似与点分治的思路,考虑走到一个点上时考虑经过这个点的路径,用 dp[i] 表示 i 号节点目前子树中的最值路。在与目前遍历的子树的结合形成路径。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+110,M=2e6+110;
int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0' && c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
	return x*f;
} 
struct Tree_edge{
	int tot,head[N],to[M],w[M],last[M];
	void add(int u,int v,int t){to[++tot]=v,w[tot]=t,last[tot]=head[u],head[u]=tot;return;}
}E1,E2;

int n,f[N][21],dfn[N],dep[N],cntdfn;//2^20>1e6
void dfs(int u,int h){
	dep[u]=h,dfn[u]=++cntdfn;
	for(int i=E1.head[u];i;i=E1.last[i]){
		int v=E1.to[i];if(v==f[u][0])continue;
		f[v][0]=u;dfs(v,h+1);
	}
	return;
}
int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	int de=dep[x]-dep[y];
	for(int i=20;i>=0;i--)
		if(de&(1ll<<i))x=f[x][i];
	for(int i=20;i>=0;i--)
		if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
	if(x!=y)x=f[x][0],y=f[y][0];
	return x;
}

int dp2[N],dp3[N],k,rcd[N],cntrcd,st[M],top,ans1,ans2,ans3,vis[N],keypos[N];
bool cmp(int A,int B){return dfn[A]<dfn[B];} 
int DP1(int u){
	int sz=vis[u];
	for(int i=E2.head[u];i;i=E2.last[i]){
		int v=E2.to[i];int tmp=DP1(v);
		sz+=tmp;
		ans1+=tmp*(k-tmp)*E2.w[i];
	}
	return sz;
}
void DP2(int u){
	dp2[u]=(!vis[u])*2e12;
	dp3[u]=(!vis[u])*(-2e12);
	for(int i=E2.head[u];i;i=E2.last[i]){
		int v=E2.to[i];DP2(v);
		ans2=min(ans2,E2.w[i]+dp2[v]+dp2[u]);
		ans3=max(ans3,E2.w[i]+dp3[v]+dp3[u]);
		dp2[u]=min(dp2[u],dp2[v]+E2.w[i]);
		dp3[u]=max(dp3[u],dp3[v]+E2.w[i]);
	}
}
void solve(){
	E2.tot=0;for(int i=1;i<=cntrcd;i++)E2.head[rcd[i]]=0;top=0;ans1=0,ans2=2e12,ans3=-2e12;cntrcd=0;//栈忘记清空会T
	
	k=read();for(int i=1;i<=k;i++)keypos[i]=read(),vis[keypos[i]]=1;
	sort(keypos+1,keypos+k+1,cmp);
	for(int i=1;i<k;i++)
		st[++top]=keypos[i],st[++top]=LCA(keypos[i],keypos[i+1]);
	st[++top]=keypos[k];
	sort(st+1,st+top+1,cmp);
	top=unique(st+1,st+top+1)-st-1;
	for(int i=1;i<top;i++){
		int tmp=LCA(st[i],st[i+1]);
		E2.add(tmp,st[i+1],dep[st[i+1]]-dep[tmp]),rcd[++cntrcd]=tmp;
	}
	DP1(st[1]);DP2(st[1]);printf("%lld %lld %lld\n",ans1,ans2,ans3);
	for(int i=1;i<=k;i++)vis[keypos[i]]=0; 
	return;
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){int u=read(),v=read();E1.add(u,v,1);E1.add(v,u,1);}
	dfs(1,1);
	for(int i=1;i<=20;i++)
		for(int j=1;j<=n;j++)
			f[j][i]=f[f[j][i-1]][i-1];
	int m=read();for(int i=1;i<=m;i++)solve();
	return 0;
} 

Kingdom and its Cities

其实虚树模板打完就是普通的树形 DP 了。

dp[u][0/1] 表示从 uu 的子树中存在 0/1 条到重要城市的路径。
然后分类讨论:

  1. 这个点是重要城市,g[u][0]=+(自己到自己也算一条路径),
    g[u][1]:对于一个子树,可以让它有 0 条路径(不然就和 u 连起来了),或者把中间切断(如果能切)然后随它又没有路径。
if(E2.w[i]) g[u][1]+=min(g[v][0],1ll+min(g[v][1],g[v][0]));//如果到自己的儿子中间夹有其他普通城市,那么边权为1
else g[u][1]+=g[v][0];

2.这个点不是重要城市。
因为不是重要城市,所以可以将它自己切断,那么就与子树肯定没有连边了,所以最多只要再切一次(切自己)。

如果切:

long long tmp=1;
for(int i=E2.head[u];i;i=E2.last[i]){	
  int v=E2.to[i];tmp+=min(g[v][1],g[v][0]);
}

如果不切:

long long de=0;
for(int i=E2.head[u];i;i=E2.last[i]){
	int v=E2.to[i];dp(v);
	g[u][0]+=g[v][0];
	de=max(de,g[v][0]-g[v][1]);
}
g[u][1]=g[u][0]-de;//可以保留最多一条到重要城市的路径

然后这题就结束了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+100,M=2e5+110;
int read(){
	int x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0' && c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
	return x*f;
}
struct Tree_edge{
	int tot,head[N],to[M],last[M],w[M];
	void add(int u,int v,int t){to[++tot]=v,w[tot]=t,last[tot]=head[u],head[u]=tot;return;}
}E1,E2;
int n,dep[N],cntdfn,dfn[N],f[N][21];//2^20>>1e5
void dfs(int u,int h){
	dep[u]=h,dfn[u]=++cntdfn;
	for(int i=E1.head[u];i;i=E1.last[i]){
		int v=E1.to[i];if(v==f[u][0])continue;
		f[v][0]=u;dfs(v,h+1);
	}
	return;
}
int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	int de=dep[x]-dep[y];
	for(int i=20;i>=0;i--)
		if(de&(1ll<<i))x=f[x][i];
	for(int i=20;i>=0;i--)
		if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
	if(x!=y)x=f[x][0],y=f[y][0];
	return x;
}
bool cmp(int A,int B){return dfn[A]<dfn[B];} 
int k,keypos[N],vis[N],st[M],top,rcd[N],cntrcd;long long g[N][2];
void dp(int u){
	g[u][0]=g[u][1]=0;
	if(vis[u]){
		g[u][0]=2e9;
		for(int i=E2.head[u];i;i=E2.last[i]){
			int v=E2.to[i];dp(v);
			if(E2.w[i]) g[u][1]+=min(g[v][0],1ll+min(g[v][1],g[v][0]));
			else g[u][1]+=g[v][0];
		}
	}
	else{
		long long de=0;
		for(int i=E2.head[u];i;i=E2.last[i]){
			int v=E2.to[i];dp(v);
			g[u][0]+=g[v][0];
			de=max(de,g[v][0]-g[v][1]);
		}
		g[u][1]=g[u][0]-de;
		long long tmp=1;
		for(int i=E2.head[u];i;i=E2.last[i]){
			int v=E2.to[i];tmp+=min(g[v][1],g[v][0]);
		}
		g[u][1]=min(g[u][1],tmp);
		g[u][0]=min(g[u][0],tmp);
	}	
	return;
}
void solve(){
	E2.tot=0;for(int i=1;i<=cntrcd;i++)E2.head[rcd[i]]=0;top=0;cntrcd=0;
	k=read();for(int i=1;i<=k;i++)keypos[i]=read(),vis[keypos[i]]=1;
	sort(keypos+1,keypos+k+1,cmp);
	for(int i=1;i<k;i++)
		st[++top]=keypos[i],st[++top]=LCA(keypos[i],keypos[i+1]);
	st[++top]=keypos[k];
	sort(st+1,st+top+1,cmp);
	top=unique(st+1,st+top+1)-st-1;
	for(int i=1;i<top;i++){
		int tmp=LCA(st[i],st[i+1]);
		E2.add(tmp,st[i+1],(dep[tmp]+1!=dep[st[i+1]])),rcd[++cntrcd]=tmp;
	}
	dp(st[1]);printf("%lld\n",min(g[st[1]][0],g[st[1]][1])>=2e9?-1:min(g[st[1]][0],g[st[1]][1]));
	for(int i=1;i<=k;i++)vis[keypos[i]]=0;
	return;
}
int main(){
	n=read();
	for(int i=1;i<n;i++){int u=read(),v=read();E1.add(u,v,0),E1.add(v,u,0);}
	dfs(1,1);
	for(int i=1;i<=20;i++)
		for(int j=1;j<=n;j++)
			f[j][i]=f[f[j][i-1]][i-1];
	int m=read();for(int i=1;i<=m;i++)solve();
	return 0;
}
posted @   FJOI  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示