题解 P1896 [SCOI2005]互不侵犯
题目描述
给定一个 \(n\times n\) 的棋盘,在其中放置 \(k\) 个国王,使这些国王相互无法攻击,求方案数。
国王能攻击到与它八连通(与这个格子有公共定点的)的任意一个格子。
\(1 \leq n \leq 9 ,1 \leq k \leq n^2\)
Solution
发现 \(n\) 非常小,并且一行的放置会不会有冲突之和上一行有关,考虑状压 DP 。
设 \(f_{i,j,k}\) 表示前 \(i\) 行放置 \(j\) 个国王,且第 \(i\) 行的放置状态是 \(k\) 的方案数。
状态的属性: \(\mathcal{Count}\) 。
转移的时候可以枚举上一行的状态 \(p\) ,设 \(vaild(i,j)\) 表示 \(i,j\) 能不能放在相邻两行,则有:
\[f_{i,j,k} = \sum_{vaild(k,p)} f_{i-1,j-count(k),p}
\]
边界: \(f_{0,0,0}=0\) 。
目标: \(\sum_i f_{n,k,i}\) 。
其中 \(count(x)\) 表示在状态是 \(x\) 的一行放置了几个国王(二进制中 \(1\) 的个数)。
分析一下复杂度: \(\mathcal{O}(nk2^n2^n)=\mathcal{O}(nk4^n)\) 。
我们可以发现,在同一行里,有很多状态是不合法的,我们可以预处理出所有合法的状态,枚举的时候直接枚举合法的状态就可以。
同一行内合法的状态大概有 \(150\) 个,所以总复杂度为 \(\mathcal{O}(nk150^2)\) 。
代码如下:
#include <cstdio>
#include <cstring>
#include <cctype>
typedef long long LL;
inline bool check(int x) { //判断当行状态是否合法
return !((x & (x >> 1)) || (x & (x << 1)));
}
inline bool check(int x ,int y) { //判断相邻两行是否合法
return !((x & y) || ((x << 1) & y) || ((x >> 1) & y));
}
inline int lowbit(int x) {return x & (-x);}
inline int count(int x) { //数二进制下 1 的个数
int ans = 0;
while (x) ans++ ,x -= lowbit(x);
return ans;
}
const int N = 15 ,S = 155 ,K = 105;
LL f[N][K][S]; //记得开 long long
int n ,m ,s[S] ,bin[S] ,idx; //s 存储所有合法的状态 ,bin 保存每个状态中 1 的个数。
signed main() {
scanf("%d%d" ,&n ,&m);
f[0][0][1] = 1; //这里是 f[0][0][1] 是因为下面的状态存下里的都是从 1 开始编号,而 s[1] = 0
for (int i = 0; i < (1 << n); i++)
if (check(i)) s[++idx] = i ,bin[idx] = count(i);
for (int i = 1; i <= n; i++)
for (int k = 1; k <= idx; k++)
for (int j = 0; j <= m; j++)
if (bin[k] <= j) //当行的个数比总个数还多
for (int p = 1; p <= idx; p++)
if (check(s[k] ,s[p]))
f[i][j][k] += f[i - 1][j - bin[k]][p];
LL ans = 0;
for (int i = 1; i <= idx; i++)
if (bin[i] <= m)
ans += f[n][m][i];
printf("%lld\n" ,ans);
return 0;
}