Description
给定一个 N N 个点、 N N 条边组成的图(可能有重边),其中含有若干个联通块,每个点有给定的点权 Wi W i 。
现在要求从 N N 个点中选择若干个点,使得选出的点中任意两点间没有直接连边,求出选出的点的点权和最大值。
输入 N N ,以下 N N 行,每行两个正整数 Wi W i 、 Vi V i ,表示第 i i 个点的点权和与第 i i 个点相连的一条边。
Simple input
3
10 2
20 3
30 1
Simple output
30
Range
N≤106.∀i,Wi≤106. N ≤ 10 6 . ∀ i , W i ≤ 10 6 .
Analyze
分析给定的图的类型。对于任意一个联通块,可以发现任意一点的度数最大为2。
- 当联通块中无重边时,这个联通快便是一个基环树。(基环树是在一个树中添加一条边形成的,有且仅有一个环)
- 当有一条重边时,去掉这一条重边,可以得到一颗树。
- 当有多条重边时,必定存在一点度数为0,与联通块矛盾。
因此,一个联通块要么是树,要么是基环树。
对于树形结构,可将树分解为若干个子树,分析是否有最优子结构和无后效性。一个树的最大点权和取决于树根是否选、和所有子树满足条件时的最大点权和。因此我们定义 dp(p,sign) d p ( p , s i g n ) , sign=0 s i g n = 0 时为以 p p 为根的子树在 p p 节点不选的情况下的最大点权和, sign=1 s i g n = 1 时为 p p 节点选的情况下最大点权和。
可以写出状态转移方程:
dp(p,sign)={Maxq∈{son(p)}{dp(q,0),dp(q,1)},Maxq∈{son(p)}{dp(q,0)},sign=0sign=1 d p ( p , s i g n ) = { M a x q ∈ { s o n ( p ) } { d p ( q , 0 ) , d p ( q , 1 ) } , sign=0 M a x q ∈ { s o n ( p ) } { d p ( q , 0 ) } , sign=1
最终答案为 Max{dp(ROOT,0),dp(ROOT,1)} M a x { d p ( R O O T , 0 ) , d p ( R O O T , 1 ) }
对于基环树,我们可以找到环,任意选择一条边断开,若这条边两端点为 V1、V2 V 1 、 V 2 ,分别以两个点为根进行两次DP。根据题目要求,这两个点最多只能选一个,因此最终答案为 Max{dp1(V1,0),dp2(V2,1)} M a x { d p 1 ( V 1 , 0 ) , d p 2 ( V 2 , 1 ) } 。
寻找环、判断结构时间复杂度为 Θ(N) Θ ( N ) ,DP时需要对树/基环树进行DFS,复杂度为 Θ(N) Θ ( N ) ,对于每个点的状态转移复杂度为 Θ(1) Θ ( 1 ) ,总时间复杂度为 Θ(N) Θ ( N ) 。
Summary
这是典型的树形DP模型,将树拆分成子树进行问题规模缩小是解决类似问题的常见思路。
题目涉及到了基环树,对于类似的图,可以对其进行删边等操作使其变为树,将问题化简。
Source
ZJOI 2008
Codes
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<cstdlib>
const int MAXN = 1E6 + 7;
using namespace std;
struct node_edge
{
int next;
int to;
};
struct node_edge edge[MAXN << 1];
int cnt_edge = 0;
int head[MAXN];
int W[1000006];
int N;
bool have_cal[1000006];
long long dp[1000006][2],ans,mx;
int ring1, ring2;
void work(int x);
void find_ring(int x, int pre);
void do_dp(int x, int fa);
void add_edge(int a, int b);
int read();
int main()
{
int t;
N = read();
memset(dp, 0, sizeof(dp));
memset(edge, 0, sizeof(edge));
memset(head, 0, sizeof(head));
for(int i = 1; i <= N; i++)
{
W[i] = read();
t = read();
add_edge(i, t);
add_edge(t, i);
}
for(int i = 1; i <= N; i++)
if(!have_cal[i])
work(i);
printf("%lld\n", ans);
return 0;
}
void work(int x)
{
ring1 = -1;
ring2 = -1;
find_ring(x, -1);
if(ring1 == -1 && ring2 == -1)
{
do_dp(x, -1);
ans += max(dp[x][0], dp[x][1]);
}
else
{
mx = 0;
do_dp(ring1, -1);
mx = max(mx, dp[ring1][0]);
do_dp(ring2, -1);
mx = max(mx, dp[ring2][0]);
ans += mx;
}
return;
}
void find_ring(int x, int pre)
{
have_cal[x] = true;
for(int i = head[x]; i; i = edge[i].next)
{
if(edge[i].to == pre)
continue;
if(!have_cal[edge[i].to])
find_ring(edge[i].to, x);
else
{
ring1 = x;
ring2 = edge[i].to;
}
}
return;
}
void do_dp(int x, int fa)
{
dp[x][0] = 0;
dp[x][1] = W[x];
for(int i = head[x]; i; i = edge[i].next)
{
if((edge[i].to == fa) || (x == ring1 && edge[i].to == ring2) || (x == ring2 && edge[i].to == ring1))
continue;
do_dp(edge[i].to, x);
dp[x][0] += max(dp[edge[i].to][1], dp[edge[i].to][0]);
dp[x][1] += dp[edge[i].to][0];
}
return;
}
void add_edge(int a, int b)
{
for(int i = head[a]; i; i = edge[i].next)
if(edge[i].to == b)
return;
edge[++cnt_edge].next = head[a];
edge[cnt_edge].to = b;
head[a] = cnt_edge;
return;
}
int read()
{
int res = 0;
char ch = getchar();
while(ch < '0' || ch > '9')
ch = getchar();
while(ch >= '0' && ch <= '9')
{
res = res * 10 + ch - 48;
ch = getchar();
}
return res;
}