首先,让我们来看一下今天的题目:
[原题]
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。
[输入格式]
只有一行,包含两个数N,K ( 1 <=N <=9, 0 <= K <= N \* N)
[输出格式]
所得的方案数
[输入样例]
3 2
[输出样例]
16
[解题思路]
在一个N×N的棋盘中,我们需要摆放不多于K个国王,使他们互不攻击。其中每个国王的攻击范围为自身周围的八格范围。
在审完题之后,我们不难联想到K皇后的题目。与本题不同的是,皇后的攻击范围为所在行、列及两条对角线所覆盖的所有区域。而我们当时采用深度优先搜索的方法,利用一维数组对应记录每行、每列以及每个对角线的攻击覆盖情况,从而枚举所有可能性。
而在本题中,由于国王的攻击范围仅为自身周围八格,若采用dfs进行深搜则需要使用二维数组记录当前每一格的攻击覆盖情况,同时需要不断更新状态,那么这样的做法就显得过于复杂。因此,我们今天采用状态压缩dp的方法,对这个问题进行时空上的优化。
二进制存储优化
在之前,我们如果想记录每一格是否放入了国王棋,那我们肯定不难想到用一个bool类型的二维数组去记录每一格是否落下了棋子。现在,我们不妨转换一下思维——我们都知道,bool类型的true和false本身分别对应了1和0,那我们能否将此时每一行的状态看作一个二进制数并进行统一的存储呢?
如下图4*4的棋盘中,对于第一行的状态{true,false,true,false},我们不难得出对应的二进制数1010,再进行进制转换,我们就可以得到一个十进制数10;同样地,对2、3、4行进行相同的转化,我们便可以得到每一行对应的数字10、0、8、1。这样,原本4*4的二维数组我们便只需要使用4个数字便可以表示出来。
通过这种方式,不论N取多少,我们都可以将一个数对应一整行的状态。特别地,当N=4时,这些状态的范围即为[0,15],我们便可以通过对0~15的遍历来枚举每一行的每一个状态。当然,这其中并不是每一个状态都是合法的,因为我们需要舍弃一些棋子相邻的情况。接下来我们就要筛选攻击范围这个约束条件下的合法情况,而由于我们是采用二进制存储行状态,因此我们需要进行按位运算来实现约束条件的限制。
合法性检验
(1)行内合法:即本行之内不存在左右相邻的棋子。
如上图所示,若某行i存在一个状态j,且j存在左右相邻的棋子,那么当对j右移一位时,必存在一位与原数同为1。我们只需要筛去这种情况,则其他情况皆为合法。因此,合法状态必满足条件
!(j & j >> 1)
(2)行间合法:即相邻两行A和B不存在上下相邻的棋子。(因为国王的攻击范围为周围八格,因此这里的上下相邻也包含斜对角的上下相邻)
如上图所示,对于状态十进制数为a、b的相邻两行,我们同样可以用and运算来判断是否存在同一位都为1的情况,同时,还需要对相邻两位进行同样的判断,这点只需进行移位操作即可。因此合法状态必须满足条件
!(a & b) && !(a & b >>1) && !(a & b << 1)
状态转移
讨论完了状态合法性的检验,接下来就要开始设计dp数组了。dp数组的设计原则就是:能利用限定的参数特征性地表示出每一个独立的状态,并能求出对应的状态转移方程。在这里,我们使用三维数组dp[i][j][k]进行状态转移,其对应的含义为:当前行为i,一共已放置了j个国王棋,当前行选择的状态十进制数为k。
这样,要想求当前状态的方案总数,我们就要枚举上一个dp状态的合法方案总数并进行加和。而接下来,我们要做的就是从当前dp状态回退至上一个dp状态:首先,由于我们是对行数进行逐一选择的,那么上一个dp状态的行标一定是i - 1;同样,若要回退至上一个dp状态,我们需要减去当前行的国王数量c(这个数值需要在初始化过程中单独求出),则第二个参数应为j - c;另外,当前行状态a需要回退至上一行的行状态b。因此,我们不难得出状态转移方程
dp[i][j][a]+=dp[i-1][j-c][b]
代码实现
现在,我们需要使用代码去实现这一过程。我们可以从上述的转移方程中看出,对于每一次状态的转移,我们需要枚举四个参数:行标i,国王个数j,当前行状态a和上一行状态b。因此在初始化阶段,我们需要把每一个合法状态找出来,并计算相应的国王个数。
这段代码如下:
for (int j = 0; j < (1 << n); j++)
{
if (!(j & j >> 1))//对行内合法性的检验
{
feasible.push_back(j);//存储合法状态
int cnt = 0;
for (int k = 0; j >> k; k++)
{
cnt += (j >> k & 1);//提取每一位并相加即为每个状态的国王数量
}
num.push_back(cnt);//存储每个合法状态的国王数量
}
}
接下来,我们对四个参数分别进行枚举,实现动规过程,这段代码如下:
for (int i = 1; i <= n + 1; i++)//1->n+1 多计算一轮使答案全部加和至n+1行,便于输出
{
for (int j = k; j >= 0; j--)
{
for (int cur = 0; cur < feasible.size(); cur++)
{
for (int pre = 0; pre < feasible.size(); pre++)
{
if (!(feasible[cur] & feasible[pre]) && !(feasible[cur] & feasible[pre] << 1) && !(feasible[cur] & feasible[pre] >> 1) && j - num[cur] >= 0)//对行间合法性的检验
dp[i][j][cur] += dp[i - 1][j - num[cur]][pre];//加和形式记录总方案数
}
}
}
}
整段代码如下:
#include <iostream>
#include <string.h>
#include <climits>
#include <vector>
#define INF 0x3f3f3f3f
typedef long long ll;
using namespace std;
const int maxsize = 12;
ll dp[maxsize][maxsize * maxsize][1 << maxsize];//每一位对应[行数][当前国王数量][状态]
vector<int> feasible;//存储可行的十进制状态
vector<int> num;//记录每个可行状态中存在的国王数量
int main()
{
//数据读入及初始化
int n, k;
cin >> n >> k;
for (int j = 0; j < (1 << n); j++)
{
if (!(j & j >> 1))//对行内合法性的检验
{
feasible.push_back(j);//存储合法状态
int cnt = 0;
for (int k = 0; j >> k; k++)
{
cnt += (j >> k & 1);//提取每一位并相加即为每个状态的国王数量
}
num.push_back(cnt);//存储每个合法状态的国王数量
}
}
//初始化:不放置国王棋也是一种方案
dp[0][0][0] = 1;
//动态规划
for (int i = 1; i <= n + 1; i++)//1->n+1 多计算一轮使答案全部加和至n+1行,便于输出
{
for (int j = k; j >= 0; j--)
{
for (int cur = 0; cur < feasible.size(); cur++)
{
for (int pre = 0; pre < feasible.size(); pre++)
{
if (!(feasible[cur] & feasible[pre]) && !(feasible[cur] & feasible[pre] << 1) && !(feasible[cur] & feasible[pre] >> 1) && j - num[cur] >= 0)//对行间合法性的检验
dp[i][j][cur] += dp[i - 1][j - num[cur]][pre];//加和形式记录总方案数
}
}
}
}
//输出答案
cout << dp[n + 1][k][0];
return 0;
}