原题
题目描述
在 $n \times n$ 的棋盘上放 $k$ 个国王,国王可攻击相邻的 $8$ 个格子,求使它们无法互相攻击的方案总数。
输入格式
共一行,包含两个整数 $n$ 和 $k$。
输出格式
共一行,表示方案总数,若不能够放置则输出$0$。
数据范围
$1 \le n \le 10$,
$0 \le k \le n^2$
输入样例:
3 2
输出样例:
16
算法解析
算法构造
这道题目,根据数据范围,不难得出,这道题目考察的是状态压缩动态规划。
分析题目,我们可以得到如下信息。
- 一个点的相邻八格,不可以有其他点。
- 棋盘置点类型。
那么,我们接下来,思考两个流程。
- 如何表示状态
- 如何转移方程
表示状态
显然,题目给的条件,是国王总数是严格限制的,就是k个。
所以说,我们放置了多少个国王,是需要考虑的。
接着,根据棋盘类型的状态压缩动态规划的套路,每一行的状态,我们需要明白。
也就是每一行,哪些位置放了国王。
综上所述,我们可以得出,动态规划的状态表示。
$$ f[i][j][s]为所有只摆在前i行,目前摆了j个国王,而且第i行的摆放状态为s $$
我们可以举一个例子
$$ n=5 \\\\ f[1][2][20]表示第一行,已经摆了两个国王,摆在左边第一个,和左边第三个 \\\\ (20)_{10}=(10100)_{2} $$
状态转移
在这里,状态之间的转移,必然要满足,国王之间不会相互攻击到,那么我们进行分析。
两个国王,如果他们存在,直接靠近(上下左右)或者简介靠近(两斜对角),那么显然是不合法的。
因此,转换成为状态理解。
对于一个状态集合而言,显然不能存在相邻的1.
$$
101 (可以) \quad 两个国王有间隔\\\\
110 (不可以) \quad 国王1和国王2相邻,可以相互攻击\\\\
$$
因为这会导致,左右两个国王相邻,然后发起攻击。
而且,对于上下两行而言,不能有共同的一位有1
$$
101 \\\\
101
$$
因为这会导致,上下两个国王相邻,然后发起攻击。
我们讨论完了,上下左右,接下来是最难的两斜对角。
$$
我们设,第i行的状态为a,第i+1行状态为b
$$
那么
$$
S=a 或 b \\
也就是S=a|b
$$
是不可以存在,有相邻的1的。
$$
a=100 \\\\
b=010 \\\\
S=110 \\\\
$$
因此这会导致,两斜对角国王相互攻击。
综上所述,我们得到集合转移的约束条件。
下面是代码部分,有很详细的解说
解题代码
#include <bits/stdc++.h>
using namespace std;
const int N=(1<<10)+20;
vector<int> state,head[N];
//state放置合法状态,head[a]表示a对应的放置集合状态可以转移到的集合状态。
int n,k;
long long f[12][110][N];
int cnt[N];
inline bool check(int x)//这里是检查有没有出现相邻的两个1
{
for(int i=0; i<n; i++)
if ( (x>>i & 1) && (x>>i+1 & 1) )//第i位是1,而且第i+1位也是1
return false;//左右攻击
return true;
}
/*
这里是O(1)算法,来自抽风大佬!!!
inline bool check(int x)//这里是检查有没有出现相邻的两个1
{
return !(x&x>>1);
}
*/
inline int count(int x)//统计这个状态有多少个1,也就是放置了多少个国王
{
int ans=0;
for(int i=0; i<n; i++)
ans+=(x>>i & 1);
return ans;
}
inline void pre()
{
//首先我们找出,所有满足不可能左右攻击的合法放置国王的状态,这是第一步筛除不合法的方案
for(int i=0; i<(1<<n); i++)
if (check(i))//这里,我们存储所有无法左右攻击的合法状态
{
state.push_back(i);
cnt[i]=count(i);//统计这个状态有多少个国王
}
for(int i=0; i<state.size(); i++)//枚举所有状态
for (int j=0; j<state.size(); j++)
{
int a=state[i],b=state[j];
if ( (a & b)==0 && check(a | b) )//这里第一个检查上下攻击,第二个检查两斜对角攻击
head[i].push_back(j);//那么j对应状态,可以转移到i对应的状态。这里我们i,j都是状态对应的坐标
}
}
inline void init()
{
scanf("%d%d",&n,&k);
pre();
f[0][0][0]=1;
for(int i=1; i<=n+1; i++)//n行转移,n+1行是为了好统计答案
for(int j=0; j<=k; j++)//目前使用的国王棋子数量
for(int a=0; a<state.size(); a++)
for(int b=0; b<head[a].size(); b++)
{
int c=cnt[state[a]];//统计state[a]对应的国王个数
if (j>=c)//目前一共放j个国王,从b转移到a,要满足国王数不超标
f[i][j][a]+=f[i-1][j-c][head[a][b]];
}
cout<<f[n+1][k][0]<<endl;//第n+1行什么都不放,相当于只在1~n行放国王,目前一共放了k个国王的总方案数,其实就是答案要求的方案
}
signed main()
{
init();
return 0;
}
为什么蒙德里安的梦想那道题最后输出的是f[n][0],而这道题输出的是f[n+1][k][0]呢?为什么不是f[n][k][0]?
好兄弟,遇到和你一样的问题了,你解决了吗
看状态表示就怎么了,因为不知道最后一排放了几个皇后,但是最后一排后一排肯定是没有放皇后的,所以用第n+1排更好表示答案
因为状态转移从i-1层转移过来,所以只能从1开始,即0行啥都不用放,所以第n行也要放国王的
牛逼 看这个看明白了 爱了
醍醐灌顶
给大佬点赞,明白a | b是怎么来的了。。
希望秦佬或者其他大佬可以回答一下我在用户Bianca评论区问的问题
大佬,那个为啥 是head[i].push_back(j),为啥存的是下标,不是head[a].push_back(b)吗,不应该直接存那个状态吗,然后最后面 直接 f[][][b]
啊,难道那个f[][][b]的第三维b 表示 的是下标,不是二进制数吗
啊,难道那个f[][][b]的第三维b 表示 的是下标,不是二进制数吗
同问,初始化f[0][0][0]的第三维的这个0不是表示状态吗?后面转移为什么用下标进行转移呀?不是应该用二进制数吗?
是二进制数,只是通过上面一部分(求state那部分)已经求出了所有合法的二进制数表示,并全部存在了state里,对state进行遍历其中所有的数字都是合法的
想问个问题,倒数第9行的状态转移方程为啥是f[i][j][a]+=f[i-1][j-c][head[a][b]];而不是f[i][j][state[a]]+=f[i-1][j-c][state[head[a][b]]]呢?
我这里题解和代码的定义有所差别,估计以后会重新修订一下了。
那个啥 为啥从数据范围能看出是状压啊
这个靠经验2333,要像秦大佬一样做大量的题才能有这种经验
一开始做如果没啥经验的话,可以参考这篇分享{:target=”_blank”}
通过这篇分享,可以得到这题应该是爆搜/状压dp
但是爆搜要一次枚举每个点是否放国王,每个点有放国王或不放国王两种状态,故爆搜的时间复杂度是 $O(2^{n^2})$ 的,会 $\text{TLE}$
所以要考虑状压dp,这里的状压dp时间复杂度是 $O(nk2^{2n})$ 的
注意这个时间复杂度是上限,由于预处理,实际计算量会比这个少很多
head[i].push_back(j) 秦大佬 这段代码对应的应该是将j对应的状态 可以转移到i对应的状态吧 这样也跟后面的转移方程相对应 由b状态转移到a状态
是的,我笔误了
看了y总的视频讲解 有些疑惑 看完秦大佬的的讲解瞬间豁然开朗 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
不会吧,我觉得y总讲解比我好多了啊
主要是我比较菜 y总讲解的时候有点跟不上 课后思考要花好久 有些地方也不理解 看了您这篇详解 豁然开朗
y总讲解大纲,我这里主要是细节处理。
因为我也是初学者,所以可能咱们思路是一样的
秦大佬就不必谦虚说自己是初学者了8%%%
老学者,能力是初学者
希望秦大佬多写写提高课的题解%%%%%%%
秦dalao好久没写题解了,顶
%%%wh聚聚
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%
这题在check里面,其实有个y总没讲到的优化(我之前整理位运算的时候想到的)
可以改成:
直接用按位与和按位左移操作,$O(1)$判断是否存在两个相邻的$1$
妙啊
这个优化雀食好妙啊~
(中间有一行的latex失效惹
感谢,已修改好了。
$\text{%%%太猛了Orz}$
写一篇题解这么快的嘛,我写一题题解要一天。。
您写省选题,我写普及题,这个质量不一样