基环树DP

基环树

在图论中,树是一种简单图 \(G=(V,E)\) ,其中 \(|V|=|E|+1\) ,且不存在环。

它有着这样的一个性质:如果在 \(G\) 中任意不相连两点间加上一条边,新图 \(G'=(V,E')\) 正好含有一个环。

而基环树就是从上述性质所得到的图 \(G'\)

因此,我们知道一棵基环树是由 \(n\) 个点和 \(n\) 条边所构成的。

如果删掉环上的一条边,基环树就退化成了树。

基环树DP

我们常常会遇到一类动态规划的题目是在基环树上进行的。

对于这类题目,我们一般有以下两种做法:

  1. 枚举每一条边,判断删去这条边后判断是否变成了一棵树,若是,则进行树形DP。当然与这条边相关的信息要特殊判断。

  2. 我们先从这棵基环树上找环,对于环上的每个节点所产生的内/外向树进行树形DP,然后再在环上进行一次环形DP即可。

例题选讲

下面提供几道较为基础的题目:

例题1:[ZJOI2008]骑士

\(\large{\text{Description:}}\)

\(n\) 个骑士组成一个队,每个骑士有他的战力并恨一个人。

要求相互憎恨的人不同时在队里,怎样安排战斗力最大?

输出最大总战斗力即可。

\(\large{\text{Solution:}}\)

其实这题就是 没有上司的舞会 的基环树版本。

我们容易发现,\(n\) 个人中会存在多个联通块,而每个联通块有且只有一个环,即原图是个基环树森林。

于是在每棵基环树中,我们先找出其中的环,而对于环上的每个点 \(x\) ,我们分别对以 \(x\) 为根的子树中进行如下的树形DP:

\(dp[u][0]\) 表示在以 \(u\) 为根的这棵子树中不选节点 \(u\) 时最大的权值和; \(dp[u][1]\) 表示在以 \(u\) 为根的这棵子树中必选节点 \(u\) 时最大的权值和。

那么不难得出这样的状态转移方程:

\[dp[u][0]=\sum_{(u,v)\in E}\max\{dp[v][0],dp[v][1]\} \]

\[dp[u][1]=\sum_{(u,v)\in E}dp[v][0]+val[u] \]

接下来我们考虑在环上的操作。

在这题中,我们只需考虑其中相邻两个点的选取情况即可。

也就是说,对于环上的任意相邻两点 \(u,v\) ,我们需要强制要求其中某点不被我们选取。

将它们之间断边,分别跑一次树形DP即可。

实现起来就是: \(ans=ans+\max(dp[u][0],dp[v][0])\)

至此,本题已解决。

\(\large{\text{Code:}}\)

#include<bits/stdc++.h>
#define Re register
using namespace std;

typedef long long LL;

const int N=1000005;

struct Node {
	int ver,nxt;
}e[N<<1];
int cnt=1,hd[N];

int n,hte; LL a[N];

int U,V,E; LL dp[N][2],ans;

bool vis[N];

inline void add(int u,int v)
{
	cnt++;
	e[cnt].ver=v;
	e[cnt].nxt=hd[u];
	hd[u]=cnt;
}

inline void dfs1(int u,int f)
{
	vis[u]=1;
	for(Re int i=hd[u];i;i=e[i].nxt)
	{
		int v=e[i].ver;
		if((i^1)==f) continue;
		if(vis[v])
		{
			U=u,V=e[i].ver,E=i;
			continue;
		}
		dfs1(v,i);
	}
}

inline void dfs2(int u,int f)
{
	dp[u][0]=0;
	dp[u][1]=a[u];
	for(Re int i=hd[u];i;i=e[i].nxt)
	{
		int v=e[i].ver;
		if(i==E||(i^1)==E||(i^1)==f) continue;
		dfs2(v,i);
		dp[u][0]+=max(dp[v][0],dp[v][1]);
		dp[u][1]+=dp[v][0];
	}
}

int main()
{
	scanf("%d",&n);
	for(Re int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		scanf("%d",&hte);
		add(hte,i);
		add(i,hte);
	}
	for(Re int i=1;i<=n;i++)
	{
		if(vis[i]) continue;
		dfs1(i,0);
		dfs2(U,0);
		LL res=dp[U][0];
		dfs2(V,0);
		ans+=max(res,dp[V][0]);
	}
	printf("%lld",ans);
	return 0;
}

例题2:[POI2012]RAN-Rendezvous

\(\large{\text{Description:}}\)

给定 \(n\) 个节点的内向基环树森林,有 \(q\) 组询问。

每组询问形式为 \((a,b)\) ,求点对 \((x,y)\) 满足:

  1. \(a\) 出发走 \(x\) 步和从 \(b\) 出发走 \(y\) 步会到达同一个点

  2. \(1\) 的基础上如果有多解,那么要求 \(\max(𝑥,𝑦)\) 最小

  3. \(1,2\) 的基础上如果有多解,那么要求 \(\min(𝑥,𝑦)\) 最小

  4. \(1,2,3\) 的基础上如果有多解,那么要求 \(x\geq y\)

\(\large{\text{Solution:}}\)

对于题目中的 \(q\) 个询问,我们考虑在线求解。

每个询问中的 \(a,b\) 我们进行下述分类讨论:

  1. \(a,b\) 不在同一棵基环树上

  2. \(a,b\) 在基环树中环上的同一点的子树上

  3. \(a,b\) 在基环树中环上的不同点的子树上

对于情况 \(1\) 而言,显然无解,只需要用并查集的思想判断即可。

对于情况 \(2\) 而言,我们在找环时,先记录以环上的某点作为根所产生的子树包含哪些点,询问时只需判断两点的根是否相同,之后操作交给 \(\text{LCA}\) 即可。

对于情况 \(3\) 而言,先让 \(a,b\) 走到其子树的根节点,并记录下步数,然后由于环上所有边的方向相同,于是我们考虑两种情况:\(a\to b,b\to a\) 在这两种中取更优的即可。

于是便做完了此题。

\(\large{\text{Code:}}\)

#include<bits/stdc++.h>
#define Re register
using namespace std;

const int N=500005;

struct Node {
	int ver,nxt;
}e[N<<1];
int cnt,hd[N];

int n,k,d[N],f[N][25];

queue<int> q;

bool mk[N];

int frm[N],dep[N],tot,Lp[N],sz[N],vis[N];

inline int rd()
{
	char ch=getchar();
	int x=0,f=1;
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

inline void add(int u,int v)
{
	cnt++;
	e[cnt].ver=v;
	e[cnt].nxt=hd[u];
	hd[u]=cnt;
}

inline int LCA(int x,int y)
{
	if(dep[x]>dep[y]) swap(x,y);
	int h=dep[y]-dep[x];
	for(Re int i=21;i>=0;i--)
	{
		if(h&(1<<i))
		{
			y=f[y][i];
		}
	}
	if(x==y) return x;
	for(Re int i=21;i>=0;i--)
	{
		if(f[x][i]!=f[y][i])
		{
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}

inline void dfs(int u,int f,int rt,int D)
{
	dep[u]=D;
	frm[u]=rt;
	for(Re int i=hd[u];i;i=e[i].nxt)
	{
		int v=e[i].ver;
		if(v==f||!mk[v]) continue;
		dfs(v,u,rt,D+1);
	}
}

inline void Loop(int u,int id,int stp)
{
	if(vis[u]) return;
	vis[u]=stp;
	sz[id]++;
	Lp[u]=id;
	Loop(f[u][0],id,stp+1);
}

inline void GetMin(int x1,int y1,int x2,int y2)
{
	if(max(x1,y1)==max(x2,y2))
	{
		if(min(x1,y1)==min(x2,y2))
		{
			if(x1>=y1)
			{
				printf("%d %d\n",x1,y1);
				return;
			}
		}
		if(min(x1,y1)>min(x2,y2))
		{
			printf("%d %d\n",x2,y2);
			return;
		}
		if(min(x1,y1)<min(x2,y2))
		{
			printf("%d %d\n",x1,y1);
			return;
		}
	}
	if(max(x1,y1)>max(x2,y2))
	{
		printf("%d %d\n",x2,y2);
		return;
	}
	if(max(x1,y1)<max(x2,y2))
	{
		printf("%d %d\n",x1,y1);
		return;
	}
}

int main()
{
	scanf("%d%d",&n,&k);
	for(Re int i=1;i<=n;i++)
	{
		f[i][0]=rd();
		add(i,f[i][0]);
		add(f[i][0],i);
		d[f[i][0]]++;
	}
	for(Re int i=1;i<=n;i++)
	{
		if(!d[i])
		{
			q.push(i);
		}
	}
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		mk[p]=1;
		d[f[p][0]]--;
		if(!d[f[p][0]])
		{
			q.push(f[p][0]);
		}
	}
	for(Re int i=1;i<=n;i++)
	{
		if(!mk[i])
		{
			dfs(i,0,i,0);
			if(!vis[i]) Loop(i,++tot,1);
		}
	}
	for(Re int j=1;j<=21;j++)
	{
		for(Re int i=1;i<=n;i++)
		{
			f[i][j]=f[f[i][j-1]][j-1];
		}
	}
	while(k--)
	{
		int x=rd(),y=rd();
		if(Lp[frm[x]]!=Lp[frm[y]])
		{
			printf("-1 -1\n");
			continue;
		}
		if(frm[x]==frm[y])
		{
			int lca=LCA(x,y);
			printf("%d %d\n",dep[x]-dep[lca],dep[y]-dep[lca]);
			continue;
		}
		int res1=dep[x]+(vis[frm[y]]-vis[frm[x]]+sz[Lp[frm[x]]])%sz[Lp[frm[x]]];
		int res2=dep[y]+(vis[frm[x]]-vis[frm[y]]+sz[Lp[frm[y]]])%sz[Lp[frm[y]]];
		GetMin(res1,dep[y],dep[x],res2);
	}
	return 0;
}
posted @ 2020-12-25 21:02  kebingyi  阅读(469)  评论(0编辑  收藏  举报