P1273 有线电视网(树形dp + 背包问题)

题目描述

某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。

从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。

现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。

写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。

输入格式

输入文件的第一行包含两个用空格隔开的整数N和M,其中 2 ≤ N ≤ 3000 2≤N≤3000 2N3000 1 ≤ M ≤ N − 1 1≤M≤N-1 1MN1 N N N为整个有线电视网的结点总数, M M M为用户终端的数量。

第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为 N − M + 1 N-M+1 NM+1 N N N

接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:

K A1 C1 A2 C2 … Ak Ck

K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。

输出格式

输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。

输入输出样例
输入 #1
5 3
2 2 2 5 3
2 3 2 4 3
3 4 2
输出 #1
2
说明/提示

样例解释
在这里插入图片描述
如图所示,共有五个结点。结点①为根结点,即现场直播站,②为一个中转站,③④⑤为用户端,共M个,编号从N-M+1到N,他们为观看比赛分别准备的钱数为3、4、2,从结点①可以传送信号到结点②,费用为2,也可以传送信号到结点⑤,费用为3(第二行数据所示),从结点②可以传输信号到结点③,费用为2。也可传输信号到结点④,费用为3(第三行数据所示),如果要让所有用户(③④⑤)都能看上比赛,则信号传输的总费用为:

2+3+2+3=10,大于用户愿意支付的总费用3+4+2=9,有线电视网就亏本了,而只让③④两个用户看比赛就不亏本了。

题解

这个题也是一个明显的树形dp和背包问题的结合,刚好前两天学习了一个树形dp和背包问题结合的案例,然后现在又碰到了这样的问题,大致思路都差不多(做题的时候死活算不出来答案,参考了大佬的题解总算磕磕碰碰的写完了)。

1.首先,理一下大致思路。这种树形dp和背包问题结合的一般是dfs遍历树,然后就是分组背包的一个选择问题。而这里构造这一棵树通常是采用链式前向星来构图。dfs遍历分解每个小的子树上面(我们只看最小的一颗子树,这样有利于我们推导状态转移方程)写出状态转移方程,这样这个题就算是完成了。

参数说明
  • v[u]:表示第 u 号用户愿意缴纳的电视费(在样例中用户编号时从③号开始的)。
  • d p [ i ] [ j ] dp[i][j] dp[i][j] i i i 表示第i件物品,即第 i 个节点(这里的i,j 的含义其实和普通背包问题里面的i,j 的表示的含义是一样的), j j j在这里的含义是第 j j j 个用户(即背包容量,在样例中背包容量为 3 3 3因为最多只有3个用户嘛,所以在样例中 j j j能取到的最大值是3)
  • edge[idx].to:表示第idx条边的终点(edge[idx].to里面存放的值是第 idx 条边的终点的值)。
  • edge[idx].w:第 idx 条边的权重。
  • edge[idx].next = head[u]:以 u 为起点的第 idx 条边的下一条边(这里存储的顺序和实际遍历的顺序是相反的,即存储时从上至下,而遍历时从下至上,可以参考链式前向星存图)。
  • head[u]:以u为起点的最后一条边在图中的编号。
状态转移方程

