The more, The Better

题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=1561
The more, The Better
Time Limit: 6000/2000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 7644 Accepted Submission(s): 4492

Problem Description

ACboy很喜欢玩一种战略游戏,在一个地图上,有N座城堡,每座城堡都有一定的宝物,在每次游戏中ACboy允许攻克M个城堡并获得里面的宝物。但由于地理位置原因,有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。你能帮ACboy算出要获得尽量多的宝物应该攻克哪M个城堡吗?

Input

每个测试实例首先包括2个整数,N,M.(1 <= M <= N <= 200);在接下来的N行里,每行包括2个整数,a,b. 在第 i 行,a 代表要攻克第 i 个城堡必须先攻克第 a 个城堡,如果 a = 0 则代表可以直接攻克第 i 个城堡。b 代表第 i 个城堡的宝物数量, b >= 0。当N = 0, M = 0输入结束。

Output

对于每个测试实例,输出一个整数,代表ACboy攻克M个城堡所获得的最多宝物的数量。

Sample Input

3 2
0 1
0 2
0 3
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
0 0

Sample Output

5
13

Author

8600

Source

HDU 2006-12 Programming Contest

Recommend

LL | We have carefully selected several similar problems for you: 1011 2159 2639 1203 2602

思路:有依赖关系的树上dp,很经典的题目;网上解法一大堆!但是,我在刚看的时候有很多不懂, 网上的题解又多数只给出程序或者一条状态转移方程,个人看起来很吃力.所以,说一下自己本来没搞懂的梗!以便遇到相似状况的朋友理解.
显然,要选择子节点,则必须先选父亲节点.所以就有了状态转移方程:dp[u][j]=max(dp[u][j], dp[u][k] + dp[v][j-k]); dp[i][j]表示在i子树攻打j个城堡能获得珠宝最大值;
我的疑问是:dp[u][j]所取策略的顶点集合J是否包含dp[v][k]所取策略的顶点集合K?(这里j>k) . 答案是否定的,因为如果一颗树从左到右有n棵子树的话,那么当我考虑第i棵子树的时候, dp[u][k]事实上表示的是只考虑前i - 1棵子树的最优策略,因而J和K是不可能相交的.这样,当把n棵子树全部考虑完毕时,dp[i][j]就确确实实表示在i子树攻打j个城堡能获得珠宝最大值了. 另外,关于i,j的遍历顺序,看过背包问题的应该都可以理解.
还有一个比较有用的优化,就是j从min(m,num)(num:已经遍历过的节点个数)开始递减,减少了很多无谓的操作,效率高很多, 看其他人的题解都是直接从m开始,让我看的一头雾水…Orz
见代码:


//邻接表实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <cstring>
#include <string>
#include <vector>

using namespace std;

int const maxn=210;
typedef vector<int> vec;
vector<vec> G;
int n,m;
int data[maxn];//珠宝数
int dp[maxn][maxn];//第i个节点选j个课程

void init()
{
    G.clear();
    G.resize(n+1);
    for (int i=0; i<=n; i++){
        for (int j=1; j<=m; j++){
            dp[i][j]=0;
        }
    }
}

int dfs(int u)
{
    dp[u][1]=data[u];//只选1
    int num=1;//根节点数
    for (int i=0; i<(int)G[u].size(); i++){
        int &v=G[u][i];
        num+=dfs(v);//以遍历节点数
        for (int j=min(num, m); j>0; j--){//j不可能大于min(num, m);
            for (int k=1; k<j; k++){//u选k,剩下给v.
                dp[u][j]=max(dp[u][j], dp[u][k] + dp[v][j-k]);
//这里的f[u][k]所代表的意义是:在遍历v这棵子树前f[u][k]的最优值.
//所以这里可以排除同一子树分支上f[u][k]和f[u][j]取值重复的bug,
//因为f[u][k]还没有考虑当前分支!!!(i递增,j递减)
            }
        }
    }
    return num;
}

int main()
{
    while (scanf("%d%d",&n,&m)==2)
    {
        if (!n&&!m) break;
        m++;//多选了0号点,切记无漏
        init();
        for (int i=1; i<=n; i++){
            int a, b;
            scanf("%d%d",&a,&b);
            data[i]=b;
            G[a].push_back(i);
        }
        dfs(0);
        printf("%d\n",dp[0][m]);
    }
    return 0;
}
//向前星实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <cstring>
#include <string>
#include <vector>

using namespace std;

int const maxn=210;
int to[maxn],head[maxn],Next[maxn];
int n,m;
int data[maxn];//珠宝数
int dp[maxn][maxn];//第i个节点选j个课程
int tol;
void init()
{
    for (int i=0; i<=n; i++){
        for (int j=1; j<=m; j++){
            dp[i][j]=0;
        }
        to[i]=Next[i]=head[i]=0;
    }
    tol=0;
}
void add(int a, int b)
{
    to[++tol]=b;//默认0号边为非法边
    Next[tol]=head[a];
    head[a]=tol;
}
int dfs(int u)
{
    dp[u][1]=data[u];//只选1
    int num=1;//根节点数
    for (int i=head[u]; i; i=Next[i]){
        int &v=to[i];
        num+=dfs(v);//以遍历节点数
        for (int j=min(num, m); j>0; j--){
        //j不可能大于min(num, m);优化比较明显,没有的话,时间15:220;
            for (int k=1; k<j; k++){//u选k,剩下给v.
                dp[u][j]=max(dp[u][j], dp[u][k] + dp[v][j-k]);
        //这里的f[u][k]所代表的意义是:在遍历v这棵子树前f[u][k]的最优值.
        //所以这里可以排除同一子树分支上f[u][k]和f[u][j]取值重复的bug,
        //因为f[u][k]还没有考虑当前分支!!!(i递增,j递减)
            }
        }
    }
    return num;
}

int main()
{
    while (scanf("%d%d",&n,&m)==2)
    {
        if (!n&&!m) break;
        m++;//多选了0号点,切记无漏
        init();
        int a;
        for (int i=1; i<=n; i++){
            scanf("%d%d",&a,&data[i]);
            add(a,i);
        }
        dfs(0);
        printf("%d\n",dp[0][m]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值