文章

状压DP(下)

状压DP用二进制压缩状态,常用于处理集合、图等组合问题,有效降低空间和时间复杂度。

状压DP(下)

状压DP(下)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    int MOD = 1e9 + 7;

    int numberWays(vector<vector<int>> &arr) {
        // 帽子颜色 1~m
        int m = 0;
        for (const auto &person: arr)
            for (const auto &hat: person)
                m = max(m, hat);

        // 人的数量 0~n-1
        int n = arr.size();
        // 记录每个帽子可以给哪些人,用 int 的低 n 位表示,1 表示可以满足那个人
        vector<int> hats(m + 1);
        for (int i = 0; i < n; ++i)
            for (const auto &hat: arr[i])
                hats[hat] |= 1 << i;

        vector<vector<int>> dp(m + 1, vector<int>(1 << n, -1));
        return fc(hats, m, n, 1, 0, dp);
    }

    // curIndex 帽子下标,status 记录这 n 个人帽子的获得情况
    int fc(vector<int> &hats, int m, int n, int curIndex, int status, vector<vector<int>> &dp) {
        // 所有人都有帽子了
        if (status == (1 << n) - 1) return 1;
        // 还有人没有帽子,但是能用的帽子已经没了
        if (curIndex == m + 1) return 0;
        if (dp[curIndex][status] != -1) return dp[curIndex][status];
        // p1:curIndex 号帽子不分配给任何人
        int res = fc(hats, m, n, curIndex + 1, status, dp);
        // p2:curIndex 号帽子尝试分配给每个它能满足的人
        int cur = hats[curIndex];
        
        // 枚举
        for (int i = 0; i < n; ++i)
            // 能满足 i 号人,且 i 号人现在没有帽子
            if ((cur & (1 << i)) != 0 && (status & (1 << i)) == 0)
                res = (res + fc(hats, m, n, curIndex + 1, status | (1 << i), dp)) % MOD;
        dp[curIndex][status] = res;
        return res;
    }
};
  • 使用 Brian Kernighan 算法提取出二进制状态中最右侧的 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    int MOD = 1e9 + 7;

    int numberWays(vector<vector<int>> &arr) {
        // 帽子颜色 1~m
        int m = 0;
        for (const auto &person: arr)
            for (const auto &hat: person)
                m = max(m, hat);

        // 人的数量 0~n-1
        int n = arr.size();
        // 记录每个帽子可以给哪些人,用 int 的低 n 位表示,1 表示可以满足那个人
        vector<int> hats(m + 1);
        for (int i = 0; i < n; ++i)
            for (const auto &hat: arr[i])
                hats[hat] |= 1 << i;

        vector<vector<int>> dp(m + 1, vector<int>(1 << n, -1));
        return fc(hats, m, n, 1, 0, dp);
    }

    // curIndex 帽子下标,status 记录这 n 个人帽子的获得情况
    int fc(vector<int> &hats, int m, int n, int curIndex, int status, vector<vector<int>> &dp) {
        // 所有人都有帽子了
        if (status == (1 << n) - 1) return 1;
        // 还有人没有帽子,但是能用的帽子已经没了
        if (curIndex == m + 1) return 0;
        if (dp[curIndex][status] != -1) return dp[curIndex][status];
        // p1:curIndex 号帽子不分配给任何人
        int res = fc(hats, m, n, curIndex + 1, status, dp);
        // p2:curIndex 号帽子尝试分配给每个它能满足的人
        int cur = hats[curIndex];

        // 使用 Brian Kernighan 算法提取出二进制状态中最右侧的 1
        int rightOne;
        while (cur != 0) {
            rightOne = cur & -cur;
            if ((status & rightOne) == 0)
                res = (res + fc(hats, m, n, curIndex + 1, status | rightOne, dp)) % MOD;
            cur ^= rightOne;
        }
        dp[curIndex][status] = res;
        return res;
    }
};

