LuoguP1273 有线电视网 树形DP

首先,我很机动,这还是蒟蒻第一次自主写出一道DP题目,没有看题解,虽然用了点数据来调试,但是毕竟是第一次,以后绝对会越来越好的.
传送门呐~

题目描述

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

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

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

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

输入格式

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

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

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

K A 1 C 1 A 2 C 2 … A k C k K A_1C_1 A_2 C_2 … A_k C_k KA1C1A2C2AkCk

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

输出格式

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

输入输出样例
输入 #1

5 3

2 2 2 5 3

2 3 2 4 3

3 4 2

输出 #1

2

思路与解析
第一眼看到这题我也是完全没有思路,脑阔里全四怎样暴力,然后我把自己的博客看了一遍(不用怀疑,我就是来安利的)顺着思路走,就有了以下的思考过程(大佬可跳过):

首先,理清楚这道题所要求的答案:在不亏钱的情况下最大的用户数,一般来说我们需要根据这个东西来设状态, d p [ x ] [ i ] dp[x][i] dp[x][i],第一维 x x x表示当前节点 x x x应该没问题叭,那么第二维呢?

我思考了一会,有两种可能: 1. d p [ x ] [ 0 / 1 ] 1.dp[x][0/1] 1.dp[x][0/1],表示该节点选或不选 2. d p [ x ] [ i ] 2.dp[x][i] 2.dp[x][i],表示该节点中用 i i i的费用.

现在就来分别思考状态转移.

你会发现,如果按照第一种来定义状态,那么如果一个非叶子节点中有2个叶子节点也就是用户,那么这种定义方法只有两种选择:两个都选或两个都不选.但是实际上有很多种选择:只选第一个;只选第二个;两个都选;两个都不选.这样就会少考虑很多种情况,那么最终得到的答案就不会是最优的.

而第二种方法的思想来源是01背包,因为你看每个用户是不是都有一个花费,和一个价值,只不过当两个叶子节点的LCA不是根节点时,花费就会重合,所以不能直接01背包,但我们是不是可以借用01背包的状态(其实就是自己想不出),那么再来考虑状态转移,然后我就发现边界是什么想不出了,因为总容量(也就是能用的钱数)不是固定的,而花费也不是固定的,就不好转移.

于是我冥思苦想,绞尽脑汁,发挥我的聪明才智,就连蒙带猜想到了这个:

设状态为 d p [ x ] [ i ] dp[x][i] dp[x][i]表示在以 x x x为根的子树中选 i i i个叶子节点所赚的钱(可能为负数,也就表示亏了钱),状态转移方程如下:
d p [ x ] [ i ] = m a x ( d p [ x ] [ i ] , d p [ s o n [ x ] [ j ] ] [ k ] + d p [ x ] [ i − k ] ) dp[x][i]=max(dp[x][i], dp[son[x][j]][k]+dp[x][i-k]) dp[x][i]=max(dp[x][i],dp[son[x][j]][k]+dp[x][ik])
s o n [ x ] [ j ] son[x][j] son[x][j]为节点 x x x的第 j j j个儿子

没看懂的再仔细思考一下,我们对于 x x x的每一个儿子,在 x x x的这个儿子更新后我们就有了 d p [ s o n [ x ] [ j ] ] [ 1 ⋯ s i z e [ s o n [ x ] [ j ] ] ] dp[son[x][j]][1\cdots size[son[x][j]]] dp[son[x][j]][1size[son[x][j]]] s i z e [ x ] size[x] size[x]指以x为根节点的子树中叶子节点的个数,不是子树大小),那么我们就可以用该子节点来更新 x x x,这样,可以先不考虑是否亏钱,因为有可能在上面的某一个节点更新答案时,它的另一个子节点赚了很多钱可以补上当前节点所亏的钱,然而如果这时候你为了不亏钱,而少选了一个点,最终答案就有可能会少一个点,就会有后效性,也就是说,只要保证在匹配到 x x x时选 i i i个点所要赚的钱越多(或者亏的钱越少)就行了.

最后的答案也很简单, i i i s i z e [ 1 ] size[1] size[1]到0,第一个 d p [ 1 ] [ i ] ≥ 0 dp[1][i]\ge0 dp[1][i]0 i i i就是我们所要求的最大用户数.

发一下弱弱的我的丑丑的代码

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#define R register int 
using namespace std;
const int N=3e3+5, M=3e3+5;

int n, m, tot;
int s[N], a[N], dp[N][N];, 

//s[x]指以x为根节点的子树中叶子节点的个数
int h[N], w[M], to[M], nex[M];//链式前向星存图

void add(int k){
	int x, y;
	scanf ("%d%d", &x, &y);
	nex[++tot]=h[k];
	to[tot]=x;
	w[tot]=y;
	h[k]=tot;
}

void dfs(int rot){
	if (a[rot]){//叶子节点的处理
		dp[rot][1]=a[rot];
		s[rot]=1;
		return ;
	}for (R i=h[rot];i;i=nex[i]){
		dfs(to[i]);
		s[rot]+=s[to[i]];
	}for (R j=m;j>=1;--j) dp[rot][j]=-1e9;
	for (R i=h[rot];i;i=nex[i])//遍历每个儿子
		for (R j=s[rot];j>=1;--j)//边界有点ex,调了我老久
			for (R k=j-s[to[i]];k<=j;++k)
				dp[rot][j]=max(dp[rot][j], dp[rot][k]+dp[to[i]][j-k]-w[i]);
}

int main (){
	int k;
	scanf ("%d%d", &n, &m);
	for (R i=1;i<=n;++i)	dp[i][0]=0;
	//每个节点都不选,那就是不亏不赚
	for (R i=1;i<=n-m;++i){
		scanf ("%d", &k);
		for (R j=1;j<=k;++j) add(i);
	}for(R i=n-m+1;i<=n;++i) scanf ("%d", &a[i]);
	dfs(1);
	for(R i=s[1];i>=0;--i)
		if(dp[1][i]>=0){//只要dp[1][i]>=0,就不亏,满足题意
			printf("%d\n", i);
			return 0;
		}

	return 0;
}

嘿嘿嘿,你竟然有耐心看完惹,也算没辜负我写了这么久,谢谢呐~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值