题目大意
求一棵树的最大独立集(1 <= n <= 6000)
6000也太水了,一般都是1e6的规模
问题分析
- 问题抽象来说是最大独立集,具体在树中的体现题中已经明示。
- 图G(V, E)的最大独立集S(V’, E’)具有如下性质:E’ = {},即最大独立集中的点两两之间没有边连接
- dp[i][?]为节点i在?情况下的最大独立集总权重,? = 0 和 ? = 1分别表示不取/选取这一点,则
d p f , 0 = ∑ m a x { d p s o n , 0 , d p s o n , 1 } d p f , 1 = ∑ d p s o n , 0 + r f \begin{aligned} dp_{f,0} &= \sum max\{dp_{son,0}, dp_{son, 1}\} \\ dp_{f,1} &= \sum dp_{son,0} \ + \ r_f \end{aligned} dpf,0dpf,1=∑max{dpson,0,dpson,1}=∑dpson,0 + rf - 显然,父节点的计算依赖于子节点的计算,所以必须有顺序地计算dp,有一下几种手段:
- 从root节点dfs,回溯时计算(一条链时可能爆栈)
- 拓扑排序,依次计算(十分科学)
- 从root节点bfs层序遍历,然后在queue中逆序计算(利用树的性质,Nice!
- 标记每个节点子节点计算ok的数量,如果ok[i]==false && ok_son[i] = son[i].size,则计算这一点(基本保证每个点只算一次,但是无用的空循环可能不少,故不推荐)
- 类似地树形DP利用的都是父子节点间的转移关系。
- 下面的代码就是采用拙劣的标记法。
AC代码
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 6e3 + 10;
const bool debug = false;
int n;
int r[maxn];
vector<int> son[maxn];
int father[maxn];
int ok_son[maxn];
bool ok[maxn];
int dp[maxn][2];
void solve()
{
int root, cnt = n;
while(cnt>0){
if(debug)printf("Round #%d:\n", cnt-1);
for(int i=1;i<=n;i++){
if(!ok[i] && ok_son[i] == (int)son[i].size()){
root = i;
ok[i] = true, cnt--;
if(debug)printf("%d ",i);
if(son[i].size() == 0){
dp[i][0] = 0;
dp[i][1] = r[i];
ok_son[father[i]]++;
}else{
ok_son[father[i]]++;
dp[i][1] += r[i];
for(int j:son[i]){
dp[i][0] += max(dp[j][0], dp[j][1]);
dp[i][1] += dp[j][0];
}
}
}
}
if(debug)printf("\n");
}
if(debug)printf("root = %d\n", root);
printf("%d\n", max(dp[root][0], dp[root][1]));
}
int main()
{
scanf("%d", &n);
for(int i=1;i<=n;i++)
scanf("%d", &r[i]);
for(int i=0;i<n-1;i++)
{
int l,k;
scanf("%d%d", &l, &k);
father[l] = k;
son[k].push_back(l);
}
int t1, t2;
scanf("%d%d", &t1, &t2);
solve();
if(debug){
printf(" i 0 1\n");
printf("=========\n");
for(int i=1;i<=n;i++)
printf("%3d%3d%3d\n", i, dp[i][0], dp[i][1]);
}
return 0;
}