P3177-树上染色 树形DP

4 篇文章 0 订阅

链接:https://www.luogu.com.cn/problem/P3177

题意:
给您一棵有 n 个点的树,树上的边有边权,让你在其中选择出k个黑点,其余的为白点,使得黑点与黑点的距离总和与白点和白点的距离总和的和最大, 让您求出这个值最大是多少。

思路:
我们在考虑多种状态后选出一种容易转移且可以保证正确性的状态,我选择的是 f[x][ j ] 表示以 x 的子树中选择 j 个黑点对于答案的最大贡献,那么,这个问题就转换成了一个树上的背包问题,即对于节点 x 每一个子节点 y 的子树 ,都可以选择若干个黑点(可以一个也不选择)(当然,x 节点也可以是黑点),这道题的答题思路大体如此。

统计答案,将题目中的统计答案变形,变成统计每一条边的边权乘上两边的黑点数量与边权乘上两边的白点数量的值的和,这个转化很好想,仔细思考下。

接下来是重点!!!

(推荐把接下来的两个问题看完,因为其中有一些东西需要体会)

两个问题:转移顺序问题 和 不合法情况去除问题

一. 转移顺序问题

我们在第一层的转移中,为了保证不重复转移(跟01背包压掉一维后的倒序转移一样),我们倒序转移。

在第二层的转移顺序的问题上,我认为:

1.正序转移和倒序转移在本质上并没有差别,但是在这道题中,对于当前枚举的子节点的子树,哪怕一个黑点也没有,它仍然可以对答案产生贡献,所以我们要先算上这种情况的贡献,否则在接下来的转移中,就会少计算本来就有的价值,从而答案错误。

2.正序枚举的好处在于,它会先枚举在当前枚举的子节点的子树中一个黑点也没有的情况,从而直接加上这种情况的贡献(不明白的话可以对着代码模拟一下就可以明白了),转移就变得比较方便。

3.第二层正序枚举为什么不会重复转移的问题在这里说一下,我们发现,我们第二层的枚举一直是在转移同一个状态(即我的代码中的 f[x][j] ),所以正序并不会用被当前枚举的子节点更新过的信息,所以并不会重复转移。

3.再来说说倒序,首先,倒序的正确性是可以保证的,但是在这道题中,一个黑点也不选的情况下,只要子树的大小改变,价值就会改变,所以当我们枚举到一个新的子节点的子树的时候,我们当前点的子树的大小会被这个新枚举的子节点的子树的大小所更新,但此时在我们的数组中,保存的仍然是子树大小没有被更新时的价值,所以我们要优先将其更新,具体代码实现可以看看 子谦。写的题解,我认为写得比较清晰。这样的话,倒序枚举也是丝毫没有问题的。

我认为神仙 popo 所说的更好理解,引用一下:

神仙popo:“但是这道题比较特殊,就是我们的k可以等于0,这就导致对 于每一个j,最后一个k一定会进行一次非法转移。通俗点讲, 最后一个转移是:f[u][j]=max(f[u][j],f[u][j]+f[v][0]+val); 这转移肯定会发生,并且我们用的来源状态f[u][j-k]由于k=0的 原因,已经不满足我们原本要求的“我们需要的原状态不会被在这 之前更新”了,因为f[u][j]已经不知道被更新多少次了。”

“当然为此我们下面就不能去计算k=0的转移了。 ”

二. 不合法情况去除问题

1.在我的代码中(下面有),我在刚开始的时候吧答案数组(即 f 数组)全部赋值了-1,-1表示这个状态不合法,我在程序刚开始的售后并不知道哪些状态合法,所以我先都赋值为-1,在DP的过程中发现合法的状态在改变它的值。

2.在我的代码的第二层循环中,我特判了 f[x][j - k] 的值为-1的情况,因为在我的枚举中,有可能会出现一种之前的最多黑点值不够转移的情况,请看如下这个例子:

比如说我们枚举在当前的子节点的子树中,我们枚举它里面有1个黑点,那么我们需要一个在其他子树中选了共 j - 1 个黑点的状态,但是如果其他的子树的大小总和还不到 j - 1 的话,那么这个状态显然是不合法的,所以我们要去除这种情况。

我们以上的例子扩宽一下,就可以得到如下策略:

我们枚举在当前的子节点的子树中,我们枚举它里面有k个黑点,那么我们需要一个在其他子树中选了共 j - k 个黑点的状态,但是如果其他的子树的大小总和还不到 j - k 的话,那么这个状态显然是不合法的,所以我们要去除这种情况。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=4020;

ll n,q;

ll tot=1;
ll head[maxn];
struct edge{
    ll u,v,w,nxt;
}e[maxn];
void addedge(int u,int v,int w){
    e[tot].u=u;
    e[tot].v=v;
    e[tot].w=w;
    e[tot].nxt=head[u];
    head[u]=tot;
    tot++;
}

ll sz[maxn];
ll dp[maxn][maxn];
//dp[u][j]表示到u点,子树中选了j个黑点对答案的贡献
void dfs(int u,int fa){
    //注意,当dp到u点时,更新的是所有u点与子节点连边(可能有多条)对答案产生的贡献
    //小心不要当成u与父节点那一条边了,那样到根节点之后做不了
    sz[u]=1;
    dp[u][0]=0;
    dp[u][1]=0;
    //dp[u][0]是u节点不选,dp[u][1]是u节点选
    //因为dp[u]记录的是子节点连边的贡献,与u选不选没有关系
    //所以在u点这步,两个记录的贡献都一样,但在u的父节点处产生区别
    
    //为什么要对dp做0和-1的标记?
    //因为这道题比较特殊,子树所有点都不选,即j=0,仍然会对答案产生贡献(因为白点的影响)
    //当k=0时,意味着子树白点选满,如果随意的加,会出现子树选了k>0个节点后又加上了白点选满的情况,造成错误
    //所以设置dp[u][0]=0,dp[u][1]=0,意思是可以由此转移产生新答案
    
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].v;
        int w=e[i].w;
        if(v==fa)continue;
        dfs(v,u);
        sz[u]+=sz[v];
        //更新子树大小  子树大小仅用于循环中优化运行速度,如果错误的计算大了也不会错
        for(ll j=min(sz[u],q);j>=0;j--){//第一个循环必须倒序(同理背包)
            for(ll k=0;k<=min(sz[v],j);k++){//第二个循环正序更新,便于先记录好白点选满(k=0)的情况
                if(dp[u][j-k]!=-1){//如果这个点可以转移到下一种状态
                    ll num=k*(q-k)+(sz[v]-k)*(n-sz[v]-q+k);
                    //该线段对答案产生的贡献=线段两侧黑点数量乘积+线段两侧白点数量乘积
                    dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]+w*num);
                }
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(0);
    memset(dp,-1,sizeof dp);
    int u,v,w;
    cin>>n>>q;
    for(int i=1;i<n;i++){
        cin>>u>>v>>w;
        addedge(u,v,w);
        addedge(v,u,w);
    }
    dfs(1,0);
    cout<<dp[1][q]<<endl;
    //system("pause");
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值