背包题目之——甜甜的背包算法糖们~~~

01背包:

题目一:采药

思路就是理解dp\left [ j \right ]=max(dp\left [ j \right ],dp[j-w[i]]+c[i])之后简化空间复杂度。

因为原本上就是加上上一轮的dp\left [ i-1 \right ]\left [ j \right ]只要省略前者就可以。

题目二:开心的金明

就是理解上题之后改变状态转移方程为 dp\left [ j \right ]=max(dp\left [ j \right ],dp[j-w[i]]+c[i]*w[i])

总结加反思(推理版):

一句废话: 一开始学习背包算法没有仔细考虑这个状态转移的细节,在这里做一个总结:

先引用 oiwiki 里的一句话解释为什么次主循环要以 j = V...0 的顺序来推:

  • 对于当前处理的物品 i 和当前状态 f_{i,j},在 j\geqslant w_i 时,f_{i,j} 是会被 f_{i,j-w_i} 所影响的。这就相当于物品 i 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法
  • 为了避免这种情况发生,我们可以改变枚举的顺序,从 W 枚举到 w_i,这样就不会出现上述的错误,因为 f_{i,j} 总是在 f_{i,j-w_i} 前被更新。

 如果没看懂,那就再看下面的解释:

其中的 f[v]=max{f[v],f[v-c[i]]} 一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将 v 的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v] 由 f[i][v-c[i]] 推知,与本题意不符。

如果你还没看懂,建议自己手推一下题目一的样例,如果懒得推下图就是最简版:

还有一点:初始化的细节问题

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

  • 如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0] 为 其它 f[1..V] 均设为 -\infty,这样就可以保证最终得到的 f[N] 是一种恰好装满背包的最优解。
  • 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 f[0..V] 全部设为0

为什么呢?可以这样理解:

初始化的 f 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为 nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是 -\infty 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装,这个解的价值为 0,所以初始时状态的值也就全部为 了。下面代码中也有解释。

 模版:

#include<bits/stdc++.h>
using namespace std;
#define int long long 
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
	const int N = 1e5+10;
	const int INF = 0x3f3f3f3f;
	int n, m;
	int dp[N], w[N], c[N];
	
	auto work=[]()
	{
		//初始化的区别
		memset(dp,0,sizeof(dp));//这是不用塞满的
//      dp[0] = 0;                这是塞满的 
//      L(i, 1, n) dp[i] = -INF; 
		cin >> n >> m;
		L(i, 1, m) cin >> w[i] >> c[i];
		L(i, 1, m)
		{
			L(j, w[i], n)
			{
				dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
			}
		}
		cout << dp[n];
	}; 
}
signed main()
{
	fastio;
	return Dino::work(), 0;
}

**************************************************************************************************************************************************************************************************************************** 

完全背包:

题目一:完全背包问题

题目大意:设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。

不难根据上面的推理,已经可以得出完全背包的状态转移方程了:

procedure CompletePack(cost,weight)
    for v=cost..V
        f[v]=max{f[v],f[v-c[i]]+w[i]}

总结加反思(复杂度版): 

 一个问题:完全背包01背包问题一样有 O(N*V) 个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态 f[i][v] 的时间是 O(v/c[i]) ,总的复杂度是超过 O(VN) 的。

为了将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。

一个简单有效的优化:

完全背包问题有一个很简单有效的优化,是这样的:若两件物品 ij满足 c[i]\leqslant c[j]且 w[i]\geqslant w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得 j 换成物美价廉的 i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。

这个优化可以简单的 O(N^2) 地实现,一般都可以承受。

 另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于 V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以 O(V+N) 地完成这个优化。

更高效的转化方法是:把第i种物品拆成费用为 c[i]\times2^k、价值为 w[i]\times2^k 的若干件物品,其中 k 满足c[i]\times 2^k\leqslant V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个 2^k 件物品的和。这样把每种物品拆成 O(log(V/c[i])) 件物品,是一个很大的改进。

但我们有更优的O(VN)的算法。但我不会告诉你,因为我已经讲过了就在上面。

**************************************************************************************************************************************************************************************************************************** 