最优账单平衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    int MAXN = 13;

    int minTransfers(vector<vector<int>> &transactions) {
        vector<int> debt = debts(transactions);

        cout << "收支表: ";
        for (int i = 0; i < debt.size(); ++i)
            cout << debt[i] << " ";
        cout << endl;

        int n = debt.size();
        vector<int> dp(1 << n, -1);
        // 数组中划分出的累加和为 0 且不可拆分的集合,集合中若有 n 个元素,则最少转账 n - 1 次即可平账
        // 集合个数越多,最终总的转账次数越少
        // 总的转账次数 = 所有累加和为 0 且不可拆分的集合中的元素总个数就是 debt.size(),再减去集合总个数
        return n - fc(debt, (1 << n) - 1, 0, n, dp);
    }

    // 转化成收支表
    vector<int> debts(vector<vector<int>> &transactions) {
        vector<int> help(MAXN, 0);
        for (const auto &tran: transactions) {
            help[tran[0]] -= tran[2];
            help[tran[1]] += tran[2];
        }
        // 只返回收支不平衡的
        vector<int> debt;
        for (int num: help)
            if (num != 0)
                debt.push_back(num);
        return debt;
    }

    // 返回累加和为 0 且不可拆分的集合的最大数量
    int fc(const vector<int> &debt, int set, int sum, int n, vector<int> &dp) {
        if (dp[set] != -1) return dp[set];
        int res = 0;
        // 集合中不止一个元素
        if ((set & (set - 1)) != 0) {
            if (sum == 0) {
                // p1: 集合整体累加和为 0
                for (int i = 0; i < n; i++) {
                    // 找到一个在集合中的元素
                    if ((set & (1 << i)) != 0) {
                        // 去掉它,剩下的集合进行尝试,返回值 + 1
                        res = fc(debt, set ^ (1 << i), sum - debt[i], n, dp) + 1;
                        // 后续的在集合中的元素就不用再被去除掉进行尝试了,结果都一样
                        break;
                    }
                }
            } else {
                // p2: 不为 0
                for (int i = 0; i < n; i++) {
                    if ((set & (1 << i)) != 0) {
                        res = max(res, fc(debt, set ^ (1 << i), sum - debt[i], n, dp));
                    }
                }
            }
        }
        dp[set] = res;
        return res;
    }
};

int main() {
    Solution sol;

    vector<vector<vector<int>>> testCases = {
            // 用例 1:闭环交易,净收支为 0
            {
                    {0, 1, 10},
                    {1, 2, 10},
                    {2, 0, 10}
            },
            // 用例 2:链式转账
            {
                    {0, 1, 10},
                    {1, 2, 5}
            },
            // 用例 3:四人闭环
            {
                    {0, 1, 5},
                    {1, 2, 5},
                    {2, 3, 5},
                    {3, 0, 5}
            },
            // 用例 4:集中借款,最坏情况
            {
                    {0, 1, 10},
                    {0, 2, 10},
                    {0, 3, 10}
            },
            // 用例 5:复杂组合
            {
                    {0, 1, 10},
                    {2, 0, 5},
                    {3, 4, 7},
                    {4, 2, 5},
                    {1, 3, 2}
            }
    };

    vector<int> expected = {0, 2, 0, 3, 3};

    for (int i = 0; i < testCases.size(); ++i) {
        int result = sol.minTransfers(testCases[i]);
        cout << "Test Case " << i + 1 << ": Output = " << result
             << ", Expected = " << expected[i]
             << (result == expected[i] ? " ✅" : " ❌") << endl << endl;
    }

    return 0;
}

