前言
通常是设一个二进制状态表示物品的取舍从而去转移。所以实际上其状态总数是没有变的,状压过程只是让状态排列的更加紧密了。
子集枚举
题目:
给定一个长度为 \(n\ (n\le15)\) 的排列,问此排列中的 \(n\) 个元素所组成的每一个集合的所有子集。
考虑暴力枚举一下。
cpp
for(int S = 0 ; S <= (1 << n) ; ++ S)
for(int T = 0 ; T <= S ; ++ T)
...
显然此时复杂度是 \(O(4 ^ n)\) 的。
但是写成这样的形式能将复杂度降至 \(O(3 ^ n)\):
cpp
for(int S = 0 ; S <= (1 << n) ; ++S)
for(int T = S ; T ; T = (T - 1) & S)
...
先考虑证明复杂度:
\(n\) 位选出 \(k\) 位的方案数有 \((_{k}^{n})\) 个,\(k\) 位的子集个数有 \(2^k\) 个
所有集合的子集的元素个数和为 \(\sum_{k=0}^{n}(_{k}^{n})\times 2^{k}\)(不妨 \(k=0\) 也计算在内)
将上式变形得:
\[\sum_{k=1}^{n}(_{k}^{n})\times 1^{n-k}\times 2^{k} \]
二项式定理知为 \(O(3^n)\)。
再证明一下正确性:
考虑 \(S\) 的子集,在二进制上从大到小排成一排,那么大的通过减若干个 \(1\) 就一定能到小的,
但是中间会产生大量的状态,这些状态中包含了一些 \(S\) 中不包含的 \(1\),故和 \(S\) 与一下,去冗即可。
从而每两个相邻的状态就都是 \(S\) 的子集,由于降序从而任意两个状态不重复,即任意子集状态均可达。
例题
P3052 [USACO12MAR] Cows in a Skyscraper G
在此题中,\(n\) 个物品的取舍是与答案相关的,而我们又发现 \(n\) 的范围很小,所以可以考虑状压 dp。
设 \(dp_{i,j}\) 将物品分成 \(i\) 组,\(n\) 个物品的取舍的不同的每组总体积小于等于 \(W\) 的第 \(i\) 组的总体积,其中 \(j\) 为二进制数。
那么很显然我们就可以通过枚举 \(n \times 2^n\) 个状态去进行转移了。
代码:
cpp
#include <bits/stdc++.h>
#define rint register int
//#define int long long
#define Debug(...) cout << i << ' ' << j << ' ' << dp[i][j] << '\n'
#define min(a, b) ((a) < (b) ? (a) : (b))
using namespace std;
const int N = 20, M = (1 << 19) + 5;
int n, W, w[N], dp[N][M];
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> W;
for(rint i = 0 ; i < n ; ++ i)
for(rint j = 0 ; j < (1 << n) ; ++ j)
dp[i][j] = 1e9;
for(int i = 0 ; i < n ; ++ i)
cin >> w[i], dp[1][1 << i] = w[i];
for(rint i = 1 ; i <= n ; ++ i)
for(rint j = 0 ; j < (1 << n) ; ++ j)
if(dp[i][j] != 1e9)
for(rint k = 0 ; k < n ; ++ k) {
// Debug();
if(j & (1 << k)) continue;
if(dp[i][j] + w[k] <= W) dp[i][j | (1 << k)] = min(dp[i][j | (1 << k)], dp[i][j] + w[k]);
else dp[i + 1][j | (1 << k)] = min(dp[i][j | (1 << k)], w[k]);
}
for(int i = 0 ; i <= n ; ++ i)
if(dp[i][(1 << n) - 1] != 1e9) return cout << i, 0;
return 0;
}
P5911 [POI 2004] PRZ
- \(dp_i = \min_{j \subseteq i} \{ dp_{i \bigoplus j} + t_j \}\)。
子集枚举即可。
代码:
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 17;
int n, m, t[N], w[N], T[1 << N], W[1 << N], dp[1 << N];
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> m >> n;
for(int i = 1 ; i <= n ; ++ i)
cin >> t[i] >> w[i];
for(int i = 0 ; i < (1 << n) ; ++ i)
for(int j = 1 ; j <= n ; ++ j)
if(i & (1 << (j - 1)))
W[i] += w[j], T[i] = max(T[i], t[j]);
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
for(int i = 0 ; i < (1 << n) ; ++ i)
for(int j = i ; j ; j = (j - 1) & i)
if(W[j] <= m) dp[i] = min(dp[i], dp[i ^ j] + T[j]);
cout << dp[(1 << n) - 1];
return 0;
}
P3694 邦邦的大合唱站队
- 状态:设 \(dp_i\) 已经排好队的乐队是二进制下的 \(i\) 时的最小代价。
- 答案:\(dp_{2 ^ m - 1}\)。
- 转移:
对于 \(dp_i\),枚举 \(i\) 中第 \(j\) 位的 \(1\):
\[dp_i = \min \{ dp_{i \bigoplus 2 ^ j} + buc_j + pre_{num + buc_j, j} - pre_{num, j} \} \]
- 维护前缀和即可。
代码:
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int M = 21;
const int N = 1e5 + 5;
int n, m, a[N], w[1 << M], id[N], dp[1 << M], buc[M], pre[N][M];
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
cin >> n >> m;
for(int i = 1 ; i <= n ; ++ i) {
cin >> a[i];
++ buc[a[i]];
for(int j = 1 ; j <= m ; ++ j)
pre[i][j] = pre[i - 1][j];
pre[i][a[i]] = pre[i - 1][a[i]] + 1;
}
for(int i = 1 ; i < (1 << m) ; ++ i)
for(int j = 0 ; j < m ; ++ j)
if(i & (1 << j)) w[i] += buc[j + 1];
for(int i = 0 ; i < (1 << m) ; ++ i) {
for(int j = 0 ; j < m ; ++ j)
if(i & (1 << j))
dp[i] = min(dp[i], dp[i ^ (1 << j)] + buc[j + 1] - pre[w[i]][j + 1] + pre[w[i ^ (1 << j)]][j + 1]);
}
cout << dp[(1 << m) - 1];
return 0;
}
/*
12 4
1
3
2
4
2
1
2
3
1
1
3
4
*/
P2167
- 状态:设 \(dp_{i, S}\) 表示已经匹配了前 \(i\) 个位置,且参与匹配的字符串的状态是 \(S\) 的方案数。
- 答案:
\[\sum_{\operatorname{popcount} (S) = k} dp_{n, S} \]
-
辅助状态:\(f_{i, c}\) 表示 \(n\) 个字符串在第 \(i\) 位有字符 \(c\) 的状态。
-
转移:
\[dp_{i, S \cup f_{i, c}} = dp_{i, S \cup f_{i, c}} + dp_{i - 1, S} \]
- 初始状态:\(dp_{0, 2 ^ n - 1} = 1\)。