狀態壓縮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];
}
};