1. 程式人生 > 其它 >狀態壓縮DP(子集DP)經典題目

狀態壓縮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個任務未完成。
狀態轉移:

\[dp[state] = min( dp[state], dp[state\ 異或 \ sub] + 1) \]

說明:

  • 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];
    }
};