基环树DP
基环树
在图论中,树是一种简单图 \(G=(V,E)\) ,其中 \(|V|=|E|+1\) ,且不存在环。
它有着这样的一个性质:如果在 \(G\) 中任意不相连两点间加上一条边,新图 \(G'=(V,E')\) 正好含有一个环。
而基环树就是从上述性质所得到的图 \(G'\) 。
因此,我们知道一棵基环树是由 \(n\) 个点和 \(n\) 条边所构成的。
如果删掉环上的一条边,基环树就退化成了树。
基环树DP
我们常常会遇到一类动态规划的题目是在基环树上进行的。
对于这类题目,我们一般有以下两种做法:
-
枚举每一条边,判断删去这条边后判断是否变成了一棵树,若是,则进行树形DP。当然与这条边相关的信息要特殊判断。
-
我们先从这棵基环树上找环,对于环上的每个节点所产生的内/外向树进行树形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\) 时最大的权值和。
那么不难得出这样的状态转移方程:
接下来我们考虑在环上的操作。
在这题中,我们只需考虑其中相邻两个点的选取情况即可。
也就是说,对于环上的任意相邻两点 \(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)\) 满足:
-
从 \(a\) 出发走 \(x\) 步和从 \(b\) 出发走 \(y\) 步会到达同一个点
-
在 \(1\) 的基础上如果有多解,那么要求 \(\max(𝑥,𝑦)\) 最小
-
在 \(1,2\) 的基础上如果有多解,那么要求 \(\min(𝑥,𝑦)\) 最小
-
在 \(1,2,3\) 的基础上如果有多解,那么要求 \(x\geq y\)
\(\large{\text{Solution:}}\)
对于题目中的 \(q\) 个询问,我们考虑在线求解。
每个询问中的 \(a,b\) 我们进行下述分类讨论:
-
\(a,b\) 不在同一棵基环树上
-
\(a,b\) 在基环树中环上的同一点的子树上
-
\(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;
}