虚树学习笔记

虚树学习笔记

虚树是用来优化树形 \(\texttt{DP}\) 的一种算法,可以选取树中的部分关键点进行 \(\texttt{DP}\) 以减小每次 \(\texttt{DP}\) 的规模,从而减少一些不必要的耗时。下面将会结合一道虚树的经典题来说明虚树的用途。

前置知识:在线 \(\texttt{LCA}\) 算法(本博客采用倍增的方式)

P2495 [SDOI2011] 消耗战

题目描述

在一场战争中,战场由 \(n\) 个岛屿和 \(n-1\) 个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为 \(1\) 的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他 \(k\) 个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。

侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到 \(1\) 号岛屿上)。不过侦查部门还发现了这台机器只能够使用 \(m\) 次,所以我们只需要把每次任务完成即可。

输入格式

第一行一个整数 \(n\),表示岛屿数量。

接下来 \(n-1\) 行,每行三个整数 \(u,v,w\) ,表示 \(u\) 号岛屿和 \(v\) 号岛屿由一条代价为 \(w\) 的桥梁直接相连。

\(n+1\) 行,一个整数 \(m\) ,代表敌方机器能使用的次数。

接下来 \(m\) 行,第 \(i\) 行一个整数 \(k_i\) ,代表第 \(i\) 次后,有 \(k_i\) 个岛屿资源丰富。接下来 \(k_i\) 个整数 \(h_1,h_2,..., h_{k_i}\) ,表示资源丰富岛屿的编号。

输出格式

输出共 \(m\) 行,表示每次任务的最小代价。

样例 #1

样例输入 #1

10
1 5 13
1 9 6
2 1 19
2 4 8
2 3 91
5 6 8
7 5 4
7 8 31
10 7 9
3
2 10 6
4 5 7 8 3
3 9 4 6

样例输出 #1

12
32
22

提示

数据规模与约定
  • 对于 \(10\%\) 的数据,\(n\leq 10, m\leq 5\)
  • 对于 \(20\%\) 的数据,\(n\leq 100, m\leq 100, 1\leq k_i\leq 10\)
  • 对于 \(40\%\) 的数据,\(n\leq 1000, 1\leq k_i\leq 15\)
  • 对于 \(100\%\) 的数据,\(2\leq n \leq 2.5\times 10^5, 1\leq m\leq 5\times 10^5, \sum k_i \leq 5\times 10^5, 1\leq k_i< n, h_i\neq 1, 1\leq u,v\leq n, 1\leq w\leq 10^5\)

朴素做法

最简单的做法就是对每一次询问都进行一次树形 \(\texttt{DP}\)

\(dp[i]\) 表示子树 \(i\) 中的答案,那么可以推出转移方程:

\[dp[i]=\begin{cases} dp[i]+\min\{dp[s],w(s,i)\} &,\text{i 不是关键点}\\ dp[i]+w(s,i) &,\text{otherwise.} \end{cases} \]

不难发现,这样的总时间复杂度为 \(\mathcal O(nm)\) 的,对于此题来说无法通过。因此我们要使用虚树对这个 \(\texttt{DP}\) 进行优化。

虚树

观察每次询问,会发现每次 \(\texttt{DP}\) 之间只会有几个关键点的转移方式是不同的。也就是说,如果我们可以建出一棵只包含关键点和他们的 \(\texttt{LCA}\) 的树,然后在这棵新的树上跑 \(\texttt{DP}\) 的话效率会大幅提升。对此,我们引入虚树的概念。

虚树就是从原来的树中抽离出来的一部分,由我们需要的关键点和这些关键点的 \(\texttt{LCA}\) 所构成。对于此题,每次询问给出的关键点就是我们每次虚树中的关键点,直接对这些关键点建树即可。

对于建树的方法,\(\mathcal O(n^2)\) 建树显然十分愚蠢,所以我们要用更好的办法去建树。

建立虚树

在倍增求 \(\texttt{LCA}\) 的预处理的同时记录每个点的 \(\texttt{DFS}\) 序。

首先需要明确,只要满足虚树中的节点的祖孙关系与原树保持一致,虚树中的节点多少并不会影响答案。其实也就是说,只要愿意,可以把所有点都加入虚树(如果你真的这么做,那就是你太闲了owo),也不会导致答案出现错误(只是时间 \(\texttt{T}\) 飞而已)

所以为了我们处理方便,首先先把根节点 \(1\) 加入虚树。对于所有的关键点,先按照 \(\texttt{DFS}\) 序进行排序(升序),然后从左向右进行处理。

维护一个栈,栈中的元素即是一条链上的节点。

对于一个新的关键点 \(s\),先与栈顶元素 \(t\) 求一个 \(\texttt{LCA}\),如果 \(\texttt{LCA}=t\),也就是说 \(s\)\(t\) 是在同一条链上的,直接将 \(s\) 入栈即可。

如果 \(\texttt{LCA}\neq t\),则说明 \(s\)\(t\) 不是在同一条链上,就需要将栈中元素一直弹出直到栈顶元素 \(t\)\(\texttt{DFS}\) 序小于 \(\texttt{LCA}\) 为止,然后再将当前元素入栈。

建树代码:

