文章

状压DP(上)

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

状压DP(上)

状压DP(上)

464. 我能赢吗

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
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    bool canIWin(int n, int m) {
        if (m == 0) return true;
        // 所有的数加起来都小于 m
        if (n * (n + 1) / 2 < m) return false;
        // dp[status] == -1:没算过
        // dp[status] == 0:false
        // dp[status] == 1:true
        // status 二进制的低 n + 1 位用来表示状态,1 表示能选,0 不能选,第 0 位不用,第 1 到 n 位对应数字 1~n
        // 如果 1~7 范围的数字,4、2 已经选了不能再选,那么 status 的低 8 位为:
        // 1 1 1 0 1 0 1 1
        // 7 6 5 4 3 2 1 0
        vector<int> dp(1 << (n + 1), -1);
        return fc(n, (1 << (n + 1)) - 1, m, dp);
    }

    // 当前的选手能否赢
    bool fc(int n, int status, int rest, vector<int> &dp) {
        if (rest <= 0) return false;
        if (dp[status] != -1) return dp[status] == 1;
        bool res = false;
        for (int i = 1; i <= n; ++i) {
            // i 没有被选过,且当前选手选了 i 之后,对手在 i 已经被选过的情况下输掉
            if ((status & (1 << i)) != 0 && !fc(n, (status ^ (1 << i)), rest - i, dp)) {
                res = true;
                break;
            }
        }
        dp[status] = res ? 1 : 0;
        return res;
    }
};

473. 火柴拼正方形

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
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    bool makesquare(vector<int> &matchsticks) {
        int sum = 0;
        for (const auto &item: matchsticks)
            sum += item;
        if (sum % 4 != 0) return false;
        int n = matchsticks.size();
        vector<int> dp(1 << n, 0);
        // status = (1 << n) - 1 表示低 n 位全 1,所有的火柴都尚未被选择
        return fc(matchsticks, sum / 4, (1 << n) - 1, 0, 4, dp);
    }

    // len: 用掉所有火柴构造出的正方形的边长
    // status: 记录火柴的选取情况,二进制位为 1 表示未被选择
    // cur: 当前要生成的边已经形成的长度
    // rest: 待生成的边的总数
    bool fc(vector<int> &nums, int len, int status, int cur, int rest, vector<int> &dp) {
        // 所有火柴用光的情况下拼成正方形
        if (rest == 0) return status == 0;
        if (dp[status] != 0) return dp[status] == 1;
        bool res = false;
        for (int i = 0; i < nums.size(); ++i) {
            // 尝试每一根二进制位为 1 的即未被使用过的火柴,且加上后,当前边长度不能超出最终的边长
            if ((status & (1 << i)) != 0 && cur + nums[i] <= len) {
                if (cur + nums[i] == len) {
                    // 刚好能生成一个完整的边
                    res = fc(nums, len, status ^ (1 << i), 0, rest - 1, dp);
                } else {
                    // 还差一些
                    res = fc(nums, len, status ^ (1 << i), cur + nums[i], rest, dp);
                }
                if (res) break;
            }
        }
        dp[status] = res ? 1 : -1;
        return res;
    }
};

698. 划分为k个相等的子集

  • 暴力递归
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
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:

    bool canPartitionKSubsets(vector<int> &nums, int k) {
        int sum = 0;
        for (const auto &item: nums) sum += item;
        if (sum % k != 0) return false;
        int n = nums.size();
        // 降序
        sort(nums.begin(), nums.end(), greater<int>());
        // 存放每个集合已经有的累加和
        vector<int> group(k);
        // 从最大的数字开始
        return fc(nums, 0, group, sum / k);
    }

    // 最终每个集合的累加和为 target
    bool fc(vector<int> &nums, int curIndex, vector<int> &group, int target) {
        // 所有整数都用完了,且之后的逻辑确保了每个集合的累加和不超过 target
        if (curIndex >= nums.size()) return true;
        int num = nums[curIndex];
        for (int i = 0; i < group.size(); ++i) {
            // 超过就跳过
            if (group[i] + num > target) continue;
            // 尝试把当前值放入 group[i]
            group[i] += num;
            if (fc(nums, curIndex + 1, group, target)) return true;
            // 回溯
            group[i] -= num;
            // 剪枝,去掉同样的失败情况,但只去掉相邻的情况
            while (i + 1 < group.size() && group[i] == group[i + 1]) i++;
        }
        return false;
    }
};
  • 记忆化搜索
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
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:

    bool canPartitionKSubsets(vector<int> &nums, int k) {
        int sum = 0;
        for (const auto &item: nums)
            sum += item;
        if (sum % k != 0) return false;
        int n = nums.size();
        vector<int> dp(1 << n, 0);
        // status = (1 << n) - 1 表示低 n 位全 1,所有的整数都尚未被选择
        return fc(nums, sum / k, (1 << n) - 1, 0, k, dp);
    }

    // len: 最终每个集合中的元素和
    // status: 记录整数的选取情况,二进制位为 1 表示未被选择
    // cur: 当前集合中的元素和
    // rest: 待生成的集合的总数
    bool fc(vector<int> &nums, int len, int status, int cur, int rest, vector<int> &dp) {
        if (rest == 0) return status == 0;
        if (dp[status] != 0) return dp[status] == 1;
        bool res = false;
        for (int i = 0; i < nums.size(); ++i) {
            // 尝试每一根二进制位为 1 的即未被选择过的整数,且加上后,当前边集合中的元素和不能超出最终的元素和
            if ((status & (1 << i)) != 0 && cur + nums[i] <= len) {
                if (cur + nums[i] == len) {
                    // 刚好能生成一个规定的集合
                    res = fc(nums, len, status ^ (1 << i), 0, rest - 1, dp);
                } else {
                    // 还差一些
                    res = fc(nums, len, status ^ (1 << i), cur + nums[i], rest, dp);
                }
                if (res) break;
            }
        }
        dp[status] = res ? 1 : -1;
        return res;
    }
};

P1171 售货员的难题

  • 即TSP 问题,全称是 旅行商问题(Travelling Salesman Problem),是一个经典的组合优化问题,属于 NP-完全问题,广泛用于算法设计、运筹学和人工智能领域。
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
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int MAXN = 20;
vector<vector<int>> graph(MAXN, vector<int>(MAXN));
vector<vector<int>> dp(1 << MAXN, vector<int>(MAXN));
int n;

// s 低 n 位记录这 n 个村庄是否经过了,二进制位为 1 表示经过了,i 代表当前村庄
int fc(int s, int i) {
    // 所有的村庄都经过了,返回从当前村庄回到起点的代价
    if (s == (1 << n) - 1) return graph[i][0];
    if (dp[s][i] != -1) return dp[s][i];
    int res = 0x7fffffff;
    for (int j = 0; j < n; ++j) {
        // 已经去过的就跳过
        if ((s & (1 << j)) != 0) continue;
        // 尝试下一个去 j 号村庄
        res = min(res, graph[i][j] + fc(s | (1 << j), j));
    }
    dp[s][i] = res;
    return res;
}

int main() {
    cin >> n;
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            cin >> graph[i][j];

    for (int i = 0; i < (1 << n); ++i)
        for (int j = 0; j < n; ++j)
            dp[i][j] = -1;

    // 表示村庄 0 已经经过,从村庄 0 出发
    cout << fc(1, 0) << endl;
    return 0;
}
本文由作者按照 CC BY 4.0 进行授权