狀態壓縮 dp,英文稱 bitmask dp,使用二進位來表示狀態,來達到類似暴力枚舉的效果

## 枚舉子集的子集

???+note "code"
	```cpp linenums="1"
	for (int mask = 0; mask < (1 << n); mask++) {
        // 給一個 mask,枚舉他的所有子集合
        for (int S = mask; S; S = (S - 1) & mask) {
            // TODO
        }
    }
    ```
    
## 題目

???+note "[Atcoder DP Contest O - Matching](https://atcoder.jp/contests/dp/tasks/dp_o)"
	有 $n$ 個男生和女生,第 $i$ 個男生與第 $j$ 的女生是否能配對取決於 $a_{i,j}=\{ 0, 1 \}$。問有幾種方法能產生 $n$ 組配對
	
	$1\le n\le 21$
	
	??? note "思路"
		可以依照 1, 2, ..., n 的順序讓男生配對,女生則用枚舉 mask
        
        dp(S) = 目前已將男生 [1, |S|] 與女生的集合 S 配對
        
        轉移 dp(S) = dp(S ^ (1 << i)) | a[|S|][i] == 1
	
	??? note "code"
		```cpp linenums="1"
		#include <bits/stdc++.h>
        #define int long long
        using namespace std;

        const int INF = 0x3f3f3f3f;
        const int M = 1e9 + 7;
        const int maxn = 21;
        int n;
        int a[maxn][maxn];
        int dp[1 << maxn];

        signed main() {
            cin >> n;
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    cin >> a[i][j];
                }
            }
            dp[0] = 1;
            for (int mask = 1; mask < (1 << n); mask++) {
                int now = __builtin_popcount(mask) - 1; 
                for (int i = 0; i < n; i++) {
                    if ((mask & (1 << i) && a[now][i])) {
                        dp[mask] += dp[mask ^ (1 << i)];
                        dp[mask] %= M;
                    }
                }
            }
            cout << dp[(1 << n) - 1];
        }
        ```

???+note "[CSES - Elevator Rides](https://cses.fi/problemset/task/1653)"
	有 $n$ 個人,第 $i$ 個人的重量為 $w_i$。給 $x$,每次可以移除一個重量和 $\le x$ 的集合,問最少移除幾次才能將 $n$ 個人都移除
	
	$n\le 20, 1\le w_i \le x \le 10^9$
	
	??? note "思路"
		如果枚舉子集的子集會需要 3^n,TLE
		
		令 dp(S) 為 S 內的人都已經移除的最小次數,f(S) 在移除次數為 dp(S) 下,最後一次移除的最小重量和為多少
		
		dp(S) = dp(S ^ (1 << i)) + (f(S ^ (1 << i)) + w[i] > x)
		
		- f(S ^ (1 << i)) + w[i] > x

			- dp(S) = dp(S ^ (1 << i)) + 1

			- f(S) = f(S ^ (1 << i)) + w[i] - x

		- f(S ^ (1 << i)) + w[i] <= x

			- dp(S) = dp(S ^ (1 << i))

			- f(S) = f(S ^ (1 << i)) + w[i]

	??? note "code"
		```cpp linenums="1"
		#include <bits/stdc++.h>
        #define int long long
        using namespace std;

        const int INF = 2e18;
        const int MAXN = 1 << 22;
        int n, x;
        int w[MAXN], dp[MAXN], t[MAXN];

        signed main() {
            cin >> n >> x;
            for (int i = 0; i < n; i++) {
                cin >> w[i];
            }

            dp[0] = 1;
            t[0] = 0;
            for (int mask = 1; mask < (1 << n); mask++) {
                dp[mask] = INF;
                t[mask] = INF;
                for (int i = 0; i < n; i++) {
                    if (mask & (1 << i)) {
                        int S = mask ^ (1 << i);
                        if (w[i] + t[S] <= x) {
                            if (dp[S] < dp[mask] || (dp[S] == dp[mask] && t[S] + w[i] < t[mask])) {
                                t[mask] = t[S] + w[i];
                                dp[mask] = dp[S];
                            }
                        } else {
                            if (dp[S] + 1 < dp[mask] || (dp[S] + 1 == dp[mask] && t[S] + w[i] < t[mask])) {
                                t[mask] = w[i];
                                dp[mask] = dp[S] + 1;
                            }
                        }
                    }
                }
            }
            cout << dp[(1 << n) - 1] << "\n";
        }
		```
	
