[模板]二进制状态压缩DP模板(详解

题目:

在 n*n(n≤20)的方格棋盘上放置 n 个车(可以攻击所在行、列),求使它们不能互相攻击的方案总数。

思路:

根据组合数学很明显是n!(n的阶乘)

我们把二进制中的 1 看做放了一个车,0 作为不放;

整个模板我们以n = 5的5*5的矩阵为例子


①开个for:1 to (1<<n)-1 这样就是一行一行放,保证一行只有一个车,即同一行中不会出现相互攻击的情况;

为什么是(1<<n)-1呢,问了大神解释说,<< 是左移运算符,1<<5就是1左移5位,1<<5就是1左移5位,右边以0填满,1<<5 = 100000,但我们要注意啊,这个10000是二进制,100000-1 = 11111是二进制的减法,11111正好代表最终的状态也就是“0-当前行”所有列都有车的状态;

一开始for循环是一行一行找,找到最后每一列都有车,显然这就是我们要找的最终状态;

介于我一开始对二进制的减法也一脸懵逼,这里也讲讲;

我先举个十进制的例子,81-9=

8 1 由于被减数的个位1<减数的个位9, 7, 11

-    9 所以我们向被减数的十位8借位-     9

———— 左边的竖式可以写成右边这种;—————

72    72

没错,所有进制的减法都是这样,关键就在一个“借位”上,我再举一个二进制的例子:11000011-101101=


是几进制就借几,比如十进制个位要向十位借,个位+10,十位-1;比如二进制10-1,就是低位0向高位1借2,0+2=2,1-1=0,形如02-1=1;


②然后考虑列,每一列只能有一个车,也就是说由当前情况往前找时,只要记录下哪一列有车,然后不去找他就可以了,这一点可以由位运算s & (1<<(i-1))实现;

相信很多刚开始接触的人跟我一样懵逼,这些位运算都是啥啊,根本看不懂啊,所以我们举个例子来解释一下:

先上一个流传很广的图:


这个图是这样的,我们设一个状态 s=01101,就表示第一、三、四列(从低位开始)已经放置了车;

由于我们是一行一行放的,所以在s状态我们应该放第三行了,那第三行我们应该放在哪呢,或者说状态s由哪些状态来的呢?就由上图来解答。

状态s(01101)由三种状态(必然是前两行的状态)来的: 前两行在三、四列放了车,第三行只好放在第一列;(01100)

  前两行在一、四列放了车,第三行只好放在第三列;(01001)

前两行在一、三列放了车,第三行只好放在第四列;(00101)

(蓝绿代表前两行的方案数,如果不懂这个我们后面解释)

无非就是这三种情况,现在我们来考虑怎么来表示状态s由这三种状态来的,还记得我们前面说的“s & (1<<(i-1))”吗,靠他实现;


如果对位运算不熟,先来打打位运算的基础:

and 运算&相同位都为1,则为1;若有一个不为1,则为0
or 运算| 相同位只要一个为1即为1
xor 运算^ 相同位不同则该位为1, 否则该位为0
not 运算~ 把0和1全部取反
shl 运算<< a<<b就是把a转为二进制后左移b位(即在后面填b个0),a shl b的值实际上就是a乘以2的b次方
shr 运算>> a>>b就是把a转为二进制后右移b位(即去掉末尾b个位),相当于a除以2的b次方(取整)


s=01101 由 01100、01001、00101三个状态来的,观察可以发现这个三个状态对应01101分别在一、三、四列少一个1;

所以我们可以设想,只有第i列有车的状态(10000、01000、00100、00010、00001)与s=01101进行某种操作,使我们可以得到这三种状态(即得到此列是否可以放车),这就是 s & (1<<(i-1)) 的作用(&优先级大于&,注意用()括起来);


带大家走一遍,for(i: 0 to 4)

01101 & (1<<0) = 01101 & 1 = 00001

01101 & (1<<1) = 01101 & 10 = 00000

01101 & (1<<2) = 01101 & 100 = 00100 

01101 & (1<<3) = 01101 & 1000 = 01000

01101 & (1<<4) = 01101 & 10000 = 00000

我们可以清楚的看出来了,只有和s=01101有1重合结果才大于0,可以根据这个特性判断此列是否可以放车;

注意了,i对应列,(i-1)对应位置;


③f[s]表示在状态S下的方案数,状态s是二进制,注意边界条件f[0] = 1;


④如果满足 s & (1<<(i-1)) 条件,即第i列可以放车,我们又看到了新的位运算temp = s ^ (1<<(i-1)),我们来走一遍,由 s & (1<<(i-1)) 确定只有一、三、四列满足情况

01101 ^ (1<<0) = 01101 ^ 1 = 01100

01101 ^ (1<<2) = 01101 ^ 100 = 01001

01101 ^ (1<<3) = 01101 ^ 1000 = 00101

没错,由 s & (1<<(i-1)) 确定了我们一直想找的s=01101的三个状态;


⑤f[s] += f[temp];

在②中我不是说蓝绿代表前两行的方案数,这什么意思呢,解释下

推广状态s,那么f[s] = Σf[s ^ (1<<(i-1))],其中i是枚举的状态s中每一个1的位置(从低位到高位)。
边界条件:f[0] = 1。
输出f中所有状态的方案数,按照S中1的个数分类:
0:1                                    0!
1:1 1 1 1 1                        1!
2:2 2 2 2 2 2 2 2 2 2         2!
3:6 6 6 6 6 6 6 6 6 6         3!
4:24 24 24 24 24              4!
5:120                                5!

( 上表来自大神)

我一开始看觉得很奇怪啊,为什么这么多2这么多6,是干嘛呀,其实是这样的:

2: 2 2 2 2 2 2 2 2 2 2         2!为例:“有两个1的情况”中有C52(从五列中任选两列)=5*4/(2*1) = 10种状态(所以有10个2),而每种状态有2!(阶乘)种方案,如果再问为什么,看看上面那个图,蓝绿代表前两行的一种状态的方案数;

那 s=01101怎么看呢,他不过就是“有三个1的情况”的10个状态中的一种,只不过此s状态又由 01100、01001、00101三个子状态得来罢了(都用了“状态”两字,可能容易混淆,仔细看);

所以f[s] += f[temp]很好理解了吧,以s=01101为例,f[13] += f[12] + f[9] + f[5] = 2 + 2 + 2 = 6,即01101状态有六种方案数,看下图理解一下:


代码:

#include <iostream>
#include <stdio.h>
using namespace std;
/*
出现“已停止工作”的情况,编译成功,但无法运行
应该把f[1<<21]挪到主函数外面做全局变量
*/
int f[1<<21];
int main()
{
    int n, temp;
    int s, i;

    scanf("%d", &n);
    f[0] = 1;//边界条件
    for(s = 1; s <= (1<<n)-1; s++)
    {
        f[s] = 0;
        for(i = 1; i <= n; i++)
            if(s & (1<<(i-1)))//排除掉第i行所有不能放置的位置之后的可放位置
            {
                temp = s ^ (1<<(i-1));//可以得到s状态的状态
                f[s] += f[temp];//s状态下的方案数
            }
    }
    printf("%d", f[(1<<n)-1]);
    return 0;
}

反思:

在把f[1<<21]开在主函数内时,出现“已停止工作”的情况,编译成功,但无法运行;

经过探索发现这种情况叫“栈溢出”,局部变量是在栈上分配空间的,栈默认大小一般为1-2M,int f[1<<21]的大小为2^21(2,097,152) ≈ 2.1^1,000,000,2M多一点,编译连接时不会有问题,但运行时由于栈溢出,程序异常终止;而全局变量在静态存储区分配内存,理想情况2-3G吧

而栈的大小与不同的编译器有关,栈默认大小一般为1-2M,一旦出现死循环或者是大量的递归调用,在不断压栈的过程中,容易造成栈容量超过1M而导致溢出。

参考博客:http://blog.csdn.net/luqiang454171826/article/details/6133972

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值