骑士 ZJOI2008

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

N106.i,Wi106. 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 ) }

对于基环树,我们可以找到环,任意选择一条边断开,若这条边两端点为 V1V2 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;
}
    原文作者:骑士周游问题
    原文地址: https://blog.csdn.net/W_ang_/article/details/81837810
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