小国王
题目描述
题意解释
红色表示一个国王,绿色表示另一个国王
核心思路
前置知识:位运算
以 3 × 3 3\times 3 3×3九宫格为例子:
0表示没有放国王,1表示放了国王。
每一行所有的状态为:000,001,010,011,100,101,110,111,映射到十进制整数就是0,1,2,3,4,5,6,7,染色部分表示不合法的状态(因为每一行的国王是不能相邻的,不然会产生攻击)。因此,去掉不合法的状态后,剩下的就是合法状态了。所以,每一行的合法状态为:000,001,010,100,101。
如何理解cnt表示同一行的合法状态的个数呢?
第i行的所有状态有:000,001,010,011,100,101,110,111。其中合法的状态有5个:000,001,010,100,101。所以cnt=5
如何理解 s [ 1 < < 12 ] s[1<<12] s[1<<12]数组是用来记录同一行的合法状态集呢?
第i行的所有状态有:000,001,010,011,100,101,110,111。其中合法的状态有5个:000,001,010,100,101。因此s数组存储的就是这5个合法的状态。
如何理解 n u m [ 1 < < 12 ] num[1<<12] num[1<<12]数组是用来记录每个合法状态包含的国王个数呢?
第i行的所有状态有:000,001,010,011,100,101,110,111。其中合法的状态有5个:000,001,010,100,101。比如对于101,那么num数组所存储101这个合法状态的国王个数就是2。
如何理解枚举行是从i<=n+1而不是i<=n呢?最后输出的是 f [ n + 1 ] [ k ] [ 0 ] f[n+1][k][0] f[n+1][k][0]呢?
这是一种编程的技巧,虽然有n行,但是我们算到了n+1行。然而第n+1行中的状态数量为0,第n+1行什么都不放,相当于没有处理第n+1行,那么相当于只在1~n行放置了国王。
其实它就等价于我们写了i<=n,然后还要加上这段代码
int ans=0;
for(int i=0;i<cnt;i++)
ans+=f[n][k][i];
cout <<ans<<endl;
但是很明显,写成i<=n+1的话,可以省事。
如何判断每行是否合法呢?即如何判断每行中国王没有相邻呢?
如果 ! ( i & i > > 1 ) !(i\&i>>1) !(i&i>>1)为真,则说明i是合法的。举个例子,i=101,执行i>>1后,得到010,和i相与后,即101&010=000,由此可知,i=101是合法状态,其实也就是让i右移一位得到x,那么x和i就错开了一位,于是就可以判断原来i的相邻两位之间是否都是1,即判断国王是否相邻。
如何判断上下两行之间放置国王是否合理呢?
!(a&b)其实是判断b的中间那格是0还是1;!(a&b>>1)其实是判断b的左上角那一格是0还是1,先让b右移一位,使得左上角那一格与a中间的1对齐,相与即可知道b左上角的那格是0还是1了;
!(a&b<<1)其实是判断b的右上角那一格是0还是1,先让b左移一位,使得右上角那一格与a中间的1对齐,相与即可知道b右上角的那格是0还是1了;
那么通过这样判断,就能知道上下两个之间放置的国王是否合理了。比如对于下面一行a状态为001,我们要看上一行b中的哪个状态与001不冲突,那么经过if语句,就可以知道000和100状态与a状态001不冲突。
状态表示:
f [ i ] [ j ] [ a ] f[i][j][a] f[i][j][a]表示前i行已经放了j个国王,第i行的第a个状态的方案数。
状态计算:
f [ i ] [ j ] [ a ] = ∑ f [ i − 1 ] [ j − c [ a ] ] [ b ] f[i][j][a]=\sum f[i-1][j-c[a]][b] f[i][j][a]=∑f[i−1][j−c[a]][b]。其中j表示前i行一共放了j个国王,c[a]表示第i行放了c[a]个国王,那么前i-1行就放了j-c[a]个国王。a表示枚举第i行所有合法状态中的哪一个状态,比如a枚举的是001这个合法状态,b表示枚举第i-1行所有合法状态中的哪一个状态且这个合法状态与第i行的a状态不冲突,那么由上图可知,b为000和100.
总方案数:
a n s = ∑ f [ n ] [ k ] [ a ] ans=\sum f[n][k][a] ans=∑f[n][k][a]。
代码
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N=12;
int n,k; //行数、国王总数
int cnt; //同一行的所有合法状态的个数
int s[1<<N]; //记录同一行的所有合法状态的集合
int num[1<<N]; //每个合法状态包含的国王数
//f[i][j][a]表示前i行放了j个国王,第i行第a个合法状态时的方案数
LL f[N][N*N][1<<N];
int main()
{
cin >>n>>k;
//预处理 算出一行内的合法状态
for(int i=0;i<(1<<n);i++) //枚举一行的所有状态
{
if(!(i&i>>1))//如果这一行中不存在相邻的1,说明国王不相邻,是合法的
{
s[cnt++]=i; //保存一行的合法状态,这个合法的二进制状态它的十进制是i
//对合法的这个二进制状态,依次查看它的二进制位中有多少个1,则说明有多少个国王
for(int j=0;j<n;j++) //统计每个合法状态包含的国王个数,比如合法状态0101就有2个国王
num[i]+=(i>>j&1);
}
}
f[0][0][0]=1;//不放国王,则不会产生攻击,那么也是一种方案
for(int i=1;i<=n+1;i++)//枚举行
{
for(int j=0;j<=k;j++)//枚举国王的个数
{
for(int a=0;a<cnt;a++)//枚举第i行中所有的合法状态,比如000,001,010,100,101就是cnt=5个合法的状态
{
for(int b=0;b<cnt;b++)//枚举第i-1行的合法状态,比如000,001,010,100,101就是cnt=5个合法的状态
{
//s[a]表示{000,001,010,100,101}这些合法状态中某个合法状态,比如100,只不过s[a]是100映射的
//十进制数4而已,num[s[a]所表示的就是100这个合法状态中所含国王的个数
int c=num[s[a]];//第i行中第a个合法状态所含有的国王个数
//判断上下两行之间是否可以合理放置国王
//这里j>=c不然如果j<c,则j-c为负数了
if((j>=c)&&!(s[a]&s[b])&&!(s[a]&(s[b]<<1))&&!(s[a]&(s[b]>>1)))
f[i][j][a]+=f[i-1][j-c][b];//从第i-1行向第i行转移
}
}
}
}
cout <<f[n+1][k][0]<<endl;//第n+1行什么都不放,相当于只在1~n行放置了国王
return 0;
}
#include<iostream>
#include<vector>
using namespace std;
typedef long long LL;
const int N=12,M=1<<12;
int cnt[M]; //存储某个合法状态中所含有的国王个数,比如0101这个合法状态则有两个国王
vector<int>state;//用来存储合法状态,是合法状态的集合.比如存储000 001 010 100 101这些合法状态
//存储与合法状态不冲突的方案 比如对于001来说,与它不冲突的合法状态就是000 100
//所以head[001]={000,100}
vector<int>head[M];
LL f[N][N*N][M];
int n,k;
//检查每行内相邻之间是否有国王,即检查行内的这个state状态是否为合法状态
bool check(int state)
{
for(int i=0;i<n;i++)
{
if((state>>i&1)&&(state>>(i+1)&1))
return false;
}
//也可以不用上面的for循环,而直接用下面的if语句即可
// if((state&state>>1))
// return false;
return true;
}
//统计这个state合法状态中有多少个国王 1表示有国王,0表示没有国王
int count(int state)
{
int res=0;
for(int i=0;i<n;i++)
res+=state>>i&1;
return res;
}
int main()
{
cin >>n>>k;
//预处理一行内的所有的合法状态
for(int i=0;i<(1<<n);i++)
{
if(check(i))//判断i是否为合法状态
{
cnt[i]=count(i);//统计i这个合法状态中有多少个国王
state.push_back(i);//将i这个合法状态加入合法状态集合中
}
}
//判断上下两行之间是否有国王攻击
for(int i=0;i<state.size();i++)//下一行 比如状态集合为{000,001,010,100,101}
{
for(int j=0;j<state.size();j++)//上一行 比如状态集合为{000,001,010,100,101}
{
//假设此时枚举到下一行的state[i]=001,我想看看上一行的所有合法状态中哪个合法状态不与state[i]冲突
//显然,上一行中000,100这两个状态不会与state[i]产生冲突,000对应下标j=0,100对应下标j=3
//所以head[001]={000,100},用十进制来存储状态就是:head[1]={0,3}
if(!(state[j]&state[i])&&check(state[i]|state[j]))
head[i].push_back(j);
}
}
//处理边界
f[0][0][0]=1;//第0行不放国王就不会产生攻击,因此也是一种合法方案
for(int i=1;i<=n+1;i++) //从第1行枚举到第n+1行
{
for(int j=0;j<=k;j++) //枚举国王的数量
{
for(int a=0;a<state.size();a++) //枚举下一行即第i行中所有的合法状态 a表示是第几个合法状态
{
for(int b=0;b<head[a].size();b++)//枚举上一行即第i-1行中所有的合法状态 b表示是第几个合法状态
{
//获取第i行中枚举到的第a个合法状态中的国王个数
int c=cnt[state[a]];
if(j>=c)//总共只有j个国王,所以必须要小于等于j个
{
f[i][j][a]+=f[i-1][j-c][head[a][b]];//状态转移
}
}
//也可以这么写 但是很奇怪,如果写成b=0;b<head[a].size();b++ 就会出错...
// for (int b : head[a])
// {
// int c = cnt[state[a]];
// if (j >= c)
// f[i][j][a] += f[i - 1][j - c][b];
// }
}
}
}
cout <<f[n+1][k][0]<<endl;
return 0;
}