d p [ u ] [ j ] = m a x ( d p [ u ] [ j ] , d p [ u ] [ j − k ] + d p [ s o n ] [ k ] − e d g e [ u ] . w dp[u][j] = max(dp[u][j],dp[u][j-k] + dp[son][k] - edge[u].w dp[u][j]=max(dp[u][j],dp[u][jk]+dp[son][k]edge[u].w

解释:在最开始当背包容量为0的时候,也就是 j = = 0 j==0 j==0的时候,这个时候也就是没有给任何一位用户安排线路,这时候的开销自然时为 0 0 0,我们在初始化的时候将 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 都设置为0。我们用一棵子树来理解一下在这里插入图片描述
由②、③、④节点组成的子树,以为时dfs嘛,所有最先遍历的就是最深处的节点,所有它会最先遍历到叶子节点(这里只是举个例子,实际程序遍历可能不是先遍历这颗子树),遍历到叶子节点,这个时候就已经达到最深处了(假设时抵达了④号节点),也是达到了满足基例的条件(以下贴出基例条件代码)。

	if(u > n-m)
    {
        dp[u][1] = v[u];
        return 1;
    }

这个时候我们就把④号节点对应的背包容量(此时背包容量为 1 因为只有它自己一个用户嘛)的值赋值为用户愿意上缴的电视费,这时候我们将 1 返回给函数,然后进行下一步的背包决策处理,这里贴出决策部分代码

	int sum=0,t; // sum 在这里表示总的背包容量
    for(int i=head[u];i;i=edge[i].next)
    {
        int son = edge[i].to;
        t = dfs(son); sum += t;
        for(int j=sum;j>0;j--)
            for(int k=1;k<=t;k++)
                if(j>=k)
                    dp[u][j] = max(dp[u][j],dp[u][j-k]+dp[son][k]-edge[i].w);
    }

sum 表示的时用户数量,仍然以这颗子树为例,当到达④号节点时这时候返回了一个 1 ,sum += t,这时候 sum 的值变成了 1 (此时已经退出在④号节点的递归层了,此时是在其父节点的递归层②号节点),然后这时候就到了分组背包的选择了,因为暂且只有一组,所以这里的 d p [ 2 ] [ 1 ] = m a x ( d p [ 2 ] [ 1 ] , d p [ 2 ] [ 1 − k ] + d p [ 4 ] [ k ] − e d g e [ 4 ] . w ) dp[2][1] = max(dp[2][1],dp[2][1-k]+dp[4][k]-edge[4].w) dp[2][1]=max(dp[2][1],dp[2][1k]+dp[4][k]edge[4].w) 。son表示枚举到这一组(即k的儿子),i表示枚举到这组中的元素:选i个用户。
在这里插入图片描述
其他节点以此类推。

输出结果
最后输出dp[1][i]>=0的i的最大值,所以反向枚举。

贴出完整代码如下:

#include<bits/stdc++.h>
using namespace std;

const int N = 3010;
int dp[N][N],v[N],head[N];
int n,m;

int idx;

struct node
{
    int to,next,w;
}edge[10000010];

void add(int u,int v,int w)
{
    edge[++idx].to = v;
    edge[idx].w = w;
    edge[idx].next = head[u];
    head[u] = idx;
}

int dfs(int u)
{
    if(u > n-m)
    {
        dp[u][1] = v[u];
        return 1;
    }
    int sum=0,t;
    for(int i=head[u];i;i=edge[i].next)    / /该点连接了几个节点意为有几组,遍历每一组 
    {
        int son = edge[i].to;                // t为该组元素个数,或是说这个儿子为根的子树大小(这里的大小指子树中用户的个数),sum为该节点最多可以选多少个用户,即背包容量
        t = dfs(son); sum += t;
        for(int j=sum;j>0;j--)
            for(int k=1;k<=t;k++)
                if(j>=k)
                    dp[u][j] = max(dp[u][j],dp[u][j-k]+dp[son][k]-edge[i].w);
    }
    return sum;
}

int main()
{
    memset(dp,~0x3f,sizeof dp);         //初始化一个极大负值,因为dp可能为负
    scanf("%d%d",&n,&m);
    for(int u = 1;u<=n-m;u++)
    {
        int size,v,w;
        scanf("%d",&size);
        for(int j = 0;j<size;j++)
        {
            scanf("%d%d",&v,&w);
            add(u,v,w);
        }
    }
    for(int i = n-m+1;i<=n;i++)
        scanf("%d",&v[i]);
    for(int i = 1;i<=n;i++) dp[i][0] = 0;         //选0个用户的花费肯定是0啦
    dfs(1);
    for(int i = m;i>=1;i--)
        if(dp[1][i]>=0) 
        {
            printf("%d\n",i);
            break;
        }
    return 0;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值