bool cmp(int x,int y) {return id[x]<id[y];}
void build()
{
	sort(h+1,h+k+1,cmp);
	s[top=1]=1,tot=0,head[1]=0;
	for (int i=1;i<=k;i++)
		if (h[i]!=1)
		{
			int lca=LCA(h[i],s[top]);
			if (lca!=s[top])
			{
				while (id[lca]<id[s[top-1]]) 
					AddEdge(s[top-1],s[top],query(s[top-1],s[top])),top--;
				if (id[lca]!=id[s[top-1]]) 
					head[lca]=0,AddEdge(lca,s[top],query(lca,s[top])),s[top]=lca;
				else 
					AddEdge(lca,s[top],query(lca,s[top])),top--;
			}
			head[h[i]]=0,s[++top]=h[i];
		}
	for (int i=1;i<top;i++) AddEdge(s[i],s[i+1],query(s[i],s[i+1]));
}

建好虚树过后,就可以直接在建好的虚树上跑我们刚刚的树形 \(\texttt{DP}\) 即可。

完整代码

#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
//#define int long long
using namespace std;
template<typename T> void read(T &k)
{
	k=0;T flag=1;char b=getchar();
	while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
	while (isdigit(b)) {k=k*10+b-48;b=getchar();}
	k*=flag;
}
template<typename T> void write(T k) {if (k<0) {putchar('-'),write(-k);return;}if (k>9) write(k/10);putchar(k%10+48);}
template<typename T> void writewith(T k,char c) {write(k);putchar(c);}
const int _SIZE=2.5e5;
int n,m,k,h[_SIZE+5],id[_SIZE+5];
struct EDGE{
	int nxt,to,w;
}edge[(_SIZE<<1)+5];
int tot,head[_SIZE+5];
void AddEdge(int x,int y,int w) {edge[++tot]=(EDGE){head[x],y,w};head[x]=tot;}
int dep[_SIZE+5],f[_SIZE+5][35],g[_SIZE+5][35],cnt;
void dfsForLCA(int x,int fa,int w)//预处理LCA和DFS序
{
	f[x][0]=fa;dep[x]=dep[fa]+1;g[x][0]=w;id[x]=++cnt;
	for (int i=1;i<=30;i++) f[x][i]=f[f[x][i-1]][i-1],g[x][i]=min(g[f[x][i-1]][i-1],g[x][i-1]);
	for (int i=head[x];i;i=edge[i].nxt) if (edge[i].to!=fa) dfsForLCA(edge[i].to,x,edge[i].w);
}
int LCA(int x,int y)//求LCA
{
	if (dep[x]<dep[y]) swap(x,y);
	int temp=dep[x]-dep[y];
	for (int i=0;temp;i++,temp>>=1) 
		if (temp&1) x=f[x][i];
	if (x==y) return x;
	for (int i=30;i>=0;i--)
		if (f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
int query(int x,int y)//求路径长
{
	if (dep[x]<dep[y]) swap(x,y);
	int temp=dep[x]-dep[y],ans=INT_MAX;
	for (int i=0;temp;i++,temp>>=1) if (temp&1) ans=min(ans,g[x][i]),x=f[x][i];
	return ans;
}
bool cmp(int x,int y) {return id[x]<id[y];}
int s[_SIZE+5],top;
void build()
{
	sort(h+1,h+k+1,cmp);
	s[top=1]=1,tot=0,head[1]=0;
	for (int i=1;i<=k;i++)
		if (h[i]!=1)
		{
			int lca=LCA(h[i],s[top]);
			if (lca!=s[top])
			{
				while (id[lca]<id[s[top-1]]) 
					AddEdge(s[top-1],s[top],query(s[top-1],s[top])),top--;
				if (id[lca]!=id[s[top-1]]) 
					head[lca]=0,AddEdge(lca,s[top],query(lca,s[top])),s[top]=lca;
				else 
					AddEdge(lca,s[top],query(lca,s[top])),top--;
			}
			head[h[i]]=0,s[++top]=h[i];
		}
	for (int i=1;i<top;i++) AddEdge(s[i],s[i+1],query(s[i],s[i+1]));
}
int dp[_SIZE+5];
bool flag[_SIZE+5];
void DP(int x,int fa)
{
	for (int i=head[x];i;i=edge[i].nxt)
	{
		int twd=edge[i].to;
		if (twd==fa) continue;
		DP(twd,x);
		if (flag[twd]) dp[x]+=edge[i].w;
		else dp[x]+=min(edge[i].w,dp[twd]);
		dp[twd]=0,flag[twd]=0;
	}
}
signed main()
{
	read(n);mem(g,0x3f);
	for (int i=1;i<n;i++)
	{
		int u,v,c;read(u),read(v),read(c);
		AddEdge(u,v,c),AddEdge(v,u,c);
	}
	dfsForLCA(1,0,0);
	read(m);
	for (int i=1;i<=m;i++)
	{
		read(k);
		for (int j=1;j<=k;j++) read(h[j]),flag[h[j]]=1;
		build();
		DP(1,0);
		writewith(dp[1],'\n');
		flag[1]=0,dp[1]=0;
	}
	return 0;
}
posted @ 2022-10-06 15:57  Hanx16Msgr  阅读(27)  评论(0编辑  收藏  举报