摆放类状压DP基础题
P1879 [USACO06NOV] Corn Fields G
有一块 \(n \times m(n,m \le 12)\) 的网格地, 每个格子用 \(0\) 或 \(1\) 代表是否这个区域是否肥沃。只有肥沃的地块才能种草,但不能将草种在相邻的两个格子中,求出一共有多少种符合要求的种草方案(可以全不种),答案对 \(10^8\) 取模。
考虑将每一行的种草情况压成一个二进制状态。对于每一行,枚举所有 \(2^m\) 种方案。由于不肥沃的地方不能种草,所以先与该行的肥沃情况的状压变量按位取与。例如,设当前枚举到的状态为 \(i\),该行的肥沃情况是 \(r\),对应的方案为 \(t=i\land r\):
接下来我们要判断是否有相邻格子都种了草。对于左右相邻,我们只需将状态全都左移一位,然后再与原来的状态按位取与。如果答案为 \(0\) 则说明没有相邻,否则有相邻。例如下面的 \(t=(101001)_2\) 就是一个合法的方案:
而 \(t=(101100)_2\) 就是不合法的:
左右相邻考虑完了,接下来考虑上下相邻。显然为了达到这一目标,我们在搜索的时候要记录上一行的状态 \(pre\)。检查当前方案中是否存在与上一行相邻的草块,只需要用这一行的状态跟上一行的状态按位取与。同样地,如果结果为 \(0\) 则说明没有相邻,否则有相邻。这个比较显然,就不用举例子了。
现在我们已经可以判断方案是否合法,但还存在一个问题:可能存在重复的方案。例如当 \(r=(110110)_2\) 时,枚举到 \(i=(100110)_2\) 或 \((101110)_2\) 得到的状态是相同的 \(t=(100110)_2\)。因此我们还需要一个标记数组 \(vis\) 来判断是否重复。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 13, MOD = 1e8;
inline int read(){
int x = 0, neg = 1;
char c = getchar();
while(!isdigit(c)) {if(c == '-') neg = -1; c = getchar();}
while(isdigit(c)) {x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * neg;
}
int n, m, a[N];
ll dp[N][1 << N];
ll dfs(int u, int pre){
if (u == n + 1) return 1;
if (dp[u][pre] != -1) return dp[u][pre];
bool vis[1 << N] = {0};
ll res = 0;
for (int i = 0; i < (1 << m); ++i) {
int t = i & a[u];
if (t & (t << 1) || t & pre || vis[t]) continue ;
res = (res + dfs(u + 1, t)) % MOD;
vis[t] = 1;
}
return dp[u][pre] = res;
}
int main() {
n = read(), m = read();
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++) a[i] = (a[i] << 1) | read();
memset(dp, -1, sizeof(dp));
printf("%lld", dfs(1, 0));
return 0;
}
P1896 [SCOI2005] 互不侵犯
在 \(N\times N(N \le 9)\) 的棋盘里面放 \(K\) 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到与它 \(8\) 邻接的位置。
跟上一题类似,用一个状压变量来表示每一行棋子的摆放情况,不同的是上一题要求摆放位置 \(4\) 邻接范围内不相邻,而这一题要求 \(8\) 邻接位置内不相邻,只需要多判断 \(t \land (pre \ll 1)\) 和 \(t \land (pre \gg 1)\) 即可。同时,题目还限制了棋子的个数,所以我们还需要多开一维表示棋子的个数,当前行内的棋子个数不得超过限制。只需要统计状压变量中 \(1\) 的个数即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 10;
inline int read(){
int x = 0, neg = 1;
char c = getchar();
while(!isdigit(c)) {if(c == '-') neg = -1; c = getchar();}
while(isdigit(c)) {x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * neg;
}
int popcount(int x){
int res = 0;
while(x) res += x & 1, x >>= 1;
return res;
}
bool check(int now, int pre){
return now & (now << 1) || now & pre || (now & (pre << 1)) || (now & (pre >> 1));
}
int n, m;
ll dp[N][N * N][1 << N];
ll dfs(int step, int k, int pre){
if(step == n + 1 && k) return 0;
if (k == 0) return 1;
if (dp[step][k][pre] != -1) return dp[step][k][pre];
ll res = 0;
for(int i = 0; i < (1 << n); i++){
int cnt = popcount(i);
if(cnt > k || check(i, pre)) continue;
res += dfs(step + 1, k - cnt, i);
}
return dp[step][k][pre] = res;
}
int main() {
n = read(), m = read();
memset(dp, -1, sizeof(dp));
printf("%lld", dfs(1, m, 0));
return 0;
}
P8756 [蓝桥杯 2021 省 AB2] 国际象棋
题目大意懒得写了,就是上一题把国王换成了马。由于马的攻击可以跨两行,所以我们要多开一维状压。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 6, M = 105, K = 25, MOD = 1e9 + 7;
inline int read(){
int x = 0, neg = 1;
char c = getchar();
while(!isdigit(c)) {if(c == '-') neg = -1; c = getchar();}
while(isdigit(c)) {x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * neg;
}
int popcount(int x){
int res = 0;
while(x) res += x & 1, x >>= 1;
return res;
}
bool check(int now, int pre1, int pre2){
return now & (pre1 >> 2) || now & (pre1 << 2) || now & (pre2 >> 1) || now & (pre2 << 1);
}
int n, m, q;
ll dp[M][K][1 << N][1 << N];
ll dfs(int step, int k, int pre1, int pre2){
if(step == m + 1 && k) return 0;
if (k == 0) return 1;
if (dp[step][k][pre1][pre2] != -1) return dp[step][k][pre1][pre2];
ll res = 0;
for(int i = 0; i < (1 << n); i++){
int cnt = popcount(i);
if(cnt > k || check(i, pre1, pre2)) continue;
res = (res + dfs(step + 1, k - cnt, i, pre1)) % MOD;
}
return dp[step][k][pre1][pre2] = res;
}
int main() {
n = read(), m = read(), q = read();
memset(dp, -1, sizeof(dp));
printf("%lld", dfs(1, q, 0, 0));
return 0;
}
P2704 [NOI2001] 炮兵阵地
给定 \(n\times m(n \le 100,m \le 10)\) 的字符网格,
H
代表山地,P
代表平原,炮兵部队只能部署在平原上。炮兵部队的攻击范围是:沿横向左右各两格,沿纵向上下各两格。在每个炮兵部队的攻击范围不重叠的情况下,求出可部署炮兵部队数量的最大值。
和前一题类似,因为有两个状压变量,我们应该考虑一下空间问题。按照之前的思路,我们应该开int dp[n][2^m][2^m]
,耗费的空间为 \(4\text{ B} \times100\times 2^{10}\times 2^{10}= 400\text{ MB}\),而原题的空间限制是 \(128\text{ MB}\),需要优化。观察到每一行状态的判断只关系到前两行,因此可以考虑滚动优化。然而滚动优化后无法打记忆化搜索,所以我们就只能打常规的迭代 \(\text{DP}\) 了。
设 \(\text{dp}_{i,j,k}\) 表示当前考虑到第 \(i\) 行,状态为 \(j\),前一行的状态为 \(k\) 的答案,则转移方程易得:
设 \(now,last\) 分别为当前行和前一行,然后进行滚动优化:
每完成一行,就交换 \(now,last\) 来实现滚动。这样,就只需要 \(4\text{ B} \times 2 \times 2^{10} \times2^{10}=8 \text{ MB}\) 的空间了。
#include<bits/stdc++.h>
using namespace std;
const int N = 105, M = 10;
inline int read(){
int x = 0, neg = 1;
char c = getchar();
while(!isdigit(c)) {if(c == '-') neg = -1; c = getchar();}
while(isdigit(c)) {x = (x << 1) + (x << 3) + (c ^ 48); c = getchar();}
return x * neg;
}
int n, m, a[N], cnt[1 << M], ans, now = 0, last = 1; //cnt[i]=popcount(i)
char g[N][M];
int dp[2][1 << M][1 << M];
bool check(int x, int i){return (x & a[i] || x & (x << 1) || x & (x << 2));} //第i行摆放状态为x是否非法
int main() {
n = read(), m = read();
for(int i = 1; i <= n; i++) {
scanf("%s", g[i]);
for(int j = 0; j < m; j++)
a[i] <<= 1, a[i] |= g[i][j] == 'H';
}
for(int i = 0; i < (1 << m); i++){
int x = i;
while(x) cnt[i] += x & 1, x >>= 1;
}
for(int i = 1; i <= n; i++) {
for(int j = 0; j < (1 << m); j++){
if(check(j, i)) continue;
for(int k = 0; k < (1 << m); k++){
if((i >= 2 && check(k, i - 1)) || j & k) continue;
for(int l = 0; l < (1 << m); l++){
if((i >= 3 && check(l, i - 2)) || j & l) continue;
dp[now][j][k] = max(dp[now][j][k], cnt[j] + dp[last][k][l]);
}
if(i == n) ans = max(ans, dp[now][j][k]);
}
}
swap(now, last);
}
printf("%d", ans);
return 0;
}
P5005 中国象棋 - 摆上马
在一个 \(n\times m(n \le 100,m \le 6)\) 的棋盘中摆放若干个中国象棋的马,求出有多少种方案使得任何马都不会互相攻击(考虑蹩马腿)。 注意:一个马都不摆也算一种方案,答案对 \((10^9+7)\) 取模。
考虑到马走日的攻击规则,这一题仍然需要存两行的状压变量,但是因为 \(m \le6\),最多只需要消耗 \(4 \text{ B} \times 100 \times 2^6 \times 2^6 \approx 1.56\text{ MB}\) 的空间,所以可以放心大胆地打记搜。这一题的恶心之处在于如何判断状态的合法性,分类讨论需要考虑很多细节。因为所谓马走日,指的是马先按格线前进一格,再斜向前进一格(因此才会有蹩马腿的存在)。所以两个可以相互攻击的马,实际上进攻路线是不同的,两个马的“马腿”也处在不同的位置。 如果在判断的时候只考虑马 \(A\) 是否能攻击到某个马 \(B\),而忘记考虑马 \(B\) 是否能攻击到这个马 \(A\) 的话,就会出错。
如果我们要在 \((x,y)\) 处放一个马,它与 \((x',y')\)(默认它们满足日字形位置关系)的马不会互相攻击,需要两个马,分别蹩掉 \((x,y)\) 和 \((x',y')\) 的马腿。显然,符合位置关系的 \((x',y')\in\{(x-2,y-1),(x-2,y+1),(x-1,y-2),(x-1,y+2)\}\),需要对这四个位置分别判断。
#include <bits/stdc++.h>
#define D(x,i) (((x) >> (i)) & 1) //取出x的第i位
using namespace std;
const int N = 6, MOD = 1e9 + 7;
int n, m, dp[105][1 << N][1 << N];
bool check(int now, int pre1, int pre2) {
for (int i = 0; i < m; ++i) {
if (D(now, i)) {
if (D(pre1, i - 2) && !(D(now, i - 1) && D(pre1, i - 1))) return 1;
if (D(pre1, i + 2) && !(D(now, i + 1) && D(pre1, i + 1))) return 1;
if (D(pre2, i - 1) && !(D(pre1, i) && D(pre1, i - 1))) return 1;
if (D(pre2, i + 1) && !(D(pre1, i) && D(pre1, i + 1))) return 1;
}
}
return 0;
}
int dfs(int u, int pre1, int pre2) {
if (u == n + 1) return 1;
if (dp[u][pre1][pre2]) return dp[u][pre1][pre2];
for (int i = 0; i < (1 << m); ++i) {
if (check(i, pre1, pre2)) continue;
dp[u][pre1][pre2] = (dp[u][pre1][pre2] + dfs(u + 1, i, pre1)) % MOD;
}
return dp[u][pre1][pre2];
}
int main() {
scanf("%d%d", &n, &m);
printf("%d", dfs(1, 0, 0));
return 0;
}