小国王

小国王

题目描述

在这里插入图片描述


题意解释

红色表示一个国王,绿色表示另一个国王

在这里插入图片描述


核心思路

前置知识:位运算

在这里插入图片描述

3 × 3 3\times 3 3×3九宫格为例子:

0表示没有放国王,1表示放了国王。

每一行所有的状态为:000,001,010,011,100,101,110111,映射到十进制整数就是0,1,2,3,4,5,67,染色部分表示不合法的状态(因为每一行的国王是不能相邻的,不然会产生攻击)。因此,去掉不合法的状态后,剩下的就是合法状态了。所以,每一行的合法状态为: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[i1][jc[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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值