虚树

在给定树上给出一些关键点,要求构造一棵树,满足所有关键点都在这棵树上,且树的形态与关键点在原树上的形态不变:即本不是祖先/后辈关系的点成为祖先/后辈是不允许的,本来是祖先/后辈的点如果在这棵树上也得是祖先/后辈。

更通俗一点的话,就是把所有关键点找出来,把两两关键点的 LCA 找出来,再根据之前的祖先后辈关系来连边,构成一棵新树。这棵新树就是原树的虚树。

一般可以在虚树上进行 DP,前提是这个 DP 只跟在虚树上的点和其它一些好维护的东西有关,比如两点距离(虚树把一条链的中间缩了,但这条链上的一些信息可以保留在虚树上对应的边上)。

先讲虚树构建方法,然后分析时间复杂度,再讲例题。

给出关键点集合,已知原树,求这个集合构成的虚树。

暴力一点,枚举任意两点及其 LCA,都放到集合里,再去重,再暴力构树。但是这样太暴力了。

朴素一点,把原树 dfs 一遍,一个点如果只有一个子树内有关键点,且自己不是关键点,就被缩,否则就不被缩。这样时间复杂度也不够优秀。

再回到第一个暴力做法,想一下去重后这个集合有多大。结合第二个做法,即最多有多少个节点自身不是关键点,且不只有一个子树内有关键点。

考虑到,每有一个上述说到的不只有一个子树内有关键点,且自身不是管简单的点,就说明至少有 2 个关键点在这个点为根的子树内。反过来——每两个关键点会诞生一个 LCA(或没有),然后这两个关键点没用了,而这个 LCA 会成为新的关键点。最多可以诞生关键点数量 -1 个新点。即——虚树大小跟关键点数量线性相关。

那么可以通过某种方法不重不漏地找出这些点,即可不与原树大小挂钩(虽然预处理需要)。

维护从根到当前要加入的点的链,从感性上:从左到右,在同一条链上就从上到下地加入关键点,如果关键点在这条链下面,就挂在这下面,否则找出加入点到维护链的第一个相交的点(即为维护链链底和加入点在原树上的 LCA)。由于加入的顺序问题,左边的已经加入完了,把链上的点从下往上删直到链上没有比刚才求的 LCA 深度更低的点,再把 LCA 和新加入点挂在链上。而虚树建边,在每一对点从链上删除之后再加边。加入完之后把当前维护的这条链上的边也加上。

具体实现,可以用单调栈。

具体加入顺序:按照 dfs 序排序。

一种很厉害的求 LCA 算法:欧拉序上求区间深度最小值在树上的编号。欧拉序每经过一个点就记录,即使递归一个儿子再回来时也要记录。欧拉序的长度为点数的两倍。可以 ST 表求区间最小,x 和 y 的 LCA 即为 x 的第一次出现时的欧拉序到 y 的第一次出现时的欧拉序的区间里深度最浅的那个点。

具体代码:

