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 两个循环交换也是可以的;否则,如果是求方案数,应该将枚举物品放外面。