多重背包:

总结加反思: 

 又一个问题:对于第 i 种物品有 n[i]+1 种策略:取 件,取 ……取 n[i] 件。令 f[i][v] 表示前 i 种物品恰放入一个容量为 v 的背包的最大权值,则有状态转移方程:f[i][v]=max(f[i-1][v-k*c[i]]+k*w[i]|0\leqslant k\leqslant n[i])  但复杂度为 O(V*\sum_{n[i]}^{})

 转化为01背包问题

  • 另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成 n[i] 01背包中的物品,则得到了物品数为 \sum_{n[i]}^{} 01背包问题,直接求解,复杂度仍然是 O(V*\sum_{n[i]}^{})
  • 但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第 i 种物品换成若干件物品,使得原问题中第 i 种物品可取的每种策略——0..n[i] ——均能等价于取若干件代换以后的物品。另外,取超过 n[i] 件的策略必不能出现 
  •  方法是:将第 i 种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^{k-1},n[i]-2^k+1,且k是满足 n[i]-2^k+1>0 的最大整数。例如,如果 n[i] 13,就将这种物品分成系数分别为 1,2,4,6 的四件物品。

这样就将第i种物品分成了 O(log n[i]) 种物品,将原问题转化为了复杂度为O(V*\sum_{logn[i]})01背包问题,是很大的改进。 

下面给出O(log amount)时间处理一件多重背包中物品的过程,其中amount表示物品的数量: 

procedure MultiplePack(cost,weight,amount)
    if cost*amount>=V
        CompletePack(cost,weight)
        return
    integer k=1
    while k<num
        ZeroOnePack(k*cost,k*weight)
        amount=amount-k
        k=k*2
    ZeroOnePack(amount*cost,amount*weight)
/*希望你仔细体会这个伪代码,如果不太理解的话,不妨翻译成程序代码以后,单步执行几次,或者头脑加纸笔模拟一下,也许就会慢慢理解了。*/

 模版:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
	const int N = 1e5+100;
	int n, m, cnt = 0;       // n是物品种数,m是总容量
	int num[N];              // num对应每种物品的个数 
	int dp[N], w[N], c[N];   // w是每个物品的重量 
	auto work=[]()           // c是每个物品的价值 
	{
		cin >> n >> m; 
		L(i, 1, n)
		{
			int x, y;
			cin >> num[i] >> x >> y;
			int k = 1;
			while(k <= num[i])
			{
				cnt++;
				w[cnt] = k * x;
				c[cnt] = k * y;
				num[i] -= k;
				k <<= 1;     // 等价于 k *= 2; 
			}
			if(num[i])
			{
				cnt++;
				w[cnt] = num[i] * x;
				c[cnt] = num[i] * y;
			}
		}
		n = cnt;
		L(i, 1, n)
			R(j, m, w[i])
				dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
		cout << dp[m];
	};
}
signed main()
{
	fastio;
	return Dino::work(), 0;
}

**************************************************************************************************************************************************************************************************************************** 

混合背包&二维费包:

没错,二维费包就是二维费用背包

 放在一块就是因为太相似了 —— —— 都太简单了

其实没有必要太复杂(——竟然不知不觉胡乱bb了4句了。。。。)

回归正题:

混合背包在哪里?

  • 如果将01背包、完全背包、多重背包,三者的杂交版放在一个题目中,那就是混合背包。
  • 故如果有三类物品,一类只能取一次,一类只能取 k 次,另外一类能取无数次,那么只需在对每个物品应用转移方程是,根据物品的类别选用顺序或逆序的循环即可,复杂度是 O(VN)