模板:[SDOI2011] 消耗战

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50;
int N,M;
struct Edge
{
	int x,y,Next; 
	long long Len;
}e[MAXN<<1];
int elast[MAXN],trlast[MAXN],tot;
void Add(int x,int y,long long Len)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Len=Len;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
void Addtr(int x,int y)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Next=trlast[x];
	trlast[x]=tot;
}
int In[MAXN],Out[MAXN],CNT;
int depth[MAXN];
long long Min[MAXN];
int Back[MAXN];
bool cmp(int a,int b)
{
	return In[a]<In[b];
}
struct STable
{
	int Id,Val;
	STable(){}
	STable(int _Id,int _Val)
	{
		Id=_Id;
		Val=_Val;
	}
}ST[MAXN][21];
void dfs(int u,int fa)
{
	In[u]=++CNT;
	Back[CNT]=u;
	depth[In[u]]=depth[In[fa]]+1;
	ST[In[u]][0].Id=In[u];
	ST[In[u]][0].Val=depth[In[u]];
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		Min[v]=min(Min[u],e[i].Len);
		dfs(v,u);
		ST[++CNT][0].Id=In[u];
		ST[CNT][0].Val=depth[In[u]];
	}
}
bool operator < (STable a,STable b)
{
	return a.Val<b.Val;
}
bool operator > (STable a,STable b)
{
	return a.Val>b.Val;
}
STable max(STable a,STable b)
{
	return a>b?a:b;
}
STable min(STable a,STable b)
{
	return a<b?a:b;
}
int Log2[MAXN];
int lowbit(int x)
{
	return x&-x;
}
void Init(int N)
{
	Log2[1]=0;
	for(int i=2;i<=N;i++)
	{
		if(i==lowbit(i))
		{
			Log2[i]=Log2[i-1]+1;
		}
		else
		{
			Log2[i]=Log2[i-1];
		}
	}
}
void GetMinST(int N)
{
	for(int i=N;i>=1;i--)
	{
		for(int j=1;i+(1<<j)-1<=N;j++)
		{
			ST[i][j]=min(ST[i][j-1],ST[i+(1<<j-1)][j-1]);
		}
	}
}
STable GetMin(int op,int l,int r)
{
	int t=Log2[r-l+1];
	return min(ST[l][t],ST[r-(1<<t)+1][t]);
}
int sta[MAXN],Top;
vector<int>Have;
void BuildTree(vector<int>Vec)
{
	bool HaveRoot=false;
	for(int j=0;j<Vec.size();j++)
	{
		if(Vec[j]==1)
		{
			HaveRoot=true;
			break;
		}
	}
	if(HaveRoot==false)
	Vec.push_back(1);
	sort(Vec.begin(),Vec.end(),cmp);
	
	Top=0;
	sta[++Top]=Vec[0];
	int LCA=0;
	for(int i=1;i<Vec.size();i++)
	{
		
		LCA=Back[GetMin(0,In[sta[Top]],In[Vec[i]]).Id];
		if(LCA==sta[Top])
		{
			sta[++Top]=Vec[i];
		}
		else
		{
			while(Top>1&&depth[In[sta[Top-1]]]>=depth[In[LCA]])
			{
				Addtr(sta[Top-1],sta[Top]);
				Top--;
			}
			if(sta[Top]!=LCA)
			{
				Addtr(LCA,sta[Top]);
				Top--;
				sta[++Top]=LCA;
			}
			sta[++Top]=Vec[i];
		}
	}
	while(Top>1)
	{
		Addtr(sta[Top-1],sta[Top]);
		Top--;
	}
	return;
}
vector<int>Vec;
long long f[MAXN];
int Color,Use[MAXN];
void dp(int u)
{
	Have.push_back(u);
	f[u]=0;
	int Son=0;
	for(int i=trlast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		dp(v);
		f[u]+=f[v];
		Son++;
	}
	if(Use[u]==Color)
	{
		f[u]=Min[u];
		return;
	}
	
	f[u]=min(f[u],Min[u]);
}
int main()
{
	scanf("%d",&N);
	for(int i=1;i<N;i++)
	{
		int x,y,Len;
		scanf("%d%d%d",&x,&y,&Len);
		Add(x,y,Len);
		Add(y,x,Len);
	}
	Min[1]=1e18;
	dfs(1,0);
	Init(CNT);
	GetMinST(CNT);
	scanf("%d",&M);
	while(M--)
	{
		Have.clear();
		Color++;
		int K;
		scanf("%d",&K);
		Vec.clear();
		for(int i=1;i<=K;i++)
		{
			int x;
			scanf("%d",&x);
			Vec.push_back(x);
			Use[x]=Color;
		}
		BuildTree(Vec);
		dp(1);
		printf("%lld\n",f[1]);
		tot=0;
		for(int i=0;i<Have.size();i++)
		trlast[Have[i]]=0;
	}
}

每次算完再清空的细节总是很多。也是写挂的常见错误点。

posted @ 2022-10-11 12:19  0htoAi  阅读(52)  评论(0编辑  收藏  举报