洛谷P5005
洛谷传送门
吐槽
并不算个难题,主要是调的时间稍微有点久了,所以放上来记录一下 水一下博客 。
思路
数据范围很小,又是求方案数类的问题,不关心具体方案,不关心具体方案会让我们想到 \({\rm DP}\) 的做法。数据很小又让我们想到了状态压缩的进行 \({\rm DP}\) 。另外这道题属实和 炮兵阵地 很是相似,故而可以猜测,这也许是一个状压 \({\rm DP}\) 吧,不妨设 \(f[i][S]\) 表示前 \(i\) 行,第 \(i\) 行状态为 \(S\) 的合法方案数,发现此时并不能转移,因为假设暴力枚举了上一行,我们并不一定能把上一行所有状态都拿过来加,理由是我们不知道上上行具体如何,所以发现此时应该记录 \({\rm f[i][now][lst]}\) ,暨连续记录这一层和上一层比较合理,对于第一层,由于没有上一层,故预处理,对于第二层,由于只有一层限制,故也预处理,然后就是后面的转移了,我们暴力枚举这一层,上一层,上上层,分别判断是否有冲突即可。
至于空间可能会开不下的问题,使用滚动数组就行了,应该可以压成 \(2\) 毕竟每一个状态压两行,所以转移只需要上一行,不过代码中实现是 \(3\) 。
预处理:
预处理:( \({\sf ps:}\) 其中的 \({\rm pls}\) 是取模优化的加法,理解为加号即可,至于 \({\rm jump1}\) 很快就会讲到,是用来判断冲突的)。
for (int i = 0; i < (1 << m); ++i) dp[1][i][0] = 1; //! 第一行随便选
for (int j = 0; j < (1 << m); ++j) {
for (int k = 0; k < (1 << m); ++k) { //! 枚举第一行和第二行
if ((jump1(j) & k) || (jump1(k) & j)) {}
else {
// dp[2][k][j] = 1;
pls(dp[2][k][j], dp[1][j][0]);
// print(k), print(j);
// system("pause");
}
}
}
冲突判断 (一些简化结构的函数):
具体来说,我们可以对于一层,暴力枚举每一匹马,然后看左右是不是蹩马腿,如果不,就记录可以攻击的位置,把可以攻击的位置压成一个二进制数传回去。
实现:
int jump1(int state) {
int re = 0;
for (int j = 0; j < m; ++j) {
if ((state >> j) & 1) {
if (j > 1) {
if ((state >> j - 1) & 1) {}
else {
re |= (1 << j - 2);
}
}
if (j < m - 2) {
if ((state >> j + 1) & 1) {}
else {
re |= (1 << j + 2);
}
}
}
}
return re;
}
对于马隔两行攻击,我们只需要把两行状态都传进去,就可以知道应该攻击到哪些点了。
实现:
int jump2(int state1, int state2) {
int re = 0;
for (int j = 0; j < m; ++j) {
if ((state1 >> j) & 1) {
if (j > 0) {
if ((state2 >> j) & 1) {}
else {
re |= (1 << j - 1);
}
}
if (j < m - 1) {
if ((state2 >> j) & 1) {}
else {
re |= (1 << j + 1);
}
}
}
}
return re;
}
转移主体:
那么目前,我们已经有了预处理,以及如何判断冲突,接下来就是比较容易的转移了。
for (int i = 3; i <= n; ++i) {
// std::memset(dp[i % 3], 0, sizeof dp[i % 3]);
for (int j = 0; j < (1 << m); ++j) {
for (int k = 0; k < (1 << m); ++k) {
dp[i % 3][j][k] = 0;
if (jump1(k) & j) continue;
if (jump1(j) & k) continue;
for (int w = 0; w < (1 << m); ++w) {
if (jump1(w) & k) continue;
if (jump1(k) & w) continue;
if (jump2(w, k) & j) continue;
if (jump2(j, k) & w) continue;
// print(w), print(j), print(k);
pls(dp[i % 3][j][k], dp[(i - 1) % 3][k][w]);
}
// if (i == n) pls(re, dp[i % 3][j][k]);
}
}
}
小疑问
这里的统计答案被注释掉了,不知道为什么会 \({\rm WA}\) 一个点,所以换了一种统计答案的方式,如果有知道的巨佬可以 \({\rm educate}\) 我一下。
整体 \(\large{\rm Code}\)
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
const int md = 1e9 + 7;
int n, m;
int dp[3][1 << 6][1 << 6];
int jump1(int state) {
int re = 0;
for (int j = 0; j < m; ++j) {
if ((state >> j) & 1) {
if (j > 1) {
if ((state >> j - 1) & 1) {}
else {
re |= (1 << j - 2);
}
}
if (j < m - 2) {
if ((state >> j + 1) & 1) {}
else {
re |= (1 << j + 2);
}
}
}
}
return re;
}
int jump2(int state1, int state2) {
int re = 0;
for (int j = 0; j < m; ++j) {
if ((state1 >> j) & 1) {
if (j > 0) {
if ((state2 >> j) & 1) {}
else {
re |= (1 << j - 1);
}
}
if (j < m - 1) {
if ((state2 >> j) & 1) {}
else {
re |= (1 << j + 1);
}
}
}
}
return re;
}
inline void pls(int &a, int b) { (a += b) >= md && (a -= md); }
inline void print(int x) {
int i = 0;
while (i < m) putchar(x % 2 + '0'), x /= 2, ++i;
puts("");
}
signed main() {
scanf("%d%d", &n, &m);
if (n == 1) {
return printf("%d\n", (1 << m)), 0;
}
for (int i = 0; i < (1 << m); ++i) dp[1][i][0] = 1;
for (int j = 0; j < (1 << m); ++j) {
for (int k = 0; k < (1 << m); ++k) { //! 枚举第一行和第二行
if ((jump1(j) & k) || (jump1(k) & j)) {}
else {
// dp[2][k][j] = 1;
pls(dp[2][k][j], dp[1][j][0]);
// print(k), print(j);
// system("pause");
}
}
}
int re = 0;
for (int i = 3; i <= n; ++i) {
// std::memset(dp[i % 3], 0, sizeof dp[i % 3]);
for (int j = 0; j < (1 << m); ++j) {
for (int k = 0; k < (1 << m); ++k) {
dp[i % 3][j][k] = 0;
if (jump1(k) & j) continue;
if (jump1(j) & k) continue;
for (int w = 0; w < (1 << m); ++w) {
if (jump1(w) & k) continue;
if (jump1(k) & w) continue;
if (jump2(w, k) & j) continue;
if (jump2(j, k) & w) continue;
// print(w), print(j), print(k);
pls(dp[i % 3][j][k], dp[(i - 1) % 3][k][w]);
}
// if (i == n) pls(re, dp[i % 3][j][k]);
}
}
}
for (int i = 0; i < (1 << m); ++i) {
for (int j = 0; j < (1 << m); ++j) {
if (jump1(i) & j) continue;
if (jump1(j) & i) continue;
pls(re, dp[n % 3][i][j]);
}
}
printf("%d\n", re);
}
一些小细节:
这里总结我犯得一些错误,也许会有所帮助 虽然显然大部分人不会和我一样蠢 。
- 错误的把象棋马的攻击当成可逆的了,也就是以为 \(A\) 能攻击 \(B\) 必然意味着 \(B\) 可以攻击 \(A\) ,后来发现不是,有可能有一个被蹩马腿,所以应该两个都判断一下。
- 滚动数组用的时候需要清空,否则会复用,导致结果算错。
- 位运算的时候就用位运算,跳跃函数用了加法,导致多个点可以攻击同个位置的时候,进位了,出现错误。