1994. 好子集的数目

  • 记忆化搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    int MAXN = 30;
    int LIMIT = (1 << 10);
    int MOD = 1e9 + 7;

    vector<int> own;

    Solution() {
        // 打表加速判断
        // 如果一个数字拥有某一种质数因子不只 1 个,那么认为这个数字无效,状态全是 0,0b0000000000
        // 比如 12,拥有 2 这种质数因子不只 1 个,所以无效,用0b0000000000表示
        // 如果一个数字拥有任何一种质数因子都不超过 1 个,那么认为这个数字有效,用位信息表示这个数字拥有质数因子的状态
        // 比如 14,拥有 2 这种质数因子不超过 1 个,拥有 7 这种质数因子不超过 1 个,有效
        // 从高位到低位依次表示:...13 11 7 5 3 2
        // 所以用 0b0000001001 表示 14 拥有质数因子的状态
        // 质数: 29 23 19 17 13 11 7 5 3 2
        // 位置:  9  8  7  6  5  4 3 2 1 0
        own = {
                0b0000000000, // 0
                0b0000000000, // 1
                0b0000000001, // 2
                0b0000000010, // 3
                0b0000000000, // 4
                0b0000000100, // 5
                0b0000000011, // 6
                0b0000001000, // 7
                0b0000000000, // 8
                0b0000000000, // 9
                0b0000000101, // 10
                0b0000010000, // 11
                0b0000000000, // 12
                0b0000100000, // 13
                0b0000001001, // 14
                0b0000000110, // 15
                0b0000000000, // 16
                0b0001000000, // 17
                0b0000000000, // 18
                0b0010000000, // 19
                0b0000000000, // 20
                0b0000001010, // 21
                0b0000010001, // 22
                0b0100000000, // 23
                0b0000000000, // 24
                0b0000000000, // 25
                0b0000100001, // 26
                0b0000000000, // 27
                0b0000000000, // 28
                0b1000000000, // 29
                0b0000000111  // 30
        };
    }

    // 1 <= nums.length <= 105
    // 1 <= nums[i] <= 30
    // todo
    int numberOfGoodSubsets(vector<int> &nums) {
        vector<int> cnt(MAXN + 1, 0);
        // 统计 1~30 出现的次数
        for (int num: nums) cnt[num]++;

        vector<vector<int>> dp(MAXN + 1, vector<int>(LIMIT, -1));
        int res = 0;
        for (int status = 1; status < LIMIT; status++)
            // 累加质因子状态为 status 的
            res = (res + fc(MAXN, status, cnt, dp)) % MOD;
        return res;
    }

    // 最终相乘的结果一定要让质因子的状态为 status,且每种质因子只能有 1 个,返回子集的数量
    // status 每一位代表的质因子如下
    // 质数: 29 23 19 17 13 11 7 5 3 2
    // 位置:  9  8  7  6  5  4 3 2 1 0
    int fc(int i, int status, vector<int> &cnt, vector<vector<int>> &dp) {
        if (dp[i][status] != -1) return  dp[i][status];
        int res = 0;
        if (i == 1) {
            if (status == 0) {
                res = 1;
                for (int j = 0; j < cnt[1]; j++)
                    res = (res << 1) % MOD;
            }
        } else {
            res = fc(i - 1, status, cnt, dp);
            int cur = own[i];
            int times = cnt[i];
            if (cur != 0 && times != 0 && (status & cur) == cur)
                res = (int) (((long long) fc(i - 1, status ^ cur, cnt, dp) * times + res) % MOD);
        }
        dp[i][status] = res;
        return res;
    }
};
  • 空间压缩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    int MAX_V = 30;
    int LIMIT = (1 << 10);
    int MOD = 1e9 + 7;
    vector<int> own;
    vector<int> cnt;
    vector<int> dp;

    Solution() {
        cnt.resize(MAX_V + 1, 0);
        dp.resize(LIMIT, 0);
        own = {
                0b0000000000, // 0
                0b0000000000, // 1
                0b0000000001, // 2
                0b0000000010, // 3
                0b0000000000, // 4
                0b0000000100, // 5
                0b0000000011, // 6
                0b0000001000, // 7
                0b0000000000, // 8
                0b0000000000, // 9
                0b0000000101, // 10
                0b0000010000, // 11
                0b0000000000, // 12
                0b0000100000, // 13
                0b0000001001, // 14
                0b0000000110, // 15
                0b0000000000, // 16
                0b0001000000, // 17
                0b0000000000, // 18
                0b0010000000, // 19
                0b0000000000, // 20
                0b0000001010, // 21
                0b0000010001, // 22
                0b0100000000, // 23
                0b0000000000, // 24
                0b0000000000, // 25
                0b0000100001, // 26
                0b0000000000, // 27
                0b0000000000, // 28
                0b1000000000, // 29
                0b0000000111  // 30
        };
    }

    int numberOfGoodSubsets(vector<int> &nums) {
        for (int num: nums) cnt[num]++;
        dp[0] = 1;
        for (int i = 0; i < cnt[1]; i++)
            dp[0] = (dp[0] << 1) % MOD;
        for (int i = 2, cur, times; i <= MAX_V; i++) {
            cur = own[i];
            times = cnt[i];
            if (cur != 0 && times != 0) {
                for (int status = LIMIT - 1; status >= 0; status--) {
                    if ((status & cur) == cur) {
                        dp[status] = (int) (((long long) dp[status ^ cur] * times + dp[status]) % MOD);
                    }
                }
            }
        }
        int res = 0;
        for (int s = 1; s < LIMIT; s++)
            res = (res + dp[s]) % MOD;
        return res;
    }
};

