背包九讲 && 01背包问题 (求方案数 求具体方案 && 01背包变形拓展

背包九讲
精讲版

背包问题求方案数
此处需要注意,虽然是01背包的另一种问法,但此题需要把dp[1…V]初始化为负无穷。这样做的原因是,如果dp数组全部初始化为0,那么dp[k]的含义为容量为k时背包装的最大价值(注意此处不一定被装满了),那么这样就会造成重复计数的问题。若把dp[1…m]初始化为负无穷,那么最大价值一定是从0开始转移的,也就是说若dp[k]是最大价值,那么一定装满了容量k

初始化负无穷,可以得到从0转移过来的,关键是Max = 0。所有数组肯定还是有更新的,之所以能把从0转移过来的和其他的分开,就是因为给 dp[0] 赋成0了,不是从0转移过来的存的数据都是负无穷,所以维护最大值Max时,从其他状态转移过来的就直接无视掉了。
对于有限制的选择问题,比如恰好选 k k k 个,就可以这样初始化
code:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 9;
const int mod = 1e9 + 7;
int dp[N];
int num[N];
int Max = 0;
int main()
{
	int n, V, M,w, v, m, s;
	//若dp[1-V]=0 则dp的含义为容量为k时能装的最大价值 不一定用完k容量
    //因此把dp[1-V]=inf 这样处理后 dp[k]的含义为容量恰好为k时能装的最大价值
	memset(dp, -0x3f, sizeof(dp));
	num[0] = 1;
	dp[0] = 0;
	cin >> n >> V ;
	for(int i = 1; i <= n; ++i)
	{
		cin >> v >> w;
		for(int j = V; j >= v; --j)
		{
			if(dp[j] < dp[j - v] + w)
				num[j] = num[j - v], dp[j] = dp[j-v] + w;
			else if(dp[j] == dp[j - v] + w)num[j] = (num[j] + num[j - v]) % mod;
			Max = max(dp[j], Max);
		}
	}
	long long ans = 0;
	for(int i = 0; i <= V; ++i)
	{
		if(dp[i] == Max) ans += num[i], ans %= mod;
	}
	cout << ans << endl;
	return 0;
}

01背包求具体方案
因为要求字典序最小,那么我们肯定采取贪心策略(能选序号小的就选序号小的)

我们如果从前往后遍历所有的物品,那么最后 dp[n][m] 就是最后答案,那我们就得从后往前遍历才可以求的具体方案 ,但是这样所求的是字典序最大的

所以我们应该反一下,从后往前去遍历所有物品,这样dp[1][m]就是最后答案,那么我们就从前往后遍历就可以求具体方案,这样求的是字典序最小的

至于为什么要从最终答案倒退:因为真正答案是转移过程中产生的,我们并不知道何时产生的,产生答案的也不止一定只有一个,如果从开始遍历不确定终点。(可能是这样解释吧)

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 9;
int n, m, v[N], w[N];
int dp[N][N];
int main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
	for(int i = n; i >= 1; --i)
	{
		for(int j = 0; j <= m; ++j)// 这层必须从0开始
		{	
			dp[i][j] = dp[i+1][j];
			if(j >= v[i]) 
				dp[i][j] = max(dp[i][j], dp[i+1][j-v[i]] + w[i]);
		}
	}
	int vol = m;
	for(int i = 1; i <= n; ++i)
	{
		if(vol - v[i] >= 0 && dp[i][vol] == dp[i+1][vol-v[i]] + w[i])
		{
			cout << i << " ";
			vol -= v[i];
		}
	}
	return 0;
}

01背包

如果背包容量比较大,按之前的思路写会超时,该怎么办?

