背包问题-面试中的动态规划
背包问题-面试中的动态规划
序言
背包问题是动态规划(Dynamic Programming)中一类经典的问题,弄懂背包问题对于理解DP有很大的帮助。在程序员的面试当中也会有许多背包问题的缩影。本文收集了一些Online Judge平台中的例子来介绍各类背包问题的情形。面试当中遇到相关的DP问题,可以举一反三。
网上也有很多讲背包问题的文章,之所以写这篇文章是想作一个的总结,
以便自己以后查看。也希望能够给读者带来一些帮助。
如果想要系统了解背包问题的理论知识,
《背包问题九讲》是很好的学习资料,但是其中给出的伪代码到可运行的源代码差距还比较大,本文给出了所有的案例的源代码实现。
01背包
从最简单的01背包开始讲起。
先看一个题目吧
Q1:
在N个物品中挑选若干物品装入背包,最多能装多满?
假设背包的大小为V,第i个物品的大小为C[i]
注意事项:
你不可以将物品进行切割。
样例:
- 如果有4个物品[2, 3, 5, 7]
- 如果背包的大小为11,可以选择[2, 3, 5]装入背包,最多可以装满10的空间。
- 如果背包的大小为12,可以选择[2, 3, 7]装入背包,最多可以装满12的空间。
- 函数需要返回最多能装满的空间大小。
这道题目来自于lintcode-backpack
//AC code
class Solution {
private:
int V;
public:
Solution():V(0) {}
void ZeroOnePack(int F[], int C, int W) {
for (int v = V; v >= C; v--) {
F[v] = max(F[v], F[v-C] + W);
}
}
/**
* @param V: An integer V denotes the size of a backpack
* @param C: Given n items with size C[i]
* @return: The maximum size
*/
int backPack(int V, vector<int> C) {
int N = C.size();
this->V = V;
int f[V+1];
// 初始化
for (int i = 0; i <= V; i++) {
f[i] = 0;
}
for (int i = 1; i <= N; i++) {
ZeroOnePack(f, C[i-1], C[i-1]);
}
return f[V];
}
};
另外一个类似的题目:
Q2:
有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大?
这道题来自于lintcode-backpack-ii
只需要将Q1的AC代码中ZeroOnePack的第三个参数改为W[i-1],即可得到Q2的AC代码。
Q1是Q2的一种特殊形式,即物品的价值就是物品本身。因为Q1的问题是如何才能使得背包装的最满,意思就是说物品的体积就是物品的价值。Q2只不过是将价值换成了一种其他的说法。
Q1,Q2是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即F[i,v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
其状态转移方程便是:F[i,v] = max{F[i−1, v], F[i−1, v−Ci] + Wi}。
这个方程是什么意思呢?用通俗易懂的语言来描述:就是假设我们已经知道了有i件物品放入v背包中的方案,其获得的最大的价值用F[i,v]来表示。那么在这个方案中,我们来研究一下第i件物品的策略(放或者不放)。如果不放第i件物品,那么问题就转化为“前i−1件物品放入容量为v的背包中”,价值为F[i−1, v];如果放第i件物品,那么问题就转化为“前i−1件物品放入剩下的容量为v−Ci的背包中”,此时能获得的最大价值就是F[i−1, v−Ci]再加上通过放入第i件物品获得的价值Wi。
//伪代码
def ZeroOnePack(F, C, W)
for(int v = V; v >= C; v--)
F[v] = max(F[v], F[v-C]+W)
def main
int F[V+1]
//初始化
for(int i = 0; i <= V; i++)
F[0] = 0;
for(int i = 1; i <= N; i++)
ZeroOnePack(F, C[i], W[i]))
这儿使用了一个优化空间的手段,可以看到我们的状态转移方程中使用的是一个二维变量F[i,v],而在伪代码中这个二维变量变成了F[V]。因为我们最后要求的结果是N个物品放入到容量为V的包中所能获得的最大价值,所以我们只关心最后的F[N,V]这个值是多少?而F[N,V]依赖于F[N-1,V]和F[N-1,V-C[N]]。
可以看到在main函数中i是从1一直自增到N的,因此我们用F[V]来表示每次循环后的结果,即F[i,V]。那么当i变为i+1的时候,计算F[i+1,V]时用到的F[i,V-C[i+1]],即F[V-C[i+1]]已经被覆盖(V-C[i+1] < V)。这儿只要从v=V自减便能解决这个问题。
还有一个关于初始化的问题:关于背包问题,通常有两种不太相同的问法。有的题目要求“恰装满背包”时的最优解,有的题目并没有要求必须把背包装满。针对于这两种不同的问法,实现时表现在于初始化的时候有所不同。如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其它F[1..V]均设为−∞,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0..V]全部设为0。
再看一个要求正好装满的题目吧
Q3:给出一个都是正整数的数组 nums,其中没有重复的数。从中找出所有的和为 target 的组合个数。
一个数可以在组合中出现多次。数的顺序不同则会被认为是不同的组合。
样例:
给出 nums = [1, 2, 4], target = 4
可能的所有组合有:
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]
[4]
返回 6
这道题目来自于lintcode-backpack-vi
//AC code
class Solution {
public:
int backPackVI(vector<int>& nums, int target) {
// Write your code here
int dp[target+1];
for(int i = 0; i < target + 1; i++) {
dp[i] = 0;
}
dp[0] = 1;
for (int i = 1; i <= target; ++i) {
for (auto a : nums)
if (i >= a) {
dp[i] += dp[i - a];
}
}
return dp[target];
}
};
完全背包问题
Q3中的场景明显与Q1和Q2不相同,Q1和Q2当中,物品的最大一个特点是:每件物品只能使用一次。
Q3描述的是一个新的问题:完全背包问题。在完全背包问题当中,每件物品的数量是没有限制的。
这儿再次描述一下完全背包问题:有N种物品和一个容量为V的背包,每种物品都有无限件可用。放入第i种物品
的费用是Ci,价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大[1]。
注意到完全背包问题是在01背包问题的基础上将每件物品的数量限制去掉了。
那么,很自然地,我们可以将完全背包问题转换成为01背包问题求解。
先给出状态转移方程: F[i,v]=max(F[i−1,v],F[i,v−Ci]+Wi)
这个状态转移方程的意思是:将前i件物品放入容量为v的背包中获取到的最大价值等价于当第i件
物品一件都不放入时,前i-1件物品放入容量为v的背包中获取到的最大价值;或者当放入第i件物品
时,前i件物品放入容量为v-Ci的背包中获取到的最大价值,注意这儿并不是前i-1件而仍然是前i件。
//伪代码
def CompletePack(F, C, W)
for v <-- C to V
F[v] = max{F[v], F[v-C]+W}
上述伪代码和01背包伪代码唯一的区别只有v的循环次序不同。
再回过头看一眼Q3:target即为背包容量,nums数组给出了每件物品的费用,物品本身的费用即为其价值。另外一类换零钱的问题也比较类似。
可以将target视为一张整钱,nums里面提供的是零钱的面额,此题即要求出换钱的方法和。
AC code中代码的意思是:dp[i]表示将nums中的物品放入容量为i的背包中的总的方法数目。因为问题是求总的换法数目,所以需要将动态转移方程中的max改为sum。dp[i]+=dp[i-a]的意思是有两种可能,a对应的物品不放入背包或者放入背包。
将Q3稍微改变一下:
Q4: 某个国家发行了3种不同的硬币,面值分别为1元,2元和4元。现在某人有一张面值为4元纸币,他需要将纸币换成硬币,问共有多少种换法?和Q3不一样的地方在于,序列[1,2,1]和[1,1,2],[2,1,1]等属于一种换法。
其实,这也是一道完全背包问题。假设F[i,j]表示只用前i种硬币将j兑换的种数,那么动态转移方程就是:
F[i,j] = F[i - 1, j] + F[i, j - C]
再做一下空间上的优化,代码如下:
class Solution {
public:
static int V;
template <class T>
void printArray(T arr[], int len) {
for(int i = 0; i < len; i++)
cout<<arr[i]<<" ";
cout<<endl;
}
void CompletePack(int F[], int C, int W) {
for(int v = C; v <= V; v++) {
F[v] = max(F[v], F[v - C] + W);
}
//printArray(F, V+1);
}
/**
* @param nums an integer array and all positive numbers, no duplicates
* @param target an integer
* @return an integer
*/
int backPackVI(vector<int>& nums, int target) {
//this is a Complete pack
int size = nums.size();
V = target;
int f[V+1];
for(int v = 0; v <= V; v++) {
f[v] = 0;
}
f[0] = 1;
for(int i = 1; i <= size; i++) {
//printf("index %d: call CompletePack -> C[%d] W[%d]\n", i, nums.at(i-1), nums.at(i-1));
CompletePack(f, nums.at(i-1), nums.at(i-1));
}
return f[V];
}
};
int Solution::V = 0;
updating..
多重背包
//Pseudo Code
def ZeorOnePack(F, C, W)
for( v = V; v >= C; v--)
F(v) = max(F(v), F(v-C) + W)
def CompletePack(F, C, W)
for(v = C; v <= V; v++)
F(v) = max(F(v), F(v-C) + W)
def MultiplePack(F, C, W, M)
if(C * M >= V)
CompletePack(F, C, V)
return
k = 1
while( k < M )
ZeroOnePack(F, kC, kW)
M -= k;
k *= 2;
ZeroOnePack(F, C * M, W * M)
def Main
for( v = 0; v < V; v++)
F(v) = 0
MultiplePack(F, C, W, M)
本文很多内容来自互联网,如有侵权,请联系作者
[参考文献]