状态压缩DP(子集DP)经典题目
状态压缩DP(子集DP)
Leeetcode 1986. 完成任务的最少工作时间段
题意
链接:https://leetcode-cn.com/problems/minimum-number-of-work-sessions-to-finish-the-tasks
你被安排了 n 个任务。任务需要花费的时间用长度为 n 的整数数组 tasks 表示,第 i 个任务需要花费 tasks[i] 小时完成。一个 工作时间段 中,你可以 至多 连续工作 sessionTime 个小时,然后休息一会儿。
你需要按照如下条件完成给定任务:
- 如果你在某一个时间段开始一个任务,你需要在 同一个 时间段完成它。
- 完成一个任务后,你可以 立马 开始一个新的任务。
- 你可以按 任意顺序 完成任务。
给你 tasks 和 sessionTime ,请你按照上述要求,返回完成所有任务所需要的 最少 数目的 工作时间段 。
测试数据保证 sessionTime 大于等于 tasks[i] 中的 最大值 。
示例 1:
输入:tasks = [1,2,3], sessionTime = 3
输出:2
解释:你可以在两个工作时间段内完成所有任务。
- 第一个工作时间段:完成第一和第二个任务,花费 1 + 2 = 3 小时。
- 第二个工作时间段:完成第三个任务,花费 3 小时。
示例 2:
输入:tasks = [3,1,3,1,1], sessionTime = 8
输出:2
解释:你可以在两个工作时间段内完成所有任务。
- 第一个工作时间段:完成除了最后一个任务以外的所有任务,花费 3 + 1 + 3 + 1 = 8 小时。
- 第二个工作时间段,完成最后一个任务,花费 1 小时。
示例 3:
输入:tasks = [1,2,3,4,5], sessionTime = 15
输出:1
解释:你可以在一个工作时间段以内完成所有任务。
提示:
- n == tasks.length
- 1 <= n <= 14
- 1 <= tasks[i] <= 10
- max(tasks[i]) <= sessionTime <= 15
题解
状态定义dp[state]: 达到状态i的最少工作时间段,其中state是一个长度为 n 的二进制表示,state从低到高的第i位为i表示第i个任务已经完成,0 表示第i个任务未完成。
状态转移:
说明:
-
sub是state的子集
这里「子集」的含义为:sub 是state 的一个子集,当且仅当 sub 中任意的 1在state 中的对应位置均为 1。
-
state ^ sub, 就是state 和子集sub之间差的那一部分,差集
代码1
#define INF 0x3f3f3f3f
class Solution {
public:
int minSessions(vector<int>& tasks, int sessionTime) {
int n = tasks.size();
int N = 1 << n;
vector<int> dp(N, INF);
dp[0] = 0;
vector<int> sum(N, 0);
// 预处理状态sum[i]和dp
for (int i = 0; i < N; ++i) {
for (int k = 0; k < n; ++k) {
if (i & (1 << k)) sum[i] += tasks[k];
}
}
for (int i = 1; i < N; ++i) {
for(int j = 0; j <= i; j++){
// 集合i是否包含集合j
if((i | j) == i){
if(sum[j] <= sessionTime){
// 拆分子集进行状态转移,i ^ sub = 两者差集 i - sub
dp[i] = min(dp[i], dp[i ^ j] + 1);
}
}
}
}
return dp[N - 1];
}
};
代码2
更快的枚举属于集合i的子集合sub
细节
枚举 mask 的子集有一个经典的小技巧,对应的伪代码如下:
subset = mask
while subset != 0 do
// subset 是 mask 的一个子集,可以用其进行状态转移
...
// 使用按位与运算在 O(1) 的时间快速得到下一个(即更小的)mask 的子集
subset = (subset - 1) & mask
end while
#define INF 0x3f3f3f3f
class Solution {
public:
int minSessions(vector<int>& tasks, int sessionTime) {
int n = tasks.size();
int N = 1 << n;
vector<int> dp(N, INF);
dp[0] = 0;
vector<int> sum(N, 0);
// 预处理状态sum[i]和dp
for (int i = 0; i < N; ++i) {
for (int k = 0; k < n; ++k) {
if (i & (1 << k)) sum[i] += tasks[k];
}
}
for (int i = 1; i < N; ++i) {
for (int sub = i; sub; sub = (sub - 1) & i) {
if (sum[sub] <= sessionTime) {
// 拆分子集进行状态转移,i ^ sub = 两者差集 i - sub
dp[i] = min(dp[i], dp[i ^ sub] + 1);
}
}
}
return dp[N - 1];
}
};
Leeetcode 1494. 并行课程 II
题意
给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 dependencies 中, dependencies[i] = [xi, yi] 表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。
在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。
请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。
示例 1:
输入:n = 4, dependencies = [[2,1],[3,1],[1,4]], k = 2
输出:3
解释:上图展示了题目输入的图。在第一个学期中,我们可以上课程 2 和课程 3 。然后第二个学期上课程 1 ,第三个学期上课程 4 。
示例 2:
输入:n = 5, dependencies = [[2,1],[3,1],[4,1],[1,5]], k = 2
输出:4
解释:上图展示了题目输入的图。一个最优方案是:第一学期上课程 2 和 3,第二学期上课程 4 ,第三学期上课程 1 ,第四学期上课程 5 。
示例 3:
输入:n = 11, dependencies = [], k = 2
输出:6
提示:
1 <= n <= 15
1 <= k <= n
0 <= dependencies.length <= n * (n-1) / 2
dependencies[i].length == 2
1 <= xi, yi <= n
xi != yi
所有先修关系都是不同的,也就是说 dependencies[i] != dependencies[j] 。
题目输入的图是个有向无环图。
题解
比上题多了前导课程的概念,用pre[i]储存状态i需要的前置课程集合即可,思路基本差不多。
class Solution {
public:
int count_one_bits(unsigned int value)
{
int count = 0;
while(value){
value=value&value - 1;
count++;
}
return count;
}
int minNumberOfSemesters(int n, vector<vector<int>>& relations, int k) {
int N = 1 << n;
vector<int> pre_c(n, 0);
vector<int> dp(N, INT_MAX / 2);
for(int i = 0; i < relations.size(); i++){
pre_c[relations[i][1] - 1] |= 1<<(relations[i][0] - 1);
}
dp[0] = 0;
// 处理状态i的前导课程和初始化dp[i]
vector<int> pre(N, 0);
for(int i = 1; i < N; i++){
int count = 0;
for(int j = 0; j < n; j++){
if(i >> j & 1){
pre[i] |= pre_c[j];
count ++;
}
}
}
for(int i = 1; i < N; i++){
for(int sub = i; sub; sub = (sub - 1) & i){
// sub 表示要选的课程集合,i ^ sub表示已经选好的课程
if(count_one_bits(sub) > k) continue;
// 判断前导课程是否全部修完
if((pre[i] & (i ^ sub)) == pre[i]){
dp[i] = min(dp[i], dp[i ^ sub] + 1);
}
//cout<<bitset<4>(i)<<" "<<bitset<4>(sub)<<" "<<bitset<4>(pre[i])<<" "<<dp[i]<<endl;
}
}
return dp[N - 1];
}
};
Leetcode 1655. 分配重复整数
题意
题目描述
给你一个长度为n的整数数组nums,这个数组中至多有50个不同的值。同时你有 m个顾客的订单 quantity,其中,整数quantity[i]是第i位顾客订单的数目。请你判断是否能将 nums中的整数分配给这些顾客,且满足:
第i位顾客 恰好有quantity[i]个整数。
第i位顾客拿到的整数都是 相同的。
每位顾客都满足上述两个要求。
如果你可以分配 nums中的整数满足上面的要求,那么请返回true,否则返回 false。
样例
示例1
输入:nums = [1,2,3,4], quantity = [2]
输出:false
解释:第 0 位顾客没办法得到两个相同的整数。
示例2
输入:nums = [1,2,3,3], quantity = [2]
输出:true
解释:第 0 位顾客得到 [3,3] 。整数 [1,2] 都没有被使用。
示例3
输入:nums = [1,1,2,2], quantity = [2,2]
输出:true
解释:第 0 位顾客得到 [1,1] ,第 1 位顾客得到 [2,2] 。
示例4
输入:nums = [1,1,2,3], quantity = [2,2]
输出:false
解释:尽管第 0 位顾客可以得到 [1,1] ,第 1 位顾客没法得到 2 个一样的整数。
示例5
输入:nums = [1,1,1,1,1], quantity = [2,3]
输出:true
解释:第 0 位顾客得到 [1,1] ,第 1 位顾客得到 [1,1,1] 。
提示
n == nums.length
1 <= n <= 10^5
1 <= nums[i] <= 1000
m == quantity.length
1 <= m <= 10
1 <= quantity[i] <= 105
nums中至多有50个不同的数字。
题解
首先依旧用二进制表示满足的顾客状态。
f[i] [j] 表示前 i 个数字可以满足顾客状态j。
如果f[i] [j] == true && cost(k) <= w[i + 1],
\[f[i + 1] [j | k] = true \]具体含义为f[i] [j] 为真且第 i + 1 个数满足集合k所需的条件,所以可以转移,到f[i + 1] [j | k],j | k表示集合j和集合k合集。
cost(k) 表示状态k需要花费的代价,w[i + 1] 表示第i + 1 个数的个数
为了加快速度,集合k的枚举要枚举剩余元素的子集
for (int t = j ^ ((1 << m) - 1), k = t; k; k = (k - 1) & t){
}
时间复杂度
i -> i + 1
class Solution {
public:
bool canDistribute(vector<int>& nums, vector<int>& quantity) {
unordered_map<int, int> hash;
for (auto x: nums) hash[x] ++ ;
vector<int> w(1);
for (auto [x, y]: hash) w.push_back(y);
int n = hash.size(), m = quantity.size();
vector<int> s(1 << m);
for (int i = 0; i < 1 << m; i ++ )
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
s[i] += quantity[j];
vector<vector<int>> f(n + 1, vector<int>(1 << m));
f[0][0] = 1;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < 1 << m; j ++ )
if (f[i][j]) {
f[i + 1][j] = 1;
for (int t = j ^ ((1 << m) - 1), k = t; k; k = (k - 1) & t)
if (s[k] <= w[i + 1])
f[i + 1][j | k] = 1;
}
return f[n][(1 << m) - 1];
}
};
i- 1 -> i
class Solution {
public:
bool canDistribute(vector<int>& nums, vector<int>& quantity) {
int m = quantity.size(), N = 1<<m;
unordered_map<int, int>count;
for(int x: nums){
count[x] += 1;
}
vector<int> w;
for(auto &x: count){
w.push_back(x.second);
}
vector<int> cost(N, 0);
for(int i = 1; i < N; i++){
for(int j = 0; j < m; j++){
if(i >> j & 1){
cost[i] += quantity[j];
}
}
}
int n = w.size();
vector<vector<bool>>f(n + 1, vector<bool>(N, false));
f[0][0] = true;
for(int i = 1; i <= n; i++){// 从1开始枚举,方便获得f[i][j] = f[i - 1][j]
//cout<<i<<endl;
for(int j = 0; j < N; j++){
f[i][j] = f[i - 1][j];
//cout<<bitset<4>(j)<<"::"<<endl;
// !f[i][j] 求出f[i][j] 为正确的即可
for(int sub = j; sub && !f[i][j]; sub = (sub - 1) & j){
if(cost[sub] <= w[i - 1]){
f[i][j] = f[i - 1][j ^ sub];
}
//cout<<bitset<4>(sub)<<" "<<w[i-1]<<" "<<f[i][j]<<endl;
}
}
}
return f[n][N - 1];
}
};