模版1:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
	const int N = 1e5+10;
	int n, m, cnt = 0;
	int dp[N], w[N], c[N], num[N];

	auto work=[]()
	{
		cin >> m >> n;
		L(i, 1, n)
		{
			int x, y, z;       //体积  价值  个数 
			cin >> x >> y >> z;
			if(z != 0 && z != 1)
			{
				int k = 1;
				while(k <= z)
				{
					cnt++;
					w[cnt] = k * x;
					c[cnt] = k * y;
					num[cnt] = k;
					z -= k;
					k <<= 1;
				}
				if(z)
				{
					cnt++;
					w[cnt] = z * x;
					c[cnt] = z * y;
					num[cnt] = z;
				}
			}else{
				cnt++;
				w[cnt] = x;
				c[cnt] = y;
				num[cnt] = z;
			}
		}
		L(i, 1, cnt)
		{
			if(!num[i])
			{
				L(j, w[i], m)
					dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
			}else{
				R(j, m, w[i])
					dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
			}
		}
		cout << dp[m];
	};
}
signed main()
{
	fastio;
	return Dino::work(), 0;
}

二维背包在哪里?

  •  二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。
  • 设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为 a[i] 和 b[i]。两种代价可付出的最大值(两种背包容量)分别为 V 和 U。物品的价值为 w[i]。费用加了一维,只需状态也加一维即可。设 f[i][v][u] 表示前 i 件物品付出两种代价分别为 v 和 u 时可获得的最大价值。状态转移方程就是:

  • f[i][v][u]=max(f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i])

  • 如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量 v 和 u 采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。

 模版2:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
	const int N = 505;
	const int M = 1e5+10;
	int vv, mm, n, cnt = 0;        //总体积  总质量 两个限制条件
	int dp[N][N], w[M], c[M], v[M];
	
	auto work=[]()
	{
		cin >> vv >> mm >> n;
		L(i, 1, n)
			cin >> v[i] >> w[i] >> c[i]; //体积  质量  价值
		L(k, 1, n)
			R(i, vv, v[k])
				R(j, mm, w[k])
					dp[i][j] = max(dp[i][j],dp[i-v[k]][j-w[k]]+c[k]);
		cout << dp[vv][mm];
	};
}
signed main()
{
	fastio;
	return Dino::work(), 0;
}

内个。。。就是因为太简单了就不给离题啦吧。。 

****************************************************************************************************************************************************************************************************************************

分组背包:

题目:通天之分组背包

  • 这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设 f[k][v] 表示前 k 组物品花费费用 v 能取得的最大权值,则有:
  • f[k][v]=max(f[k-1][v],f[k-1][v-c[i]]+w[i]) 物品 i 属于组 k

使用一维数组的伪代码如下:

for 所有的组k
    for v=V..0
        for 所有的i属于组k
            f[v]=max{f[v],f[v-c[i]]+w[i]}

要千万千万注意这三层循环的意思和顺序不能搞错了

再给出oiwiki上的核心代码做以借鉴

for (int k = 1; k <= ts; k++)          // 循环每一组
  for (int i = m; i >= 0; i--)         // 循环背包容量
    for (int j = 1; j <= cnt[k]; j++)  // 循环该组的每一个物品
      if (i >= w[t[k][j]])             // 背包容量充足
        dp[i] = max(dp[i],
                    dp[i - w[t[k][j]]] + c[t[k][j]]);  // 像0-1背包一样状态转移

 模版&题解:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
	const int N = 1e5+10;
	const int M = 505;
	int vv, n, T;
	int dp[N], t[M][M], w[N], c[N], tong[N];
	
	auto work=[]()
	{
		cin >> vv >> n;  //cin >> T;
		L(i, 1, n)
		{
			int x, y, z;     // 体积  价值  所属组号 
			cin >> x >> y >> z;
			T = max(T, z);   // 应题目要求本来这一句在上面就读入了
			t[z][++tong[z]] = i;
			w[i] = x;
			c[i] = y;
		} 
		L(k, 1, T)
			R(i, vv, 0)
				L(j, 1, tong[k])
					if(i >= w[t[k][j]])
						dp[i] = max(dp[i],dp[i-w[t[k][j]]]+c[t[k][j]]);
		cout << dp[vv];
	};
}
signed main()
{
	fastio;
	return Dino::work(), 0;
}

****************************************************************************************************************************************************************************************************************************

有关于基础背包类型到这里就结束了,我会在下一篇再给出变形背包和其他有意思的优化。

完结撒花

人生不能像做菜,把所有的料都准备好才下锅                           —— ——《饮食男女》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值