P2014 [CTSC1997] 选课 or P1273 有线电视网(树型dp + 分组背包问题)

9 篇文章 0 订阅
6 篇文章 0 订阅

题目描述

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少?

输入格式

第一行有两个整数 N , M 用空格隔开。( 1≤N≤300 , 1≤M≤300 )

接下来的 N 行,第I+1 行包含两个整数 ki​和 si​, ki​ 表示第I门课的直接先修课,si​ 表示第I门课的学分。若 ki​=0 表示没有直接先修课(1≤ki​≤N , 1≤si​≤20)。

输出格式

只有一行,选 M 门课程的最大得分。

输入输出样例

输入 #1复制

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

输出 #1复制

13

解析:

在这道中我们可以提炼出,对于这一门课,我们可以选择修或不修,这时典型的01背包问题。

而他们又有联系,他们构造成一棵树。我们可以从根节点开始遍历,他还有个条件,就是需要选择预备课程要先修,才可以修下门课程。

这时我们可以定义在节点为u时,共有 子树 cnt 个 ,cnt-1 子树外 第 cnt 棵子树的节点为v,需要学修的课程容量为j,k为v子节点需要的容量为 k, dd 为 v子树的个数 

dp[ u ][ cnt ][ j ] = max(dp[ u ][ cnt ][ j ],dp[ u ][ cnt - 1 ][ j - k ] + dp[ v ][ dd ][ k])

我们可以利用01背包的性质,后一个状态是可以由前一个状态继承的关系。

变成滚动数组,这里可能会疑惑,dp[ v ][ dd ][ k ]它怎么解决,这里不用担心,因为回溯时,它的状态是从它的子节点开始的,所以dp[ v ][ dd ][ k ] 最终我们不需要遍历 v的子数组。

这时 就可以变成 :

 dp[ u ][ j ] = max(dp[ u ][ j ],dp[ u ][ j-k ]+dp[ v ][ k ]);

u代表当前根节点, v代表子节点,j代表背包容量,k代表v子数组的要的背包容量。

这里有个问题,你有没有想到呢?

在dfs时,是从根节点开始遍历到子节点的,而在回溯的过程中,子节点选了父节点没有选这是不是不符合题目规定的条件。

我的回答是不会,因为我们初始化时,当遍历到 u 节点时 ,我们初始化表示 dp[u][ 1] 只选u课程时的学分。因为滚动数组是从后面开始遍历的由前面没有改变的状态,开始转移的,我们尽可能把他装满,这时一定有选择到自己本身。

比如当容量为2时,它一定有遍历到dp[u][2] = max(dp[u][2] , dp[u][ 1 ] + dp[ v ][ 1 ] , dp[u] [0] + dp[v ][ 2 ]),而刚好 dp[ v ] [ 2 ] 为 0不可能选到。这就是01 背包问题。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N= 399;
vector<int> e[N];
int n,m;
int dp[N][N];
int size1[N];
int xue[N];
void dfs(int u)
{
	size1[u]=1;//整棵树的节点的多少 
	dp[u][1] = xue[u];
	for(int i =0;i < e[u].size();i++)
	{
		int v = e[u][i];//遍历子树  
		dfs(v);
		size1[u]+=size1[v];
		// dp[i][u][j] = max(dp[i][u][j] , dp[i-1][u][j-k] + dp[v][fullsize(v)][k]);
		for(int j = size1[u];j > 0;j--)
		{//滚动数组 从后先前更新  
			for(int k = 0;k < j;k++){
				dp[u][j] = max(dp[u][j],dp[u][j-k]+dp[v][k]); 
			}	
		}	
	} 
}
int main()
{
	cin >> n >> m;
	for(int i = 1;i <= n;i++)
	{
		int u,v;
		cin >> u >> xue[i];
		e[u].push_back(i);
	}
	dfs(0);
	cout << dp[0][m+1];
	return 0;
} 

 P1273 有线电视网

题目描述

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

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

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

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

输入格式

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

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

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

K  A1​  C1​  A2​  C2​  …  Ak​  Ck​

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

输出格式

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

输入输出样例

输入 #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,有线电视网就亏本了,而只让 ③④ 两个用户看比赛就不亏本了。

解析:

这道题和上面一道一的状态方程基本一样,唯一要注意的就是,入度为0时的处理。


#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=3e3+100;
typedef long long LL;
inline LL read(){LL x=0,f=1;char ch=getchar();	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;}
LL val[maxn],out[maxn],siz[maxn];
struct Edge{
    LL to,cost;
};
vector<Edge>g[maxn];
LL dp[maxn][maxn];
void dfs(LL u,LL fa){
     siz[u]=1;
     dp[u][0]=0;
     if(out[u]==0) dp[u][1]=val[u];///叶子的价值初始化
     for(LL i=0;i<g[u].size();i++){
         LL v=g[u][i].to;LL cost=g[u][i].cost;
         if(v==fa) continue;
         dfs(v,u);
         siz[u]+=siz[v];
         for(LL j=siz[u];j>=1;j--){
             for(LL k=1;k<=j;k++){
                 dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-cost);
             }
         }
     }
}
int main(void){
   cin.tie(0);std::ios::sync_with_stdio(false);
   LL n,m;cin>>n>>m;
   for(LL i=1;i<=n-m;i++){
       LL k;cin>>k;
       for(LL j=1;j<=k;j++){
           LL v,cost;cin>>v>>cost;
           g[i].push_back({v,cost});out[i]++;
           g[v].push_back({i,cost});
       }
   }
   for(LL i=1;i<=n;i++){
       if(out[i]==0){
          cin>>val[i];
       }
   }
 
 
   memset(dp,-0x3f,sizeof(dp));
   dfs(1,-1);
 
   LL ans=0;
   for(LL j=1;j<=n;j++){
       if(dp[1][j]>=0){
           ans=max(ans,j);
       }
   }
   cout<<ans<<"\n";
   return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值