Leetcode状压DP

状压DP,是状态压缩和DP相结合,通常是将某个局面,或某种选择方案视为一个状态,状态与状态进行转移。
涉及一些位运算知识:

for(int i = 0;i < (1<<n);i++)   // 枚举所有的状态
i&(1<<j)   // 判断i的第j位
(i>>j)&1   // 判断i的第j位,且可以取值
for(int cur=s; cur>0; cur=(cur-1)&s)  // 枚举s的子集

解释一下s的子集枚举,假设s=13,即1101,s的所有子集如下:

1 1 0 1 
1 1 0 0 
1 0 0 1 
1 0 0 0 
0 1 0 1 
0 1 0 0 
0 0 0 1

\(2^3-1\)
通常,状态转移的时候是选择其中一个‘1’,但也有可能是选择其中一个子集,例如下面的情况:

for(int i = 0;i < (1<<n);i++) {
    for(int s = i;s;s=(s-1)&i) {
        ....
    }
}

它的时间复杂度是多少呢?\(O(3^n)\)
由于长度为\(n\)且包含\(k\)个1的二进制表示有\(C_{n}^{k}\)个,其有\(2^k\)个子集,动态规划的时间复杂度就是每个二进制表示的子集个数之和:\(\sum_{i=0}^{n} C_{n}^{i} {2^i} = 3^n\),因为它就是\(3^n\)的二进制展开。

Leetcode 1879. 两个数组最小的异或值之和

题解:问题模型就是两个数组的带权匹配,可以用状压DP、匈牙利算法KM、随机算法模拟退火。这里只介绍状压DP的做法。
dp[i]表示左边状态为i,右边选择到了第__bulitin_popcount(i)个,递推的时候相当于先算出1的个数为k的状态,在算出1的个数为k+1的状态

class Solution {
public:
    int minimumXORSum(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), ans = 0x3f3f3f3f;
        int dp[1<<n];
        memset(dp, 0x3f, sizeof(dp));
        dp[0] = 0;
        for(int i = 0;i < (1<<n); i++) {
            int cnt = __builtin_popcount(i);
            if(cnt > n)  continue;
            for(int j = 0;j < n;j++) {
                if(i & (1<<j)) {
                    dp[i] = min(dp[i], dp[i^(1<<j)] + (nums1[j] ^ nums2[cnt-1]));
                }
            }
            if(cnt == n)  ans = min(ans, dp[i]);
        }
        return ans;
    }
};

Leetcode 2172. 数组的最大与和

题解:把两组数组都扩展到2*m长度,就和上一题一样了

class Solution {
public:
    int maximumANDSum(vector<int>& nums, int m) {
        int n = nums.size(), ans = 0;
        int dp[1<<2*m];
        memset(dp, 0, sizeof(dp));
        for(int i = 0;i < (1<<2*m);i++) {
            int cnt = __builtin_popcount(i);  // 1的个数
            if(cnt > n) continue;
            for(int j = 0;j < 2*m;j++) {
                if((i>>j)&1) {
                    dp[i] = max(dp[i], dp[i^(1<<j)] + ((j/2+1) & nums[cnt-1]));
                }
            }
            if(cnt == n)  ans = max(ans, dp[i]);
        }
        return ans;
    }
};

Leetcode 1947. 最大兼容性评分和

题解:同1879

class Solution {
public:
    int cal(vector<int>& nums1, vector<int>& nums2) {
        int res = 0, n = nums1.size();
        for(int i = 0;i < n;i++)  res += (1-(nums1[i]^nums2[i]));
        return res;
    }
    int maxCompatibilitySum(vector<vector<int>>& students, vector<vector<int>>& mentors) {
        int n = students.size(), ans = 0;
        vector<int>dp(1<<n, 0);
        for(int i = 0;i < (1<<n);i++) {
            int cnt = __builtin_popcount(i);
            for(int j = 0;j < n;j++) {
                if(i & (1<<j)) {
                    dp[i] = max(dp[i], dp[i^(1<<j)] + cal(students[j], mentors[cnt-1]));
                }
            }
        }
        return dp.back();
    }
};

