基环树DP
基环树定义
先回忆一下普通树结构有哪些特性:
1. N个点 N-1条边。
2. 没有父节点的节点称为根节点,除根节点外每个几点都有一个父节点。
基环树可以理解为:
1. 有N个点N条边
2. 在原来一颗树的基础上,添加一条边,那么就是基环树了
同理,在基环树上的环中,删掉一条边,那么就是一颗普通树了。
也正是利用这个特性,在基环树上进行动态规划。
基环树如何进行DP
还是考虑这条性质
在基环树上的环中,删掉一条边,那么就是一颗普通树了
也就是说,如果进行正常的DP,在环中是无法处理的,没法进行状态转移。
做法也很简单,我们把环拆开。假设连在一起的两个点为u和v,能够这样处理的原因在于,u和v存在限制关系(往往是不能同时选择),这样拆开后,只要分别讨论,选择u不选择v,选择v不选择u两种情况,就可以得到最后正确的结果了。
例题 - BZOJ1040 骑士
分析
对于这道题来说,可以看出题目并不一定是一颗基环树,可能是基环树森林。做法比基环树多一步,就是对每一棵基环树进行操作,然后求和即可。
算法
存图采用的是链式向前星,由于题目中的树是无向边,所以要存两次,对于某条边来说,其正反(即由u到v和由v到u)边的序号的关系可以用,not_pass和 not_pass ^1 来表示。
存图后,首先进行dfs,找出基环树的环上一条边,然后设置好u和v以及not_pass(即要跳过的边)。然后开始进行dp操作,按照选择u不选择v和选择v不选择u分别进行DP,求得结果。
对于基环树的每棵树,都进行如上的操作。最后累加求和,即为答案。
代码
#include<bits/stdc++.h>
using namespace std;
const int nmax = 1000100;
typedef long long ll;
int head[nmax];
int tot = 0;
struct edge{
int to,nxt;
edge() {}
edge(int _u, int _v){
to = _v, nxt = head[_u];
}
}e[nmax<<1];
bool visit[nmax];
int val[nmax];
int not_pass, p1,p2,n;
ll dp[nmax][2];
void add(int u, int v){
e[tot] = edge(u,v);
head[u] = tot++;
}
void init(){
memset(head,-1,sizeof head);
memset(visit,0,sizeof visit);
tot = 0;
}
void dfs(int rt, int fa){
visit[rt] = true;
for(int i = head[rt];i!=-1;i = e[i].nxt){
if(e[i].to == fa) continue;
if(!visit[e[i].to]) dfs(e[i].to,rt);
else{
not_pass = i; // 标记边
p1 = e[i].to; // 标记u
p2 = rt; // 标记v
}
}
}
void getdp(int rt, int fa){
dp[rt][0] = 0, dp[rt][1] = val[rt];
for(int i = head[rt]; i!=-1; i=e[i].nxt){
if(e[i].to == fa) continue;
if(i == not_pass || i == (not_pass^1)) continue; // 如果当前是标记边,就跳过
getdp(e[i].to,rt);
dp[rt][0] += max(dp[e[i].to][0],dp[e[i].to][1]);
dp[rt][1] += dp[e[i].to][0];
}
}
int main(){
init();
scanf("%d",&n);
int v,t;
for(int i = 1;i<=n;++i){
scanf("%d %d",&v,&t);
val[i] = v; add(i,t); add(t,i);
}
ll ans = 0;
for(int i = 1;i<=n;++i){
if(visit[i]) continue;
dfs(i,-1);
getdp(p1,-1);
ll ans1 = dp[p1][0];
getdp(p2,-1);
ll ans2 = dp[p2][0];
ll temp = max(ans1,ans2);
ans += temp;
}
printf("%lld\n",ans);
return 0;
}