1655. 分配重复整数

  • 枚举所有 status 的子集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <bitset>

using namespace std;

int main() {
    int status = 0b1001100;
    for (int i = status; i > 0; i = (i - 1) & status) {
        // 输出低 7 位二进制
        cout << bitset<7>(i) << endl;
    }
    /*
     1001100
     1001000
     1000100
     1000000
     0001100
     0001000
     0000100
     */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    bool canDistribute(vector<int> &nums, vector<int> &quantity) {
        // 排序后统计词频
        sort(nums.begin(), nums.end());
        int n = 1;
        for (int i = 1; i < nums.size(); i++)
            if (nums[i - 1] != nums[i])
                n++;
        vector<int> cnt(n);
        int c = 1;
        for (int i = 1, j = 0; i < nums.size(); i++) {
            if (nums[i - 1] != nums[i]) {
                cnt[j++] = c;
                c = 1;
            } else {
                c++;
            }
        }
        cnt[n - 1] = c;

        int m = quantity.size();
        // sum[status] 表示满足 status 状态的订单总共要多少个相同的整数
        vector<int> sum(1 << m);
        // 枚举是生成 quantity 中的每个子集,所需要数字的个数
        for (int i = 0; i < quantity.size(); i++) {
            int v = quantity[i];
            int h = 1 << i;
            // 枚举所有比 h 小的状态
            for (int j = 0; j < h; j++)
                sum[h | j] = sum[j] + v;
        }
        vector<vector<int>> dp(1 << m, vector<int>(n, 0));
        return fc(cnt, sum, (1 << m) - 1, 0, dp);
    }

    // curIndex 为当前数字的下标
    // status 为订单状态,1 表示待满足
    bool fc(vector<int> &cnt, vector<int> &sum, int status, int curIndex, vector<vector<int>> &dp) {
        // 所有订单都被满足了
        if (status == 0) return true;
        // 整数已经用完
        if (curIndex == cnt.size()) return false;
        if (dp[status][curIndex] != 0) return dp[status][curIndex] == 1;

        bool res = false;
        // 当前数字的个数
        int k = cnt[curIndex];
        // 枚举所有 status 的子集
        for (int i = status; i > 0; i = (i - 1) & status) {
            // 这个 status 的子集的状态所需要的相同的数字个数小于当前数字个数
            // 满足这个子集,然后 status 去掉这个子集,尝试下一个数字
            if (sum[i] <= k && fc(cnt, sum, status ^ i, curIndex + 1, dp)) {
                res = true;
                break;
            }
        }
        // 当前数字啥也满足不了,尝试下一个数字
        if (!res) res = fc(cnt, sum, status, curIndex + 1, dp);
        dp[status][curIndex] = res ? 1 : -1;
        return res;
    }
};
本文由作者按照 CC BY 4.0 进行授权