Leetcode 1595. 连通两组点的最小成本

题解:dp[i][[s]表示左边选择前i个,右边选择s,不过和前面相比,是从dp[i][s]往后推,而不是从前推出dp[i][s]。

class Solution {
public:
    int connectTwoGroups(vector<vector<int>>& cost) {
        int n = cost.size(), m = cost[0].size();
        int dp[n+1][(1<<m)];
        memset(dp, 0x3f, sizeof(dp));
        dp[0][0] = 0;
        for(int i = 1;i <= n;i++) {
            // cout << "i: " << i << endl;
            for(int s = 0;s < (1<<m);s++) {
                for(int j = 0;j < m;j++) {
                    dp[i][s|(1<<j)] = min(dp[i][s|(1<<j)], min(dp[i-1][s] + cost[i-1][j], dp[i][s]+cost[i-1][j]));
                }
            }
        }
        return dp[n][(1<<m)-1];
    }
};

Leetcode 1494. 并行课程 II

题解:把已经上完的课当做一个状态learned,当前状态下可选择的课为wait,枚举wait的子集作为一个选择,所有选择里取最小值。
参考链接:钰娘娘】1494. 并行课程 II 拓扑反例+状态压缩动态规划

class Solution {
public:
    int minNumberOfSemesters(int n, vector<vector<int>>& relations, int k) {
        int pre[n], dp[1<<n];
        memset(pre, 0, sizeof(pre));
        memset(dp, 0x3f, sizeof(dp));
        dp[0] = 0;
        for(auto x : relations) {
            pre[x[1]-1] |= (1<<(x[0]-1));
        }
        for(int learned = 0;learned < (1<<n);learned++) {
            int wait = 0;
            for(int i = 0;i < n;i++) {
                if((pre[i] & learned) == pre[i]) wait |= (1<<i);
            }
            wait = wait & (~learned);  // 只有在未学习的情况下才能学习
            for(int cur = wait;cur > 0;cur=(cur-1)&wait) {  // 枚举wait的一个子集学习
                if(__builtin_popcount(cur) > k)  continue;  // 子集1的个数不能超过k,这个nb!!
                dp[learned|cur] = min(dp[learned|cur], dp[learned] + 1);
            }
        }
        return dp[(1<<n)-1];
    }
};

Leetcode 1655. 分配重复整数

题解:dp[i][j] 表示cnts的前i个能否满足quantity的子集j。dp[i][j] = dp[i-1][j'] && sum[j&(~j)] <= cnts[i-1]
参考链接:【子集枚举】经典套路状压 DP](https://leetcode-cn.com/problems/distribute-repeating-integers/solution/zi-ji-mei-ju-jing-dian-tao-lu-zhuang-ya-dp-by-arse/)

class Solution {
public:
    bool canDistribute(vector<int>& nums, vector<int>& quantity) {
        unordered_map<int, int>freq;
        for(int i = 0;i < nums.size();i++) {
            freq[nums[i]]++;
        }
        vector<int>cnts;
        for(auto& x : freq)  cnts.push_back(x.second);

        int n = cnts.size(), m = quantity.size();
        vector<int>sum(1<<m, 0);
        for(int i = 0;i < (1<<m);i++) {
            for(int j = 0;j < m;j++) {
                if(i & (1<<j)) {
                    sum[i] = sum[i^(1<<j)] +  quantity[j];
                    break;
                }
            }
        }

        bool dp[n+1][1<<m];
        memset(dp, 0, sizeof(dp));
        for(int i = 0;i <= n;i++)  dp[i][0] = true;
        // dp[0][0] = true;
        for(int i = 1;i <= n;i++) {
            for(int j = 0;j < (1<<m);j++) {
                if(dp[i-1][j]) {dp[i][j] = true; continue;}  // 其实对应s=0的情况
                for(int s = j;s;s = (s-1)&j) {  // 当前选的
                    int pre = j & (~s);  // 之前选的
                    dp[i][j] = dp[i-1][pre] && (sum[s] <= cnts[i-1]);
                    if(dp[i][j])  break;
                }
            }
        }
        return dp[n][(1<<m)-1];
    }
};

LC 1986. 完成任务的最少工作时间段

题解:dp[i]枚举所有i的子集转移即可

class Solution {
public:
    int minSessions(vector<int>& tasks, int sessionTime) {
        int n = tasks.size();
        int sum[1<<n];
        memset(sum, 0, sizeof(sum));
        for(int i = 0;i < (1<<n);i++) {
            for(int j = 0;j < n;j++) {
                if(i & (1<<j)) {
                    sum[i] = sum[i^(1<<j)] + tasks[j];
                    break;
                }
            }
            // cout << i << " " << sum[i] << endl;
        }

        int dp[1<<n];
        memset(dp, 0x3f, sizeof(dp));
        dp[0] = 0;
        for(int i = 0;i < (1<<n);i++) {
            for(int s = i;s;s=(s-1)&i) {
                int pre = i & (~s);
                if(sum[s] <= sessionTime) {
                    dp[i] = min(dp[i], dp[pre]+1);
                }
            }
        }
        return dp[(1<<n)-1];
    }
};

LC 1434. 每个人戴不同帽子的方案数

题解:状压DP,dp[i][j]表示前i个帽子能满足子集j的方案数

const int mod = 1e9+7;

class Solution {
public:
    int numberWays(vector<vector<int>>& hats) {
        int n = hats.size();
        int dp[41][1<<n];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1;
        for(int i = 1;i <= 40;i++) {
            for(int j = 0;j < (1<<n);j++) {
                dp[i][j] = dp[i-1][j];
                for(int k = 0;k < n;k++) {
                    if(j & (1<<k)) {
                        if(find(hats[k].begin(), hats[k].end(),i) != hats[k].end()) {
                            dp[i][j] = (dp[i][j] + dp[i-1][j^(1<<k)]) % mod;
                        }
                    }
                }
            }
        }
        return dp[40][(1<<n)-1];
    }
};

LC 1799. N 次操作后的最大分数和

题解:dp[i]表示状态i的最大分数,可以由子集j转移而来,i减去两个1形成j

class Solution {
public:
    int gcd(int a, int b) {
        return b ? gcd(b, a%b) : a;
    }
    int maxScore(vector<int>& nums) {
        int n = nums.size();
        int dp[1<<n];
        memset(dp, 0, sizeof(dp));
        for(int i = 0;i < (1<<n);i++) {
            int cnt = __builtin_popcount(i);
            if(cnt%2)  continue;
            for(int k = 0;k < n;k++) {
                for(int t = k+1;t < n;t++) {
                    if((i&(1<<k)) && (i&(1<<t))) {
                        int j  = i ^ (1<<k) ^ (1<<t);
                        // cout << "j: " << j << " " << k << " " << t  << " " << cnt << endl;
                        dp[i] = max(dp[i], dp[j] + ((n-cnt)/2+1)*gcd(nums[k], nums[t]));
                        // cout << "dp: " << dp[i] << endl;
                    }
                }
            }
        }
        return dp[(1<<n)-1];
    }
};

LC 1349. 参加考试的最大学生数

题解:头插DP,也算是状压DP的一种吧,dp[i][j]表示前i行且最后一行的状态为j的最大学生数目,然后逐行转移,计算行与行之间的互斥性。

class Solution {
public:
    int maxStudents(vector<vector<char>>& seats) {
        int n = seats.size(), m = seats[0].size();
        int dp[n+1][1<<m];
        memset(dp, 0, sizeof(dp));
        for(int i = 1;i <= n;i++) {
            for(int j = 0;j < (1<<m);j++) {
                for(int k = 0;k < (1<<m);k++) {
                    int cnt = 0;
                    for(int t = 0;t < m;t++) {
                        if(seats[i-1][t] == '#')  continue;
                        if(t>0 && (j & (1<<(t-1)))) continue;
                        if(t>0 && (k & (1<<(t-1)))) continue;
                        if(t < m-1 && (j & (1<<(t+1))))  continue;
                        if(t < m-1 && (k & (1<<(t+1)))) continue;
                        if(k & (1<<t))  cnt++; 
                    }
                    dp[i][j] = max(dp[i][j], dp[i-1][k] + cnt);
                }
            }
        }
        return *max_element(dp[n], dp[n]+(1<<m));
    }
};

LC 1681. 最小不兼容性

题解:子集枚举,要求子集的大小为kk,将其作为新的一组

class Solution {
public:
    bool check(int s, int n, vector<int>& nums) {
        unordered_map<int,int>mp;
        for(int i = 0;i < n;i++) {
            if(s & (1<<i))  mp[nums[i]]++;
        }
        for(auto x : mp) {
            if(x.second > 1)  return false;
        }
        return true;
    }
    int cal(int s, int n, vector<int>& nums) {
        int mymax = 0, mymin = 0x3f3f3f3f;
        for(int i = 0;i < n;i++) {
            if(s & (1<<i)) {
                mymax = max(mymax, nums[i]);
                mymin = min(mymin, nums[i]);
            }
        }
        return mymax-mymin;
    }
    int minimumIncompatibility(vector<int>& nums, int k) {
        int n = nums.size();
        int kk = n/k;
        int valid[1<<n], cha[1<<n],oneCnt[1<<n];
        for(int i = 0;i < (1<<n);i++) {
            valid[i] =  check(i, n, nums);
            cha[i] = cal(i, n, nums);
            oneCnt[i] = __builtin_popcount(i);
        }

        int dp[1<<n];
        memset(dp, 0x3f, sizeof(dp));
        dp[0] = 0;
        for(int i = 0;i < (1<<n);i++) {
            if(oneCnt[i] % kk)  continue;
            for(int s = i;s;s=(s-1)&i) {
                int pre = i & (~s);
                if(oneCnt[s] != kk)  continue;
                if(!valid[s])  continue;
                dp[i] = min(dp[i], dp[pre]+cha[s]);
            }
        }
        int ans = dp[(1<<n)-1];
        return ans==0x3f3f3f3f? -1 : ans;
    }
};

LC 691. 贴纸拼词

题解:由于可以重复,通过采用刷表法。dp[i]表示target的状态为i时需要的最少贴纸数目

class Solution {
public:
    int minStickers(vector<string>& stickers, string target) {
        int n = stickers.size(), m = target.size();
        int dp[1<<m];
        memset(dp, 0x3f, sizeof(dp));
        dp[0] = 0;

        vector<vector<int>>cnt(n, vector<int>(26, 0));
        for(int i = 0;i < n;i++) {
            for(char ch : stickers[i]) {
                cnt[i][ch-'a']++;
            }
        }

        for(int i = 0;i < (1<<m);i++) {  // 逐层染色的感觉
            if(dp[i] == 0x3f3f3f3f)  continue;
            for(int j = 0;j < n;j++) {   // 每次都有n中选择(因为可以重复)
                int nxt = i;
                vector<int>left = cnt[j];
                for(int k = 0;k < m;k++) {
                    if(i & (1<<k)) continue;
                    if(left[target[k]-'a']) {
                        nxt |= (1<<k);
                        left[target[k]-'a']--;
                    }
                }
                // cout << i << " " << j << " " << nxt << endl; 
                dp[nxt] = min(dp[nxt], dp[i]+1);
            }
        }


        int ans = dp[(1<<m)-1];
        return ans==0x3f3f3f3f ? -1 : ans;
    }
};

值得注意的是,由于只考虑最少数目,不要求顺序,因此i 和 j 两个循环交换也是可以的;否则,如果是求方案数,应该将枚举物品放外面。

posted @ 2022-02-20 18:30  Rogn  阅读(335)  评论(0编辑  收藏  举报