???+note "[TIOJ 1418 . 交大都是雷](https://tioj.ck.tp.edu.tw/problems/1418)"
	有 3n 個人,給定倆倆之間若在同組的罰分。問將以 3 個為一組,分成 n 組的最少總罰分
	
	$1\le n\le 7$
	
	??? note "思路"
		每次枚舉 mask 裡面的 3 個元素 i, j, k 做轉移,而 i 其實只要枚舉一次就好,因為他無論如何都要配對,早晚不是問題
		
		注意: 需要使用 Top down,因為 Bottom up 會跑到很多沒用的狀態
		
	??? note "code"
		```cpp linenums="1"
		#include <bits/stdc++.h>
        #define int long long
        using namespace std;

        int n, v[22][22];
        string s;
        int dp[1 << 22];

        int cost(int a, int b, int c) {
            int ret = v[a][b] + v[a][c] + v[b][c];
            return ret;
        }

        int solve(int mask) {
            if (dp[mask] > -1) return dp[mask];
            if (!mask) return 0;
            int mn = 1e9;
            for (int i = 0; i < n; i++) {
                if (!((1 << i) & mask)) continue;
                for (int j = i + 1; j < n; j++) {
                    if (!((1 << j) & mask)) continue;
                    for (int k = j + 1; k < n; k++) {
                        if (!((1 << k) & mask)) continue;
                        mn = min(mn, solve(mask ^ (1 << i) ^ (1 << j) ^ (1 << k)) + cost(i, j, k));
                    }
                }
                break;
            }
            return dp[mask] = mn;
        }

        signed main() {
            int t;
            cin >> t;
            while (t--) {
                cin >> n;
                for (int i = 0; i < n; i++) {
                    for (int j = 0; j < n; j++) {
                        cin >> v[i][j];
                    }
                }
                for (int i = 0; i < (1 << n); i++) {
                    dp[i] = -1;
                }
                cout << solve((1 << n) - 1) << "\n";
            }
        }
        ```
	
???+note "[TIOJ 1014. 打地鼠](https://tioj.ck.tp.edu.tw/problems/1014)"
	有 $n$ 個地鼠,分別在點 $1$ 到 $n$。地鼠 $i$ 每 $t_i$ 秒會出現一次,玩家從 $0$ 出發,每秒可以移動一格,問打完所有地鼠所需的最少秒數
	
	$n \leq 16$
	
	??? note "思路"
		dp(S, i) = 打完集合 S 內的地鼠,目前在位置 i
	
		dp(S, i) = dp(j, S ^ (1 << i)) + cost(j, i)
		
		cost(j, i) 實則是要將 dp(S, i) 設為 dp(j, S ^ (1 << i)) + |i - j| 下一次 t[i] 出現的時候,所以我們可以列出
		
		dp(S, i) = ceil(dp(j, S ^ (1 << i)) + |i - j|, t[i]) * t[i]
		
	??? note "code"
		```cpp linenums="1"
		#include <bits/stdc++.h>
        #define int long long
        using namespace std;

        const int INF = (1LL << 60);
        int dp[(1 << 16)][17], t[17];

        int iceil(int a, int b){
            if(b < 0) a *= -1, b *= -1;
            if(a > 0) return (a + b - 1) / b;
            else return a / b;
        }

        signed main() {
            ios_base::sync_with_stdio(0);
            cin.tie(0);
            int n;
            cin >> n;
            for (int i = 0; i < n; i++) {
                cin >> t[i];
            }
            memset(dp, 0x3f, sizeof(dp));

            dp[0][0] = 1;
            for (int mask = 1; mask < (1 << n); mask++) {
                for (int i = 0; i < n; i++) {
                    if (mask & (1 << i) == 0) continue;
                    for (int j = 0; j < n; j++) {
                        int time = dp[mask ^ (1 << i)][j] + abs(j - i);
                        dp[mask][i] = min(dp[mask][i], iceil(time, t[i]) * t[i]);
                    }
                }
            }
            int ans = INF;
            for (int i = 0; i < n; i++) {
                ans = min(ans, dp[(1 << n) - 1][i]);
            }
            cout << ans << '\n';
        }
		```

