状压DP题单
作者:互联网
状态压缩DP
技巧
求一个数的二进制表示中含有多少个 t r u e true true:
int table[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
int popcount(unsigned int x) {
int ret = 0;
for (int i = 0; i < 8; i++)
ret += table[x & 15], x >>= 4;
return ret;
}
1.互不侵犯
考虑构成: 1 ≤ N ≤ 9 1 \leq N \leq 9 1≤N≤9,所以根据 N N N 构造状压 d p dp dp,将每一行国王的摆放情况进行压缩
考虑状态: 数组 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 表示第 i i i 行国王的摆放情况为 j j j 的情况下,已经摆放了 k k k 个国王
考虑转移: 第 i i i 行的国王摆放状态受到第 i − 1 i - 1 i−1 行国王摆放状态的制约,所以第 i i i 行由第 i − 1 i - 1 i−1 行转移得到
具体做法: 外层枚举行数,内层分别枚举第 i i i 行的摆放情况以及第 i − 1 i - 1 i−1 行的摆放情况,如果满足条件,则还需要考虑对截止到第 i − 1 i - 1 i−1行摆放的国王数量做状态转移(虽然第 i − 1 i - 1 i−1 行的摆放情况确定了,但是截止到第 i − 1 i - 1 i−1 行时,所有摆放的国王数量是不定的,所以对之前的所有情况做状态转移)
条件限制:
- 设第 i i i 行的国王摆放状态为 x x x
- 此时考虑第
i
+
1
i+1
i+1 行的国王状态
y
y
y,为了避免互相攻击,即避免对角线方向有连续的两个
1
1
1,需要满足两个条件
- x & ( y < < 1 ) = 0 x \& (y << 1) = 0 x&(y<<1)=0
- x & ( y > > 1 ) = 0 x \& (y >> 1) =0 x&(y>>1)=0
- 同时每一行的国王不能相邻,即:
- x & ( x < < 1 ) = 0 x\&(x << 1) = 0 x&(x<<1)=0
- x & ( x > > 1 ) = 0 x\&(x >> 1) = 0 x&(x>>1)=0
- 注意还有一个限制条件,即国王的数量不能超过 k k k
- 预处理出所有满足条件的摆放情况,能减少不必要的枚举
示例代码(如有雷同,纯属抄袭):
#include <bits/stdc++.h>
using namespace std;
#define int long long
vector<int> vec;
int sum[1 << 10];
int f[20][20][1 << 10];
int n, m;
int getsum(int x) {
int cnt = 0;
while(x) {
cnt += x & 1;
x >>= 1;
}
return cnt;
}
signed main() {
cin >> n >> m;
for(int i = 0; i < 1 << n; i++) {
sum[i] = getsum(i);
if( (i & (i << 1)) or (i & (i >> 1)) or sum[i] > m) continue;
vec.push_back(i);
}
for(auto i : vec) {
f[0][sum[i]][i] = 1;
}
for(int i = 1; i < n; i++) {
for(auto j : vec) {
for(auto k : vec) {
if(j & k) continue;
if(j & (k << 1)) continue;
if(j & (k >> 1)) continue;
for(int s = m; s >= sum[j]; s--) {
f[i][s][j] += f[i - 1][s - sum[j]][k];
}
}
}
}
int ans = 0;
for(auto i : vec) {
ans += f[n - 1][m][i];
}
cout << ans << endl;
return 0;
}
2.邦邦的大合唱站队
考虑构成: 由于队伍数量 1 ≤ M ≤ 20 1\leq M \leq 20 1≤M≤20,所以根据 M M M 构造状压 d p dp dp,将所有队伍的排列顺序进行状态压缩
考虑状态: 由于题目要求所有队伍必须相邻,即相当于是对于一段连续的位置,我们依次分配给每个队伍一段位置,长度就是这个队伍的人数,最后的结果就是 ( 1 … 1 … 1 ) 2 (1…1…1)_2 (1…1…1)2 ,表示所有的队伍都已经分配好了位置,所以
- 构造数组 f [ i ] f[i] f[i],表示状态 i i i 下,最少出列的偶像人数
- 构造数组 s u m [ i ] [ j ] sum[i][j] sum[i][j],前 i i i 个位置包含队伍编号为 j j j 的人数
- 构造数组 n u m [ j ] num[j] num[j],编号为 j j j 的总人数
考虑转移: 对于状态 i i i 而言,如果分配给编号 j j j 的队伍位置,那么 [ p o s l , p o s r ] [pos_l, pos_r] [posl,posr] 这一段区间位置都应该由队伍 j j j 的队员占领,出队人数应该是这段区间内不属于编号 j j j 的人的数量,利用前缀和计算即可
示例代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N = 21, M = 100010;
int f[1 << N], sum[M][N], num[N];
int n, m;
signed main() {
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int x;
cin >> x;
num[x - 1]++;
for(int j = 0; j < m; j++)
sum[i][j] = sum[i - 1][j];
sum[i][x - 1]++;
}
memset(f, 0x3f3f3f, sizeof f);
f[0] = 0;
for(int i = 0; i < 1 << m; i++) {
int length = 0;
for(int j = 0; j < m; j++)
if(i & (1 << j))
length += sum[n][j];
for(int j = 0; j < m; j++)
if(i & (1 << j))
f[i] = min(f[i], f[i ^ (1 << j)] + num[j] - sum[length][j] + sum[length - num[j]][j]);
}
cout << f[(1 << m) - 1] << endl;
return 0;
}
3.砝码称重
考虑构成: 由于砝码数量为 1 ≤ N ≤ 20 1 \leq N \leq 20 1≤N≤20,所以根据砝码数量 N N N 构造状压 d p dp dp,将所有砝码的所有选择情况进行状态压缩
考虑状态: 由于是要求最多能称量出多少种重量,所以我们需要枚举出每一种满足题意的状态,一旦我们知道选择哪几个砝码后,求能称出的重量,就是类似于01背包求方案数的做法。
考虑转移: 考虑到每一次称出的重量都是在上一次的基础上得到的,所以枚举当前已经有的重量,第 i i i 个合法砝码选择后方案数是否能增加,从之前的所有称出的重量的基础上得到
具体做法: 首先可以采用 d f s dfs dfs 或者 f o r for for 循环的方式枚举得到所有的合法状态,对于每一个合法状态都需要进行一次 d p dp dp,在 d p dp dp 过程中,首先要对标记数组 v i s vis vis 初始化,表示是否得到过这个重量,注意 v i s [ 0 ] = 1 vis[0] = 1 vis[0]=1,一开始啥也没有嘛毕竟。之后对于状态 s t a t e state state,枚举砝码的编号 [ 1 , n ] [1, n] [1,n],如果 s t a t e > > ( i − 1 ) & 1 state >> (i - 1) \& 1 state>>(i−1)&1 为 1 1 1 表示这个砝码是被选择了的,之后从所有重量进行状态转移即可。
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int n, m, ans;
int state[1 << 21], a[30], f[30], vis[2020];
int table[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
int popcount(unsigned int x) {
int ret = 0;
for (int i = 0; i < 8; i++)
ret += table[x & 15], x >>= 4;
return ret;
}
void dp(int state) {
memset(vis, 0, sizeof vis);
vis[0] = 1;
int tot = 0;
int cnt = 0;
for (int i = 0; i < n; i++) {
if (!(state >> i & 1)) {
for (int j = tot; j >= 0; j--) {
if (vis[j] && !vis[j + a[i + 1]]) {
vis[j + a[i + 1]] = 1;
cnt++;
}
}
tot += a[i + 1];
}
}
ans = max(ans, cnt);
return;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 0; i < (1 << n); i++)
if (popcount(i) == m)
dp(i);
cout << ans << endl;
return 0;
}
4.炮兵阵地
考虑构成: 由于列数 1 ≤ M ≤ 10 1\leq M \leq10 1≤M≤10,所以考虑根据每一行的排列情况进行 d p dp dp,将每一行的炮兵部队状态进行压缩
注意事项:
- 基本和[《互不侵犯》]([P2704 NOI2001] 炮兵阵地 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))一样
- 本题在枚举到第 i i i 行的时候,同时需要判断第 i − 1 i -1 i−1 行和第 i − 2 i - 2 i−2 行的排列情况
- 同时每一行的状态还要注意和地形的情况联系起来
示例代码:
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int a[1 << 10], n, m, sum[1 << 10], dp[1 << 10][1 << 10][2];
char x;
int getsum(int s) {
int res = 0;
while(s) {
res += (s & 1);
s >>= 1;
}
return res;
}
int main() {
scanf("%d %d", &n, &m);
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
cin >> x;
a[i] <<= 1;
a[i] += (x == 'H' ? 1 : 0);
}
}
for(int s = 0; s < (1 << m); s++) {
sum[s] = getsum(s);
if(!((s & a[0]) or (s & (s << 1)) or (s & (s << 2)))) {
dp[0][s][0] = sum[s];
}
}
for(int l = 0; l < (1 << m); l++) {
for(int s = 0; s < (1 << m); s++) {
if(!((l & s) or (l & a[0]) or (s & a[1]) or (l & (l << 1)) or (l & (l << 2)) or (s & (s << 1)) or (s & (s << 2)))) {
dp[l][s][1] = sum[l] + sum[s];
}
}
}
for(int i = 2; i < n; i++) {
for(int l = 0; l < (1 << m); l++) {
if((l & a[i - 1]) or (l & (l << 1)) or (l & (l << 2))) {
continue;
}
for(int fl = 0; fl < (1 << m); fl++) {
if((fl & a[i - 2]) or (fl & (fl << 1)) or (fl & (fl << 2)) or (fl & l)) {
continue;
}
for(int s = 0; s < (1 << m); s++) {
if((s & a[i]) or (s & (s << 1)) or (s & (s << 2)) or (s & fl) or (s & l)) {
continue;
}
dp[l][s][i % 2] = max(dp[l][s][i % 2], dp[fl][l][(i + 1) % 2] + sum[s]);
}
}
}
}
int ans = 0;
for(int l = 0; l < (1 << m); l++) {
for(int s = 0; s < (1 << m); s++) {
ans = max(ans, dp[l][s][(n - 1) % 2]);
}
}
cout << ans << endl;
return 0;
}
5.Corn Fields G
可以看作是[《互不侵犯》]([P2704 NOI2001] 炮兵阵地 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))和《炮兵阵地》 的变式,考虑地形以及相邻两行的种植状态之间的相互约束即可。
示例代码:
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 20, mod = 100000000;
int f[N][1 << N];
int x[N][N];
vector<int> state;
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> x[i][j];
}
}
for (int i = 0; i < (1 << m); i++) {
if ((i & (i << 1)) or (i & (i >> 1)))
continue;
state.push_back(i);
f[0][i] = 1;
}
for (int i = 1; i < n; i++) {
for (auto j : state) { // 当前行状态
for (auto k : state) { // 上一行状态
if (j & k)
continue;
int flag = 0;
for (int t = 0; t < m; t++) {
if (((j >> t) & 1) == 1 and x[i][t] == 0) {
flag = 1;
break;
}
if (((k >> t) & 1) == 1 and x[i - 1][t] == 0) {
flag = 1;
break;
}
}
if (flag) {
continue;
}
f[i][j] = (f[i][j] + f[i - 1][k] + mod) % mod;
}
}
}
long long ans = 0;
for (auto i : state) {
int flag = 0;
for (int j = 0; j < m; j++) {
if (((i >> j) & 1) == 1 and x[n - 1][j] == 0) {
flag = 1;
break;
}
}
if (!flag) {
ans = (ans + f[n - 1][i] + mod) % mod;
}
}
cout << ans << endl;
return 0;
}
6.No Change G
考虑构成: k ≤ 16 k \leq 16 k≤16,所以根据 k k k 构造状压 d p dp dp,将所有硬币的状态进行压缩
考虑状态: 数组 p o s [ i ] pos[i] pos[i] 表示 i i i 状态下的硬币可以购买到第几个商品, d p [ i ] dp[i] dp[i] 表示状态 i i i 下的花费
考虑转移: 使用当前硬币的状态一定由使用上一个硬币的状态转移而来
具体做法: 外层循环枚举所有状态,内层枚举每一个硬币,如果是使用到的,则进行一次转移,注意到本题要求了必须依次购买商品,所以可以用一个前缀和维护商品的总花费,之后二分出用当前硬币能够购买到的最后一件商品的编号,如果此时已经能够购买到第 n n n 件商品,直接对花费取最小值,最后判断所有硬币的总额是否大于花费即可。
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e5 + 10;
int n, m, pay, money;
long long ans = 0x3f3f3f3f3f;
int coins[N], sum[N];
int pos[1 << 17], dp[1 << 17];
int check(int value, int st) {
int l = st, r = n;
while (l <= r) {
int mid = (l + ((r - l) >> 1));
if (sum[mid] - sum[st - 1] == value)
return mid;
else if (sum[mid] - sum[st - 1] > value)
r = mid - 1;
else
l = mid + 1;
}
return r;
}
int main() {
cin >> m >> n;
for (int i = 1; i <= m; i++) {
cin >> coins[i];
money += coins[i];
}
for (int i = 1; i <= n; i++) {
cin >> pay;
sum[i] = sum[i - 1] + pay;
}
for (int i = 0; i < (1 << m); i++) {
for (int j = 0; j < m; j++) {
if ((i >> j) & 1) {
int last = i ^ (1 << j);
int sum = check(coins[j + 1], pos[last] + 1);
if (sum > pos[i]) {
pos[i] = sum;
dp[i] = dp[last] + coins[j + 1];
}
if (pos[i] == n) {
ans = min(ans, (long long)dp[i]);
}
}
}
}
cout << (money - ans < 0 ? -1 : money - ans) << endl;
return 0;
}
标签:状态,int,sum,状压,++,DP,ans,dp,题单 来源: https://blog.csdn.net/a12311weq/article/details/121421724