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

  • 相关阅读:
    .net c# gif动画如何添加图片水印
    Webbrowser控件设置所使用的内核
    WPF 用户控件不显示标记
    Spark应用场景以及与hadoop的比较
    Spark基本函数学习
    uniapp H5发布后在部分手机字体变大,导致页面错乱
    uni 小程序图片路径转base64
    uniapp px与upx(rpx)相互转换
    uniapp调用扫码功能
    uniapp 引入第三方字体
  • 原文地址:https://www.cnblogs.com/lfri/p/15916118.html
Copyright © 2020-2023  润新知