什么是0-1背包问题?
给定两个数组weight[N]和value[N]分别表示物品的重量和价值,现在有一个容量为s的背包,要求在背包中放置若干件物品使得物品的总价值最大。
怎么解决?
采用动态规划,令dp[i][j]表示在前i件产品中做选择且背包中物品重量小于等于j时所选择的物品的最大价值,显然第i件物品的选还是不选完全取决于之前的状态,状态转移方程为dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),如果dp[i][j] == dp[i-1][j]说明不选择第i件物品,背包中物品的重量仍然为j;如果dp[i][j] == dp[i-1][j-weight[i]] + value[i]说明选择第i件物品,背包重量增加了weight[i]且物品总价值增加了value[i]。最终的返回值为dp[N][s]。
没有进行空间优化的代码:
int knapsackSolve1(const vector<int>& weight, const vector<int>& value, int s)
{
if (weight.size() != value.size())
{
return 0;
}
int n = weight.size();
vector<vector<int> > dp(n, vector<int>(s + 1, 0));
for (int i = 1; i < n; ++i)
{
for (int j = weight[i]; j <= s; ++j)
{
if (dp[i - 1][j - weight[i]] + value[i] > dp[i - 1][j])
dp[i][j] = dp[i - 1][j - weight[i]] + value[i];
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n - 1][s];
}
进行空间优化的代码:
int knapsackSolve2(const vector<int>& weight, const vector<int>& value, int s)
{
if (weight.size() != value.size())
{
return 0;
}
int n = weight.size();
vector<int> dp(s + 1, 0);
for (int i = 1; i < n; ++i)
{
for (int j = s; j >= weight[i]; --j)//可以节省空间,j必须从大到小
{
//dp[j]是本轮的,dp[j - weight[i]]是上一轮的,所以必须保证dp[j]在dp[j - weight[i]]之前更新
if (dp[j - weight[i]] + value[i] > dp[j])
{
dp[j] = dp[j - weight[i]] + value[i];
}
}
}
return dp[s];
}
用背包思想解决数组分割问题:
有一个无序的、元素个数为2n的正整数数组,要求把这个数组分成两个长度为n的子数组,并使两个子数组的和最接近。
用dp[i][j][c]来表示前面i个元素取j个且这j个元素和不大于c(可以认为是背包的容量)的最佳方案(也就是最接近c的方案)。如果第i个元素不取,那么dp[i][j][c] = dp[i-1][j][c];如果第i个元素被选取,那么dp[i][j][c] = dp[i-1][j-1][c-a[i]]+a[i],因此可知动态规划的递推式为dp[i][j][c] = max(dp[i-1][j-1][c-a[i]]+a[i], dp[i-1][j][c])。
代码已经进行空间优化
int divideArray(const vector<int>& array)//已经去除最外围空间
{
int n;
if (array.size() % 2 != 0)
{
return 0;
}
else
{
n = array.size() / 2;
}
int sum = std::accumulate(array.begin(), array.end(), 0);
vector<vector<int>> dp(n + 1, vector<int>(sum / 2 + 2, 0));
for (int i = 1; i < array.size(); ++i)
{
for (int j = 1; j <= i && j <= n; ++j)//最多只需要选取n个元素
{
for (int c = sum / 2 + 1; c >= array[i]; --c)//从大到小,为了节约最外围空间
{
dp[j][c] = max(dp[j - 1][c - array[i]] + array[i], dp[j][c]);
}
}
}
return dp[n][sum / 2 + 1];
}
关于数组分割问题《编程之美》上有一个更好的解决方案:
维护一个二维数组,用来存储array子数组和,isOk[j][s]这个bool类型的数组表示array中是否存在j个数其和等于s。通过动态规划求出isOk[n][i]=true其中i是小于sum / 2 + 1的最大的整数。
代码如下
int divideArray2(const vector<int>& array)
{
int n;
if (array.size() % 2 != 0)
{
return 0;
}
else
{
n = array.size() / 2;
}
int sum = std::accumulate(array.begin(), array.end(), 0);
vector<vector<bool>> isOk(n + 1, vector<bool>(sum / 2 + 2, 0));
isOk[0][0] = true;//选取0个元素其和为0这种情况肯定为真
for (int i = 0; i < array.size(); ++i)
{
for (int j = 1; j <= n && j <= i; ++j)
{
for (int s = sum / 2 + 1; s >= array[i]; --s)
{
if (isOk[j-1][s - array[i]])
{
isOk[j][s] = true;
}
}
}
}
//选出最接近sum / 2 + 1的和
for (int s = sum / 2 + 1; s >= 0; --s)
{
if (isOk[n][s])
{
return s;
}
}
return 0;
}
上面的两种方法都求出了数组分割后子数组的和,但是子数组具体包含哪些元素并不能知道,下面的代码采用深度优先搜索的方法求数组中的N个元素且它们的和为定值。
//从array中获取和为sum且元素个数为n的子序列(子序列不要求连续)
bool fixedSumNSequence(const vector<int>& array, int start, int n, int sum, vector<int>& subArray)
{
if (subArray.size() >= n)
{
if (sum == 0)
{
return true;
}
else
{
return false;
}
}
for (int i = start; i < array.size(); ++i)
{
subArray.push_back(array[i]);
if (fixedSumNSequence(array, i + 1, n, sum - array[i], subArray))
{
return true;
}
subArray.pop_back();
}
return false;
}
bool fixedSumNSequence(const vector<int>& array, int n, int sum, vector<int>& subArray)
{
if (n >= array.size())
{
return false;
}
return fixedSumNSequence(array, 0, n, sum, subArray);
}
测试用例:
int main()
{
vector<int> weight = { 0, 10, 20, 30 };//在数组前面加上0元素是为了简便代码(动态规划的边界好确定)
vector<int> value = { 0, 60, 100, 120 };
cout << knapsackSolve1(weight, value, 50) << endl;
cout << knapsackSolve2(weight, value, 50) << endl;
vector<int> array = { 0, 1, 5, 7, 8, 9, 6, 3, 11, 20, 17, 12 };
cout << divideArray(array) << endl;
vector<int> subArray;
if (fixedSumNSequence(array, array.size() / 2, divideArray2(array), subArray))
{
for (auto p : subArray)
{
cout << p << " ";
}
cout << endl;
}
return 0;
}