分组背包是01背包的进阶问题,和01背包的思想基本类似,在背包进阶问题中是最简单的一类问题,但是难在它的衍生问题。要注意明晰分组背包与01背包的不同,理解状态转移方程的含义,而不是记住板子。
有 n 件物品和一个容量为 v 的背包。这些 物品被划分为 m 组,第i组的第j件物品的体积为c[i][j],价值为w[i][j],每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。
可见和01背包唯一的区别就是从每个物品最多拿一件变成了每组物品最多拿一件,既然最多拿一件没有像完全背包多重背包那样的限制,问题的难度其实并没有上升很多。
问题变成了每组物品有若干策略,也就是说我们每组可以选一件也可以不选,我们不妨定义状态dp[i][j]为前i组物品(每组最多选一个)恰好放入容量为j的背包的最大价值。
同样的,对于每个物品有两种选择:
则有状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
?
1
]
[
j
]
,
d
p
[
i
?
1
]
[
j
?
c
[
i
]
[
k
]
]
+
w
[
i
]
[
k
]
)
dp[i][j] = max(dp[i - 1][j] , dp[i - 1][j - c[i][k]] + w[i][k])
dp[i][j]=max(dp[i?1][j],dp[i?1][j?c[i][k]]+w[i][k])
对于n个物品容量为v的背包,显然有O(n*v)种状态,每次状态的转移消耗为O(1),所以时间复杂度为O(nv)
注意我们这里时间复杂度的计算是按照物品数目来计算而非组数,状态转移消耗是O(1)。
vector<vector<int>> dp(m + 1 , vector<int>(v + 1));
for (int i = 1; i <= n; i++)
for (int j = v; j >= 0; j--)
for (int k = 0; k < c[i].size() ; k++)
dp[i][j] = max(dp[i - 1][j] , dp[i - 1][j - c[i][k]] + w[i][k]);
我们前i组状态转移仍然是只跟前i - 1组有关,每个具体状态也只跟状态表中左上方矩形区域内的状态有关,因此我们仍然可以优化二维为一维,则有状态转移方程
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
?
c
[
i
]
[
k
]
]
+
w
[
i
]
[
k
]
)
dp[j] = max(dp[j] , dp[j - c[i][k]] + w[i][k])
dp[j]=max(dp[j],dp[j?c[i][k]]+w[i][k])
vector<int> dp(v + 1);
for (int i = 1; i <= n; i++)
for (int j = v; j >= 0; j--)
for (int k = 0; k < c[i].size() ; k++)
dp[j] = max(dp[j] , dp[j - c[i][k]] + w[i][k]);
首先抽象成分组背包问题。对于每个骰子可以看成一组,每个面为组内成员。那么我们就有了n组,n*k个物品,求恰好放入target容量背包内的最大方案数。
定义dp[i][j]为前i组恰好放入容量为j的背包的最大方案数,那么有递推
d
p
[
i
]
[
j
]
=
∑
d
p
[
i
?
1
]
[
j
?
x
]
,
j
>
=
x
且
x
<
=
k
dp[i][j] = \sum dp[i - 1][j - x],j >= x 且 x <= k
dp[i][j]=∑dp[i?1][j?x],j>=x且x<=k
对于初始状态dp[0][0]显然为1
然后跑板子即可
一维优化代码如下:
class Solution {
public:
const int mod = 1e9 + 7;
int numRollsToTarget(int n, int k, int target) {
vector<int> dp(target + 1);
dp[0] = 1;
for(int i = 1 ; i <= n ; i++)
for(int j = target ; j >= 0 ; j--)
{
dp[j] = 0;
for(int x = 1 ; j >= x && x <= k ; x++)
dp[j] = (dp[j] + dp[j - x]) % mod;
}
return dp[target];
}
};
对于这道题而言,矩阵的每一行是一组,我们要求的是每组必须选一个,得到的总重量和target之间差值的绝对值最小值
这就不是求最值问题了,而是求方案的可行性。
我们求出所有的可能的最终重量然后遍历维护最小绝对差即可
定义dp[i][j]为前i组每组拿一个恰好放入容量j的背包中是否可行
那么有
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
]
∣
d
p
[
i
?
1
]
[
j
?
c
[
k
]
]
dp[i][j] = dp[i][j]|dp[i - 1][j - c[k]]
dp[i][j]=dp[i][j]∣dp[i?1][j?c[k]]
状态初始化dp[0][mat[0][i]] = 1,然后跑板子即可
这里的代码利用了位图的位运算特性,也可以使用vector代替
class Solution {
public:
int minimizeTheDifference(vector<vector<int>>& mat, int target) {
bitset<5000> dp[71];
int m = mat.size() , n = mat[0].size();
for(int i = 0 ; i < n ; i++) dp[0][mat[0][i]] = 1;
for(int i = 1 ; i < m ; i++)
{
for(int j = 0 ; j < n ; j++)
dp[i] = dp[i] | (dp[i - 1] << mat[i][j]);
}
int ret = 10000;
for(int i = 0 ; i < 4901 ; i++)
if(dp[m - 1][i]) ret = min(ret , abs(i - target));
return ret;
}
};
滚动数组优化
由于位图空间开销极小,使用智能指针滚动数组优化对于空间利用没有太大提升,反而因为每次置0有了一丢丢的性能降低
class Solution {
public:
#define bs bitset<4901>
int minimizeTheDifference(vector<vector<int>>& mat, int target) {
auto dp = make_unique<bs>(1) , cur = make_unique<bs>();
for(auto& x : mat){
for(auto& y : x) *cur |= *dp << y;
swap(cur , dp);
*cur = 0;
}
int res = 4901;
for(int i = 0 ; i < 4901 ; i++)
if((*dp)[i])
res = min(res , abs(i - target));
return res;
}
};
如何抽象成分组背包问题呢?
显然k个栈就是k组,不过组内物品并不是栈的每一个元素,而是栈的前缀和,这样物品的体积就变成了前缀和的长度
然后就变成了分组背包裸题
一维优化代码如下:
class Solution {
public:
int maxValueOfCoins(vector<vector<int>>& piles, int k) {
int m = piles.size() , n;
vector<int> dp(k + 1);
for(int i = 1 ;i <= m ; i++)
{
n = piles[i - 1].size();
for(int x = 1 ; x < n ; x ++)
piles[i - 1][x] += piles[i - 1][x - 1];
for(int j = k ; j >= 0 ; j--)
for(int x = 0 ; x + 1 <= j && x < n ; x++)
dp[j] = max(dp[j] , dp[j - x - 1] + piles[i - 1][x]);
}
return dp[k];
}
};
通过三个简单例题也能感受到板子其实没什么难度,很多时候题目并不会那么直白直接给你分好组,有时候甚至都没有物品,这就要求我们对算法本身的理解,要能够把具体问题抽象为我们熟悉的问题,然后求解。
分组背包本身理解比较容易,但它将是依赖背包,树形DP,树上分组背包的基础。