状压 dp

前言

博客。

通常是设一个二进制状态表示物品的取舍从而去转移。所以实际上其状态总数是没有变的,状压过程只是让状态排列的更加紧密了。

子集枚举

题目:

给定一个长度为 \(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 邦邦的大合唱站队

  1. 状态:设 \(dp_i\) 已经排好队的乐队是二进制下的 \(i\) 时的最小代价。
  2. 答案:\(dp_{2 ^ m - 1}\)。
  3. 转移:

对于 \(dp_i\),枚举 \(i\) 中第 \(j\) 位的 \(1\):

\[dp_i = \min \{ dp_{i \bigoplus 2 ^ j} + buc_j + pre_{num + buc_j, j} - pre_{num, j} \} \]

  1. 维护前缀和即可。

代码:

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

  1. 状态:设 \(dp_{i, S}\) 表示已经匹配了前 \(i\) 个位置,且参与匹配的字符串的状态是 \(S\) 的方案数。
  2. 答案:

\[\sum_{\operatorname{popcount} (S) = k} dp_{n, S} \]

  1. 辅助状态:\(f_{i, c}\) 表示 \(n\) 个字符串在第 \(i\) 位有字符 \(c\) 的状态。

  2. 转移:

\[dp_{i, S \cup f_{i, c}} = dp_{i, S \cup f_{i, c}} + dp_{i - 1, S} \]

  1. 初始状态:\(dp_{0, 2 ^ n - 1} = 1\)。