铅笔大佬写的题解很好,参考:https://www.acwing.com/solution/content/56348/
二进制表示进行状态压缩: 每个格子有两种状态,放或者不放。因此,我们用二进制数中的每一位代表一个格子。在一行中,10个格子可以用10位二进制数来表示。 假设如果格子放国王了,对应的二进制位置上为1,否则为0。
两种合法状态:
- 只要任意王之间只要 不相邻,就是 合法的状态,即二进制表示的数中没有连续两个1相邻。
if((state >> i & 1)&&(state >> i + 1 & 1))
return false;//如果存在连续两个1的话就不合法
- 相邻的两行中, 纵坐标不相邻,就是合法的转移状态
if((a&b)==0&&check(a|b)) 为真,代表两行之间的状态转移合法。
check函数检查state状态是否合法。
bool check(int state)
{
for(int i=0;i<n;i++)
if((state >> i & 1)&&(state >> i + 1 & 1))
return false;//如果存在连续两个1的话就不合法
return true;//否则的话就是合法的
}
状态表示
f[i][j][k] : 考虑前 i 层的棋盘,前 i 层放置了 j个国王,且第 i 层状态是 k 的方案总数
状态转移
当前第i层的f[i][j][k]要考虑到上一层所有的合法状态,它是上一层所有合法状态的总和。
具体怎么转移还是看一下代码吧,可不是那么容易就转移来的。
初始状态
f[0][0][0] = 1 表示第0行,放了0个国王,状态是0。这样的方案数量是1,不可以为0。这和上一节的线性dp,求价值最大不一样。这里求得是状态数量得总和。什么都没有也是一种状态。
目标状态
先存起来所有合法的状态,这样会减少很多对不合法状态的枚举。
附上一篇带y总字幕的详细代码:https://www.acwing.com/solution/content/42890/
// 铅笔大佬写的题解很赞,带y总字母的代码也很帅。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<cstdio>
using namespace std;
typedef long long LL;
const int N = 12; // 开到12很帅啊
// M是二进制状态的十进制表示
const int M = 1 << 10, K = 110; // k是国王的数量
int n, m; // m表示国王数量,输入
vector<int> state; //state 用来表示所有的合法的状态
int id[M]; // 一个状态 映射到 一个下标
vector<int> head[M]; // 每一个状态可以转移到的所有其他状态
int cnt[M]; /*然后cnt的话存的是每个状态里面 1 的个数,因为我们刚才我们的状态转移方程里面,
其实有一个需要求这个每一个状态里面1的个数的一个过程对吧*/
LL f[N][K][M];//这就是我们刚才说的那个状态表示
bool check(int state)
{
for(int i=0;i<n;i++)
if((state >> i & 1)&&(state >> i + 1 & 1))
return false;//如果存在连续两个1的话就不合法
return true;//否则的话就是合法的
}
int count(int state)//这里y总没具体解释,我补充一下,这里就是计算某个数二进制里面1的个数
{
int res=0;
for(int i=0;i<n;i++)res+=state>>i&1;
return res;
}
int main()
{
cin >> n >> m;
// 首先处理所有的合法状态
for(int i=0; i<1<<n; i++)
if(check(i))
{
state.push_back(i);
id[i] = state.size() - 1; // 合法状态为i,i在state中对应的下标为id[i]
cnt[i]=count(i);//cnt的话存的是这个i里面 1 的个数是多少
}
//然后我们来看一下这个不同状态之间的一个这个边的关系
// 在合法的状态之间,两两找关系。
for(int i=0; i<state.size(); i++)
for(int j=0; j<state.size(); j++)
{
//用a来表示第一个状态,用b来表示第二个状态
int a=state[i],b=state[j];
//这里是建立一个不同状态之间的转移关系
//先预处理一下哪些状态和哪些状态之间可以转移
//首先转移的话是要满足两个条件对吧
//一个是这个 a 和 b 的交集必须要是空集,它必须是空集才可以,否则的话同一列的两个国王会攻击到
//并且的话它们的这个这个并集的话也是需要去满足我们不能包含两个相邻的1的
if((a&b)==0&&check(a|b))
{
// 表示i可以转移到j,其实这个关系是双向的
// 只不过咱们会存两次罢了。
head[i].push_back(j);
}
}
//好,那剩下的就是 DP 了对吧
f[0][0][0]=1;
//最开始的时候,我们f[0][0][0]=1
/*什么意思呢,就是说,前0行对吧,我们前0行已经摆完了,其实也就是一行也没有摆的情况下,
那么此时由于我们这个是在棋盘外面,
所以肯定每个国王都不能摆对吧,所以我们就只有0这个状态时合法的,那么这个状态的方案数是1*/
//好,然后从前往后枚举每一种状态
for(int i=1;i<=n+1;i++)
for(int j=0;j<=m;j++)//j的话是从0到m对吧,m表示的是国王数量
for(int a=0;a<state.size();a++)//然后我们来枚举一下所有的状态,a表示第i行的状态
for(int b : head[a])//然后来枚举所有a能到的状态
{
//这里要判断一下
//首先要判断的是
//求一下我们a里面的一个1的个数对吧
int c=cnt[state[a]];
//好,然后如果说,呃,就我们的j必须要大于等于c对吧,j是必须要大于等于c的
//为什么呢,因为我们这个表示我们当前这行摆放的国王数量一定要小于等于我们整个的上限对吧
if(j>=c)//如果数说满足要求的话,那么我们就可以转移了
{
f[i][j][a]+=f[i-1][j-c][b];
//转移的话就是f[i][j][a]+=f[i-1][j-c][b],然后从b转移过来
}
}
//好,那我们最终的答案是什么呢?
//我们的最终的答案应该是这个f[n][m][ ],然后最后一维可以直接去枚举对不对
//去枚举一下最后一维是从,就是所有合法状态都是可以的,就最后一行它所有合法状态都是可以的对不对
//那这里的话我们可以偷个懒,不是偷个懒,我们可以有个小技巧,就是我们在枚举i的时候,枚举到n+1就可以了
//就是我们去算到第i+1行,假设我们的棋盘是一个n+1 * n的一个棋盘,多了一行
/*那么我们最终算的时候 就需要输出一个 f[n+1],就是第n+1行,
一共摆到了第n+1行,然后m,然后0,因为第n+1行一个都没摆,对吧*/
cout<<f[n+1][m][0]<<endl;
/*就是我们假设存在第n+1行,但是第n+1行完全没有摆,
那这种情况里面的所有方案其实就是等于这个这个我们只有n行的一个所有方案,对吧*/
/*那这样枚举n+1的一个好处是我们最后不需要再循环枚举最后一行的状态了,
就是我们这个f[n+1][m][0]已经在这个循环里面被循环算出来了*/
//所以可以少一层循环
/*这里的话就是为什么我们一开始N要从12开始,对吧,首先我们要用到11这个下标对吧,
那其实11这个下标是需要开长度是12才可以*/
return 0;
}