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

题目:

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

思路:

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

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

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


①开个for:1 to (1<

为什么是(1<

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

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

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

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

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

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

72    72

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

[模板]二进制状态压缩DP模板(详解_第1张图片

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


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

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

先上一个流传很广的图:

[模板]二进制状态压缩DP模板(详解_第2张图片

这个图是这样的,我们设一个状态 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< 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状态有六种方案数,看下图理解一下:

[模板]二进制状态压缩DP模板(详解_第3张图片

代码:

#include 
#include 
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<

反思:

在把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

你可能感兴趣的:(动态规划,状态压缩,模板)