例题博客
思路:
普通的 01 01 01 背包是:容量为 j j j 时,装尽量大的价值(遍历的是容量范围
当背包容量很大的时候(一般给定的价值范围会比较小,这时候转换思维,价值为 j j j,确定尽量小的容量(遍历的是价值范围
要求容量最小,注意初始化最大,倒着找第一个满足容量范围的价值即可

Robberies
题意:
第一行给定 总被抓概率 p p p 和 银行数量 n n n
小偷去偷 n n n 个银行,总被抓概率 p p p (最后求得的总概率必须小于他,否则被抓),每个银行有一个可以偷到的价值 w i w_i wi 和被抓住的概率 v i v_i vi,求不被抓的情况下,最多偷到的物品总价值
思路:
首先概率是小数,而总价值是整数,并且范围小于等于 1 e 4 1e4 1e4,考虑物品价值作为背包容量,而直接用被抓的概率作为价值,这样是否合适?
假设我们偷了 k k k 个银行,那么我们没有被抓的概率,应该是 ( 1 − p 1 ) ∗ ( 1 − p 2 ) ∗ . . . ( 1 − p k ) (1-p_1)*(1-p_2)*...(1-p_k) (1p1)(1p2)...(1pk)
因此应该用成功脱身的概率作为价值
这样背包就确定了,用物品价值去确定尽量大的脱身概率
注意初始化,最后我们求得的 f [ i ] f[i] f[i] 代表的是偷总价值 i i i 的物品,脱身的概率为 f [ i ] f[i] f[i]
那么被抓的概率是 ( 1 − f [ i ] ) (1-f[i]) (1f[i]),倒着遍历找到满足 ( 1 − f [ i ] ) < q (1-f[i])<q (1f[i])<q 最大的 i i i 即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
ll n, m;
double f[maxn];

void work()
{
	double m;
	cin >> m >> n;
	for(int i = 0; i <= 10000; ++i) f[i] = 0;
	f[0] = 1;
	for(int i = 1; i <= n; ++i){
		ll w;double v;
		cin >> w >> v;
		for(int j = 10000; j >= w; --j)
			f[j] = max(f[j], f[j - w] * (1 - v));
	}
	for(int i = 10000; i >= 0; --i) if((1 - f[i]) < m){
		cout << i  << endl;return;
	}
}

int main()
{
	ios::sync_with_stdio(0);
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

给容量添加限制条件

HDU3466-Proud Merchants
题意:
n n n 个商人卖货物,每个商人只卖一件货物,货物的价格为 p i p_i pi,价值为 v i v_i vi,但是这里的商人比较自负,如果你手头没有 q i q_i qi 那么他会拒绝和你交易。有 m m m 的货币,问能获得的最大价值是多少。
在01背包的基础上,第 i i i 个物品只有在容量大于等于 q i q_i qi 时才能选择
Sample Input
n , m n,m n,m
p , q , v p,q,v p,q,v
2 10
10 15 10
5 10 5
3 10
5 10 5
3 5 6
2 7 3
Sample Output
5
11
思路:
先放题解
由于容量限制条件的存在,会产生物品选择顺序不同,得到答案不同的情况,也就是不满足无后效性这个条件
p p p 价格,即体积, q q q 容量限制, v v v 价值

模拟一组测试数据:
2 10
5 10 5
3 5 6
1)若先选择第一组先来:
背包所用容量: 0 1 2 3 4 5 6 7 8 9 10
第一遍循环:: 0 0 0 0 0 0 0 0 0 0 5
第二遍循环:: 0 0 0 0 0 6 6 6 6 6 6
2)若先选择第二组先来:
背包所用容量: 0 1 2 3 4 5 6 7 8 9 10
第一遍循环:: 0 0 0 0 0 6 6 6 6 6 6
第二遍循环:: 0 0 0 0 0 6 6 6 6 6 11
可见选择物品的顺序会影响最后的结果。
再看方程: d p [ j ] = m a x ( d p [ j ] , d p [ j − a r r [ i ] . p ] + a r r [ i ] . v ) dp[j] = max(dp[j], dp[j - arr[i].p] + arr[i].v) dp[j]=max(dp[j],dp[jarr[i].p]+arr[i].v)
结合测试数据发现,只有保证 d p [ j − a r r [ i ] . p ] dp[j - arr[i].p] dp[jarr[i].p] 最优,才能保证 d p [ j ] dp[j] dp[j] 最优,满足无后效性
若想使 d p [ j − a r r [ i ] . p ] dp[j - arr[i].p] dp[jarr[i].p] 最优,即要保证:
对于任意两组值: p 1 , q 1 , v 1   和   p 2 , q 2 , v 2 p_1, q_1, v_1 \ 和 \ p_2, q_2, v_2 p1,q1,v1  p2,q2,v2
考虑贪心算法,邻项交换法
假设先选择第一组,则若想满足无后效性,
那么 j − a r r [ 2 ] . p > = a r r [ 1 ] . q j-arr[2].p >= arr[1].q jarr[2].p>=arr[1].q ,且 j − a r r [ 1 ] . p < = a r r [ 2 ] . q j-arr[1].p <= arr[2].q jarr[1].p<=arr[2].q (否则可能出现,依赖先选 2 2 2 )计算的值取到更优的解)
由此推得: a r r [ 1 ] . q − a r r [ 1 ] . p < = a r r [ 2 ] . q − a r r [ 2 ] . p arr[1].q - arr[1].p <= arr[2].q - arr[2].p arr[1].qarr[1].p<=arr[2].qarr[2].p,得到如何去决定物品的顺序

当然如果不想这样证明,还有另外一种理解方式:
q < = p q<=p q<=p 的时候,这个物品无论何时选择,容量限制不会影响最优解,而 q > p q>p q>p 时,容量限制才会对最优解产生影响,并且 q − p q-p qp 越大,对最优解产生影响的可能越大,所以我们排序时让 q − p q-p qp 小的在前
q − p q-p qp 的差值越小,在每次循环时,与基本的 01 01 01 背包相比,不更新的值也就越少,也就越接近基本的01背包,求出的值也就相对越大。
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int f[maxn];
struct node
{
	int p, q, v;
	bool operator<(const node &B)const{
		return (q - p) < (B.q - B.p);
	}
}a[maxn];
void work()
{
	//cin >> n >> m;
	for(int i = 0; i <= m; ++i) f[i] = 0;
	for(int i = 1; i <= n; ++i)
	{
		cin >> a[i].p >> a[i].q >> a[i].v; 
	}
	sort(a + 1, a + 1 + n);
	for(int i = 1; i <= n; ++i)
		for(int j = m; j >= max(a[i].p, a[i].q); --j)
			f[j] = max(f[j], f[j - a[i].p] + a[i].v);
	cout << f[m] << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	while(cin >> n >> m)
	work();
	return 0;
}


01背包变形:两个条件最优化

UVA12563 劲歌金曲 Jin Ge Jin Qu hao----洛谷链接
vj
题意:
KTV里面有 n n n 首歌曲你可以选择,每首歌曲的时长都给出了. 对于每首歌曲,你最多只能唱 1 1 1 遍. 现在给你一个时间限制 t   ( t < = 1 0 9 ) t \ (t<=10^9) t (t<=109) , 问你在最多 t − 1 t-1 t1 秒的时间内可以唱多少首歌曲 n u m num num , 且最长唱歌时间是多少 t i m e ( t i m e 必 须 < = t − 1 ) time (time必须<=t-1) time(time<=t1) ? 最终输出 n u m + 1 num+1 num+1 t i m e + 678 time+678 time+678 即可.
注意: 你需要优先让歌曲数目最大的情况下,再去选择总时长最长的.
思路:
01背包板子题
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int t[55], Max, _ = 0;
struct node
{
	int num, time;
	bool operator<(const node &B)const{
		if(num != B.num) return num < B.num;
		else return time < B.time;
	}
}f[maxn];

void work()
{
	cout << "Case " << ++_ << ": ";
	cin >> n >> Max;
	memset(f, 0, sizeof(f));
	int sum = 0;//所有歌曲总时长
	for(int i = 1; i <= n; ++i)
		cin >> t[i], sum += t[i];
	Max = min(sum, Max - 1);
	int ans = 0;
	for(int i = 1; i <= n; ++i)
	{
		for(int j = Max; j >= t[i]; --j)
		{
			node tmp = {f[j - t[i]].num + 1, f[j - t[i]].time + t[i]};
			f[j] = max(f[j], tmp);
		}
	}
	cout << f[Max].num + 1 << " " << f[Max].time + 678 << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值