首页 > 代码库 > 状态压缩DP
状态压缩DP
若元素数量比较小(不超过 20)时,想要存储每个元素取或不取的状态时,可以借助位运算将状态压缩。
需要借助状态压缩过程的动态规划就是状态压缩 DP(很多地方会简称为 状压 DP)。
取若干元素,也就是对应的位置记为 1,其余位置记为 0。
例如,一共有 5 个元素 a,b,c,d,e,我们分别用 1,2,4,8,16 表示这五个元素,
则集合{a,c,e} 可以用 (10101)_2=21 来表示,而集合 {b,c,d} 可以用 (01110)_2=14 表示。
对于元素个数为 n 的情况,其空间复杂度为 O(2^n)。
例题 1
给定一个 n×m 的矩阵,行数和列数都不超过 20,其中有些格子可以选,有些格子不能选。现在你需要从中选出尽可能多的格子,且保证选出的所有格子之间不相邻(没有公共边)。
例如下面这个矩阵2×3 的矩阵,可选的位置标记为 1,不可选的位置标记为 0:
1 1 1
0 1 0
最多可选 3 个互不相邻的格子,方案如下(选中的位置标记为x
):
x 1 x
0 x 0
例题解析
我们可以自上而下,一行行地选择格子。在一行内选择格子的时候,只和上一行的选择方案有关,我们就可以将“当前放到第几行、当前行的选择方案”作为状态进行动态规划。
这里,我们就要用到刚刚提到的状态压缩:一行里选择格子的方案实际上是一个集合,我们要将这个集合压缩为一个整数。
比如,对于一个 3 列的矩阵,如果当前行的状态是(5)?10??=(101)?2??,那么就意味着当前行选择了第一个和第三个格子;
类似地,如果当前行的状态是 (6)?10??=(110)?2??,那么就意味着当前行选择了第一个和第二个格子。
如果上一行的状态是now
,下一行的状态是prev
,那么我们只需要确保上下两行的选择方案里没有重复的元素,也就是(now & prev) == 0
就可以了。
此外,我们还需要判断当前行的状态是否合法,因为读入的矩阵中并不是每个格子都可以选择的,如果我们将矩阵中每行的值也用状态压缩来存储,
不妨记为flag
,那么当前行选择的格子集合一定包含于当前行合法格子的集合,也就是说,(now | flag) == flag
必须成立。
这样,我们就可以通过枚举上一行的所有状态,来更新当前行、当前状态的最优解了。直到算完最后一行,统计一下所有状态的最大值即可。
C++ 示例代码如下:
const int MAX_N = 20;
const int MAX_M = 20;
int state[MAX_N];
int dp[MAX_N + 1][1 << MAX_M];
bool not_intersect(int now, int prev) {
return (now & prev) == 0;
}
bool fit(int now, int flag) {
return (now | flag) == flag;
}
int count(int now) {
int s = 0; // 统计 now 的二进制形式中有多少个 1
while (now) {
s += (now & 1); // 判断 now 二进制的最后一位是否为 1,如果是则累加
now >>= 1; // now 右移一位
}
return s;
}
int main() {
// 初始化所有数组
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
int flag;
cin >> flag;
state[i] |= (1 << j) * flag; // 将 (i,j) 格子的状态放入 state[i] 中,state[i] 表示第 i 行的可选格子组成的集合
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < (1 << MAX_M); ++j) { // 枚举当前行的状态
if (!fit(j, state[i])) { // 如果当前行状态不合法则不执行后面的枚举
continue;
}
int cnt = count(j); // 统计当前行一共选了多少个格子
for (int k = 0; k < (1 << MAX_M); ++k) { // 枚举前一行的状态
if (!not_intersect(j, k)) { // 判断当前行和前一行的方案是否冲突
dp[i][j] = max(dp[i][j], dp[i - 1][k] + cnt); // 更新当前行、当前状态的最优解
}
}
}
}
int ans = 0; // 保存最终答案
for (int i = 0; i < (1 << MAX_M); ++i) {
ans = max(ans, dp[n - 1][i]); // 枚举所有状态,更新最大值
}
cout << ans << endl;
return 0;
}
例题 2
给定一个长度不超过 16 字符串 s,如果 s 中的一个子序列是回文,那么我们就可以从 s 中移除这个子序列,
求最少经过多少步我们可以移除整个字符串 s。如:我们可以从"dqewfretd"
中移除 "defed"
,剩下的字符串即:"qwrt"
。
例题解析
使用状态压缩 DP 求解,当某状态对应的子序列是回文时,答案为 1;否则是其两个互补的子集操作步骤数之和的最小值。
在更新时我们需要枚举要求解的状态的所有的子集,可以仿照如下的代码进行求解。时间复杂度是 O(pow(3,n)+n×pow(2,n)??)。
for (int t = 1; t < (1 << n); t++) { // 枚举当前状态
dp[t] = IsPalindrome(t) ? 1 : inf; // 判断当前状态是否是回文,如果是回文则步骤数为 1
for(int i = t; i; i = (i - 1) & t) { // 枚举 t 的所有子集
dp[t] = min(dp[t], dp[i] + dp[t ^ i]); // 更新当前状态的解的最小值
}
}
printf("%d\n", dp[(1 << n) - 1]); // 输出最终答案
状态压缩DP