状压DP练习
-
题意:给定 n × n n\times n n×n的棋盘,每个格子的数非负,从中选若干个方格取出数字且保证选定方格间没有公共边,问取出的数的和最大为多少。输入规模 n ≤ 20 n\le 20 n≤20
思路:可以考虑到用0表示不取,1表示取,则每行有1<<n个状态,如果直接枚举1~n比较浪费时间,可以先预处理出哪些状态是符合要求的。首先我们关注相邻行,假设两个单行状态 s 1 , s 2 s1,s2 s1,s2已经满足要求,显然状态对应的二进制不能有上下相邻的1,即1不能和1对上。我们考虑两个二进制数进行&的操作,1 & 1才能得到1,其他情况都是0,那么 s 1 & s 2 = = 0 s1 \& s2==0 s1&s2==0。再考虑如何让单行状态满足要求,对于一个数 x x x,考虑将它们错开一位
将二进制为从左往右分别记为x[0]~x[k],那么x[0]和x[1]不同时为1,转化为上下不同时为1,x[1]和x[2]不同时为1,同样转化为上下不同时为1,所以只要 x & ( x > > 1 ) = = 0 x \& (x >> 1) == 0 x&(x>>1)==0, x x x就是一个合法状态细节见代码
#include <cstdio> #include <iostream> #include <cstring> const int N = 21; int n, a[N][N], dp[2][1 << N], state[1 << N]; inline int calc(int row, int state) { int ans = 0, p = n - 1; while(state) { if(state & 1) ans += a[row][p]; state >>= 1; p--; } return ans; } int main() { while(~scanf("%d", &n)) { for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) scanf("%d", &a[i][j]); memset(dp, 0, sizeof(dp)); int cnt = 0; for(int i = 0; i < (1 << n); i++) if((i & (i >> 1)) == 0) state[++cnt] = i; for(int i = 1; i <= cnt; i++) dp[0][i] = calc(0, state[i]); for(int i = 1; i < n; i++) { int t = i & 1; for(int j = 1; j <= cnt; j++) { for(int k = 1; k <= cnt; k++) if((state[j] & state[k]) == 0) dp[t][k] = std::max(dp[t ^ 1][j] + calc(i, state[k]), dp[t][k]); } } int ans = 0; for(int i = 0; i < 2; i++) for(int j = 1; j <= cnt; j++) ans = std::max(ans, dp[i][j]); printf("%d\n", ans); } return 0; }
-
题意:给定一个 n × m n\times m n×m的01矩阵,0代表不可以种玉米,1代表可以种玉米,John想要知道使得每两株玉米都不相邻(上下左右均不相邻)的种植方法总共有多少种。值得注意的是,他可以选择一株也不种,这也算一种种法
思路:去掉01矩阵的条件和上一题很像,我们可以将玉米地的每行进行状态压缩,如将’111’压缩成整数7,将可种植状态记为map[]。和上题类似,我们用state[]数组来代表单行没有相邻1的状态,和上题不同的是,我们不仅需要考虑state[j] & state[k]是否为0(即上下是否有相邻1),还要考虑状态是否和玉米地的可种植状态匹配,因为玉米只能种在1的位置,也就是说state的1必须对应map的1,而state的0可以对应map的0或1,也就是说state[x] & map[i] == state[x],因为state的所有1都对应map的1
细节见代码
#include <cstdio> const int N = 13; const int MOD = 1e8; int n, m, dp[N][1 << N], map[N], state[1 << N], cnt; int main() { scanf("%d%d", &n, &m); for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) { int tmp; scanf("%d", &tmp); if(tmp) map[i] += (1 << (m - j - 1)); } for(int i = 0; i < (1 << m); i++) if((i & (i >> 1)) == 0) state[++cnt] = i; for(int i = 1; i <= cnt; i++) dp[0][i] = (map[0] & state[i]) == state[i]; for(int i = 1; i < n; i++) for(int j = 1; j <= cnt; j++) { if((map[i - 1] & state[j]) != state[j]) continue; for(int k = 1; k <= cnt; k++) if(((map[i] & state[k]) == state[k]) && (state[j] & state[k]) == 0) dp[i][k] = (dp[i][k] + dp[i - 1][j]) % MOD; // 可以滚动数组优化一下,但是懒 } int ans = 0; for(int i = 1; i <= cnt; i++) ans = (ans + dp[n - 1][i]) % MOD; printf("%d\n", ans % MOD); return 0; }
-
题意: N × N N\times N N×N的棋盘中放置K个国王,国王可以攻击其周围8个格子,求有多少种放置方法使得K个国王互不攻击
思路:首先预处理处单行不冲突状态 s t a t e [ ] state[] state[],用 d p [ i ] [ k ] [ l ] dp[i][k][l] dp[i][k][l]表示第 i i i行状态为 s t a t e [ k ] state[k] state[k]时,前 i i i行中已经放置了 l l l个国王的方法数,得
d p [ i ] [ k ] [ l ] = ∑ s t a t e [ j ] ∈ S d p [ i − 1 ] [ j ] [ l − n u m ] dp[i][k][l]=\sum_{state[j]\in S}dp[i-1][j][l-num] dp[i][k][l]=state[j]∈S∑dp[i−1][j][l−num]
其中 S S S是与 s t a t e [ k ] state[k] state[k]相容的状态的集合, n u m num num为 s t a t e [ k ] state[k] state[k]的二进制表示中 1 1 1的个数#include <cstdio> const int N = 10; int n, k1, cnt; int state[1 << N]; long long dp[N][1 << N][N * N]; // 防止爆int inline int calc(int state) { int ans = 0; while(state) { if(state & 1) ans++; state >>= 1; } return ans; } inline bool check(int j, int k) { return ((state[j] & state[k]) == 0) && (((state[j] << 1) & state[k]) == 0) && (((state[j] >> 1) & state[k]) == 0); } int main() { scanf("%d%d", &n, &k1); for(int i = 0; i < (1 << n); i++) if(((i & (i >> 1)) == 0) && ((i & (i << 1)) == 0)) state[++cnt] = i; dp[1][0][0] = 1; for(int i = 1; i <= cnt; i++) dp[1][i][calc(state[i])] = 1; for(int i = 2; i <= n; i++) for(int k = 1; k <= cnt; k++) { int num = calc(state[k]); for(int j = 1; j <= cnt; j++) if(check(j, k)) for(int l = num; l <= k1; l++) dp[i][k][l] += dp[i - 1][j][l - num]; // 还是可以滚动数组优化,懒得写 } long long ans = 0; for(int i = 1; i <= cnt; i++) ans += dp[n][i][k1]; printf("%lld\n", ans); return 0; }
-
题意:题目是中文的直接看题吧
懒得解释题意思路:这道题和POJ 3254玉米地有点像,都利用地图的01限制能否放置到该位置。因为炮兵可以打到下面两行的位置,所以要分别枚举
细节见代码
写得巨丑#include <cstdio> #include <cstring> #include <iostream> const int N = 101, M = 7; // 可行状态其实很少,所以1 << 7 = 128不会爆,貌似开70就够 int map[N], state[1 << M], dp[2][1 << M][1 << M]; int n, m, cnt; inline int calc(int state) { int ans = 0; for(; state; state >>= 1) if(state & 1) ans++; return ans; } inline bool check(int row, int state) { return (state & map[row]) == state; } int main() { while(~scanf("%d%d", &n, &m)) { for(int i = 1; i <= n; i++) { char ch[10]; map[i] = 0; scanf("%s", ch); for(int j = 0; j < m; j++) { map[i] <<= 1; if(ch[j] == 'P') map[i]++; } } cnt = 0; memset(dp, 0, sizeof(dp)); for(int i = 0; i < (1 << m); i++) if((i & (i >> 1)) == 0 && (i & (i >> 2)) == 0) state[++cnt] = i; for(int i = 1; i <= cnt; i++) if(check(1, state[i])) dp[1][i][1] = calc(state[i]); for(int i = 2; i <= n; i++) { int t = i & 1; for(int j = 1; j <= (i == 2 ? 1 : cnt); j++) { if(!check(i - 2, state[j])) continue; for(int k = 1; k <= cnt; k++) { if((state[j] & state[k]) || !check(i - 1, state[k])) continue; for(int l = 1; l <= cnt; l++) if((state[k] & state[l]) == 0 && (state[j] & state[l]) == 0 && check(i, state[l])) dp[t][l][k] = std::max(dp[t][l][k], dp[t ^ 1][k][j] + calc(state[l])); // 滚动数组优化,主要是因为如果数组开太大上面的memset会有点慢 } } } int ans = 0; for(int i = 1; i <= cnt; i++) for(int j = 1; j <= cnt; j++) ans = std::max(ans, dp[n & 1][i][j]); printf("%d\n", ans); } return 0; }
-
POJ 2411 Mondriaan’s Dream
题意:给一个 n × m n\times m n×m的棋盘 1 ≤ n , m ≤ 11 1\le n,m\le 11 1≤n,m≤11,问用 1 × 2 1\times 2 1×2的矩形铺满棋盘有多少中方法
思路:对于横着的矩形,我们将其标记为[1 1],对于竖着的矩形,我们将上面记为0,下面记为1,可以发现最后合法的情况必定是最后一行全为1。现在考虑如何得到合法的转移,我们只需要考虑相邻的两行,假设我们已经将前 c o l col col列放好了合法的矩形,那么目前合法的放置方法只有三种:- 在上面的行放一个竖着的
- 上面的行放的是一个竖着的块的1,下面的行放的是另一个竖着的块的0
- 上下两行都放横着的
我们可以用dfs先处理出合法的上下行转移表,然后dp
#include <cstdio> #include <cstring> #include <iostream> const int N = 12; typedef long long ll; int n, m, cnt, state[1 << 16][2]; ll dp[N][1 << N]; void dfs(int col, int pre, int suc) { if(col > m) return ; if(col == m) { state[cnt][0] = pre; state[cnt++][1] = suc; return ; } dfs(col + 1, pre << 1, suc << 1 | 1); dfs(col + 1, pre << 1 | 1, suc << 1); dfs(col + 2, pre << 2 | 3, suc << 2 | 3); } int main() { while(~scanf("%d%d", &n, &m) && n) { cnt = 0; if(n < m) std::swap(n, m); memset(dp, 0, sizeof(dp)); dfs(0, 0, 0); dp[0][(1 << m) - 1] = 1; for(int i = 1; i <= n; i++) { for(int j = 0; j < cnt; j++) dp[i][state[j][1]] += dp[i - 1][state[j][0]]; } printf("%lld\n", dp[n][(1 << m) - 1]); } return 0; }
-
题意:给定己方 n n n个小兵,敌方 m m m个小兵,每个小兵都有 1 ∼ 6 1 \sim 6 1∼6的的整数生命值,此时使用魔法对场上随机目标造成1点伤害,共造成 d d d次,每次伤害会使一个小兵的生命值减1,小兵生命值为0时会离场,问该魔法使得敌方所有小兵都离场的概率,己方小兵是否离场都没关系,数据范围: 1 ≤ n , m ≤ 5 , 1 ≤ d ≤ 100 1\le n,m\le5, 1\le d\le100 1≤n,m≤5,1≤d≤100
思路:因为小兵的生命值只有 [ 1 , 6 ] [1,6] [1,6],我们可以用一个12位的十进制整数来表示场上的情况,每个位置表示血量为 i i i的小兵的人数(敌我双方分开表示),因为我们只需要杀掉敌方小兵,我方的情况不用管,所以我们可以将敌方的情况放在12位整数的前6位,这样如果表示目前状态的整数 s t a < 1 e 6 sta<1e6 sta<1e6,就说明前6为全为0,即敌方小兵全部离场了,这样可以剪枝,同时我们采用记忆化搜索来记录某个状态的值,具体细节见代码
#include <cstdio> #include <unordered_map> typedef long long ll; int n, m, d; int life[2][9]; std::unordered_map<ll, double> dp; inline ll getSta() { ll sta = 0; for(int i = 1; i <= 6; i++) sta = sta * 10 + life[1][i]; for(int i = 1; i <= 6; i++) sta = sta * 10 + life[0][i]; return sta; } inline int count() { int sum = 0; for(int i = 1; i <= 6; i++) sum += life[1][i]; for(int i = 1; i <= 6; i++) sum += life[0][i]; return sum; } double dfs(ll sta, int d) { if(dp.count(sta)) return dp[sta]; if(sta < 1000000) return 1.0; if(!d) return 0.0; int cnt = count(); double ans = 0.0; for(int i = 0; i < 2; i++) { for(int j = 1; j <= 6; j++) { if(!life[i][j]) continue; life[i][j]--, life[i][j - 1]++; ll newsta = getSta(); double tmp = dfs(newsta, d - 1); dp[newsta] = tmp; life[i][j]++, life[i][j - 1]--; ans += tmp * life[i][j] / cnt; } } return ans; } int main() { scanf("%d%d%d", &n, &m, &d); int num; for(int i = 1; i <= n; i++) scanf("%d", &num), life[0][num]++; for(int i = 1; i <= m; i++) scanf("%d", &num), life[1][num]++; printf("%.8f\n", dfs(getSta(), d)); return 0; }
-
…
之后再写的时候大概会更新