???+note "[LeetCode 698. Partition to K Equal Sum Subsets](https://leetcode.com/problems/partition-to-k-equal-sum-subsets/)"
	給一個長度為 n 的序列 a,問能不能分成 k 組,使得每組的總和相同
	
	$1\le k\le n\le 16, 0\le a_i\le 10^4$
	
	??? note "思路"
		先看 target = a[0], ..., a[n - 1] 是否符合 target % k == 0,不是的話直接 return,是的話再將 target /= k
	
		dp(S) = S 是否能分成好幾組,每組的 sum 都是 target
		
		dp(S) = dp(S ^ U) | (sum[U] == target)
		
		複雜度 O(3^n),其實我們在預處理 sum[U] 可以做到 O(2^n),就是利用 
		
		<center>
		sum[U] = sum[U - lowbit(U)] + a[__builtin_ctz(lowbit(mask))]
		</center>
		
	??? note "code"
		```cpp linenums="1"
		#define lowbit(x) (x & (-x))
        class Solution {
           public:
            bool canPartitionKSubsets(vector<int>& a, int k) {
                int n = a.size();
                int tar = 0;
                for (auto ele : a) tar += ele;
                if (tar % k) return 0;
                tar /= k;
                vector<int> sum(1 << n);
                for (int mask = 1; mask < (1 << n); mask++) {
                    int idx = __builtin_ctz(lowbit(mask));
                    sum[mask] = sum[mask - lowbit(mask)] + a[idx];
                }
                vector<int> dp(1 << n);
                dp[0] = true;
                for (int mask = 1; mask < (1 << n); mask++) {
                    for (int s = mask; s > 0; s = (s - 1) & mask) {
                        if (sum[s] == tar && dp[mask ^ s]) {
                            dp[mask] = true;
                            break;
                        }
                    }
                }
                return dp[(1 << n) - 1];
            }
        };
		```

???+note "[CF 1886 E - I Wanna be the Team Leader](https://codeforces.com/contest/1886/problem/E)"
    有 $m$ 個 projects,難度分別為 $b_1, \ldots ,b_m$,有 $n$ 個人,抗壓程度分別為 $a_1, \ldots ,a_n$。現在需要分組,滿足以下條件 :

    - 使每個 project 至少有一個人做
    - 每個人至多負責一個 project
    - 令負責第 $j$ 個 project 的人有 $k$ 個,則這些 $a_i$ 須滿足 $a_i\le\frac{b_j}{k}$
    
    $n\le 2\times 10^5, m\le 20,1\le a_i, b_i\le 10^9$
    
    ??? note "思路"
        考慮將序列 $a$ 大到小 sort,每個 $b_i$ 挑的就會是一個 prefix,因為如果不是一個 prefix,那對於最後面的那格我們只在意前面選的數量,因此可將倒數第二格與前面的某個的 $b_i$ swap,還可以讓其他 $b_i$ 挑的 $a_i$ 的最小值變大,一直這樣做就會變好幾個連續區段。那問題就可以用 bitmask dp 解決,$dp[S]$ 代表 $S$ 目前 prefix 選到哪裡可使 $S$ 內的 $b_i$ 都滿足條件,轉移的話枚舉不在 $S$ 內的 $j$,看 $j$ 要延伸到哪裡,我們令 $j$ 支配的區段為  $[dp[S]+1,dp[S\cup j]]$,$dp[S\cup j]=nxt(j,dp[S])$。$nxt(j,i)$ 代表 $b_j$ 從 $i$ 開始往右選最少選到哪裡即可使 $b_j$ 滿足條件。



## Graph coloring problem

最小點著色、 k 點著色,都是 NP-complete 問題

??? info "四色定理"
	平面圖一定可以四著色,P 問題,有著 O(N²) 演算法,但不一定可以三著色。詳見 [Wiki](https://zh.wikipedia.org/zh-tw/%E5%9B%9B%E8%89%B2%E5%AE%9A%E7%90%86) 或 [演算法筆記](https://web.ntnu.edu.tw/~algo/Coloring.html)

???+note "k-Vertex Coloring"
	給一張 $n$ 點 $m$ 邊無向圖,問是否能 $k$-coloring,使每條邊兩端的顏色都不同
	
	??? note "思路"
		> k = 2
		
		二分圖判斷
		
		---
		
		> k = 3
		
		$O(m\times 2^n)$ 列舉第一種獨立集,剩下的二分圖
		
		---
		
		> k = 4
		
		令 $dp(S)=S$ 是否可以 2-colors 上色,建表複雜度 $O(m\times 2^n)$。列舉 $S$ 使得 $dp(S)$ 跟 $dp(V\setminus S)$ 都要是 true

???+note "Minimum Vertex Coloring  / Chromatic Number"
	給一張 $n$ 點 $m$ 邊無向圖,問最少塗幾種顏色可使每條邊兩端的顏色都不同
	
	??? note "思路"
		令 $dp(S)=S$ 最少多少 coloring,轉移式
		
		$$
		dp(S)=\min\limits_{T\in S} \{ dp(S \setminus T) + 1 \}
		$$
		
		其中 $T$ 要是獨立集,複雜度 $O(3^n)$