dp.状态压缩

来源:互联网 发布:楼层 噪音 知乎 编辑:程序博客网 时间:2024/05/22 03:07

状态压缩

看到这的人想必你都已经学会基本的dp优化,如斜率优化,单调栈之类的(如果没有,请自行度娘科普0.0)

状态压缩(States Compression,SC)是一种对dp的优化,如一列灯的亮灭就可以通过1和0来表示,那么我们就可以用二进制的数来表示所有的状态,然后利用位运算的一些功能
来实行玄学操作。

为了更好的理解状压dp,首先介绍位运算相关的知识。

1.“&”符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。

2.“|”符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。

3.“^”符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。

4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。

这四种运算在状压dp中有着广泛的应用,常见的应用如下:

1.判断一个数字x二进制下第i位是不是等于1。

方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)

将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。

2.将一个数字x二进制下第i位更改成1。

方法:x = x | ( 1<<(i-1) )

证明方法与1类似,此处不再重复证明。

3.把一个数字二进制下最靠右的第一个1去掉。

方法:x=x&(x-1)

例一

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

这道题有很多做法,比如以组合数学的角度来看,第一个位置有n中摆放方法,第二个位置有(n-1)……那么方案数就是n!,但既然我们是学习状压的角度,那就用状压来思考这道题。

用1表示该列已经放了棋子,f【s】【x】表示第x行状态为s的情况。那么

对于这个状态f【11101】【4】,上一行可能的状态就位这一行状态减去这一行状态减去其中任何一个1,即 01101,10101,11001,11100这四种状态转移而来。

那么f[s][x]=isf[s2i1][x1]

那么这道题用dfs赋值一下即可:

      for( i=1; i< 1<<n; i++)        {              for( t=i; t>0; t -= (t & -t))            {                  f[i] += f[i & ~(t & -t)];  //注意理解              }          }  

例二

给出一个n*m的棋盘(n、m≤80,n*m≤80),要在棋盘上放k(k≤20)个棋子,使得任意两个棋子不相邻。每次试验随机分配一种方案,求第一次出现合法方案时试验的期望次数,答案用既约分数表示。

那么我们联想到状态压缩。这道题状态压缩和例一稍有不同。

首先我们先考虑期望次数怎么求。显然期望次数就是一种方案为合法方案概率的倒数。同时 概率可以表示为

我们先思考怎么设计状态。 显而易见,剩余棋子量(或者已使用)、当前行的状态是必不可少的。行数我们可以用滚动数组在优化空间复杂度,这里我们就设定状态为f[i][j][t]表示第 i 行状态为 j 剩余 t 颗棋子的方案数。

这里我们引入这类棋盘问题比较常用的一种办法,将一行内可行的方案(可通过dfs,也可通过循环位判断)存入一个二维数组中,再用一个二维布尔数组表示两种状态是否能作为相邻行。那如果我们拥有了所有的方案,判断的时候只要满足两行的状态&的值等于0那么就可以作为相邻两行。

那现在的问题就是如何把一行的方案枚举出来。

一个方法是dfs,如果当前位为1,那就dfs下一位为0,如果当前位为0或者起点,那就dfs下一位为1或0,dfs到终点后保存方案即可。

同时我们要用到一行放某个状态时需要话费的棋子数量。这可以在我们dfs的时候顺便处理好。存入数组cot

那么f[i][j][t]=sf[i1][s][tcot[s]](check[i][s]=true)

另一个方案是for循环枚举0~2^n-1,然后转换为二进制,如果没有两个相邻的‘1’就保存方案就行。

然后就可以循环赋值了。

    for(int i=1;i<=m;++i)//第i行         for(int j=1;j<=tot;++j)//当前状态             for(int t=0;t<=k;++t){//已放棋子                 if(t<cot[j])continue;                for(int s=1;s<=tot;++s){                    if(check[j][s])continue;                    f[i][j][t]+=f[i-1][s][t-c[j].sum];                }            }

这道题的坑人之处就是要用期望次数,这必定是一个分数,
而总方案数当数据稍微大一点就会爆longlong 需要我们边计算边约分。

例三(bzoj1087)互不侵犯的king

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。

这道题和上题的套路没什么大区别,让我们求的是方案数。那么状态仍然不变。状态转移方程仍类似。

每一行的方案还是和上题一模一样,唯一的区别就是处理bool数组的时候的方法。

这道题影响的范围比上一题多两个格子而已。那么显而易见的,只要满足两行&值为0且|值中没有两个连续‘1’。

那么大体程序没什么区别。

 for(int x=n;x>=2;--x) //行数  f[状态][剩余][行]   for(int i=1;i<=q;++i)  //状态    for(int r=0;r<=m;++r)  //剩余     {      if(f[i][r][x]==0) continue;      for(int j=1;j<=q;++j) //下一行状态        if(check[i][j])        if(r+sum[i]<=m)        f[j][r+sum[i]][x-1]+=f[i][r][x];     }

做到这题大家也许会发现一个问题。那就是这个 布尔数组当情况麻烦的时候处理就会有些棘手。这个问题在接下来的例五会进行介绍。

例四 炮兵阵地(noi2001 ,poj 1185)

司令部的将军们打算在NM的网格地图上部署他们的炮兵部队。一个NM的地图由N行M列组成,地图的每一格可能是山地(用”H” 表示),也可能是平原(用”P”表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
**
这里写图片描述
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
这道题我们思考预处理的方法是否可行?我们先不考虑那个图片上山地的问题。那么这题与之前例二的区别就是影响的范围扩展到了两行。那么就是说如果上一行的状态确定了,我们这一行的状态仍然无法确定,因为上上行仍然未知。

这道题数据并不大,那我们考虑比较暴力的方法。因为这里的影响为2行,我们不妨多设置一行状态。空间任然不会超。那么f[i][j][x]表示

这道题我们仍然使用前面预处理的方法。

我们同样的预处理出一行中可能存在的方案。不同的是这里影响为2格。那么我们用dfs也许会更方便,如果当前位为0,那么dfs下一位为1或者0,如果当前位为1,那么dfs下面两位都为0。

假如没有障碍预处理好之后我们就可以进行赋值了,我们只要枚举上两行的状态就可以对当前状态进行赋值了。

好,那么我们现在在考虑图片上的障碍问题。我们只要把障碍也当做状态存进一个数组,枚举的时候再顺便判断图片上的状态与枚举的状态是否冲突即可。

    for(int x=3;x<=n;++x)//枚举行     for(int k=1;k<=num;++k)//枚举上上行的状态      {       if( (sum[k]&mapp[x-2])!=sum[k] ) continue;       for(int zh1=1;zh1<=num;++zh1)//枚举上行的状态        {         if(!f[k][zh1]||( !(sum[zh1]&mapp[x-1])==sum[zh1] ) )   continue;          for(int zh2=1;zh2<=num;++zh2)    //枚举挡墙行状态                       if(f[k][zh2]&&f[zh1][zh2] && ( (sum[zh2]&mapp[x])==sum[zh2] ) )             dp[zh1][zh2][x]=max(dp[zh1][zh2][x],dp[k][zh1][x-1]+cot[zh2]);        }      }

有了上面这么一些例题,相信大家对状态压缩也有一定的理解了吧。下面是一道非常经典的问题

例五 广场铺砖

有一个W行H列的广场,需要用1*2小砖铺盖,小砖之间互相不能重叠,问有多少种不同的铺法?(1<=W,H<=11)
这道题可以说是非常经典了,有很多种办法来做。
第一种是玄学公式法,因为这道题的经典程度,很多人来研究,甚至研究出来了公式:
这里写图片描述

第二种是矩阵乘法,在这个状态压缩学习博客里面也不多阐述。

第三种就是我们的状态压缩,状态压缩也有多种方法做,之前说的那种预处理仍然奏效,以1代表铺砖,0代表不铺砖。

不过这里不同的是这里没有完全不符合情况的状态,所有1的位置我们都可以视为铺一块竖的砖,素有0的位置可以视为上一行放了一块竖的砖。要注意的是相邻两行的判断的处理方法。经过思考我们可以发现:

如果两个状态a,b如果~a&b!=a,肯定不行,(此处a、b可以交换位置)。然后如果(~a|b)-(~a&b)中有单独的1:如000100、10100、001110都是不符合的(即所有的1都要两两配对)。(这一段看不懂没事)对于任意一行的状态,我们将它取反,也就是上一行放竖砖的位置我们这一行都不放,上一行不放砖的位置我们这一行放一块竖转。同时如果上行有连续两个位置对本行没有影响,那么我们就可以放一块竖砖。

看到这我们就会发现这个方法有很大的问题,一个就是难以想象相邻两行的处理方法,并且也没有什么普遍性,也就是情况稍微再复杂一点(如本体拓展)就难以入手。这也是我之前说的问题。

那么这里我更推崇的是另一个方法,也就是通过dfs来赋值。

代码如下:(相信连着注释大家都能看懂)

void dp(int i,int s,int s1,int s2,int d){//当前讨论第i行的第d位,第i行初始状态为s,当前状态为s1,i+1状态为s2    if(s1==all){//如第i行已经铺完,则累加        f[i+1][s2]+=f[i][s];        return;    }    if(!(s1&(1<<d))){//第d位为0         dp(i,s,s1|1<<d,s2|(1<<d),d+1);//竖放一块,将第d位变为1,并右移1位         if(d<w-1&&(!(s1&(1<<(d+1)))))//如果第d+1位也为0,表示可以横放一块,则直接搜索d+2位             dp(i,s,s1|(1<<d)|(1<<(d+1)),s2,d+2);    }    else dp(i,s,s1,s2&~(1<<d),d+1);//将s1的第d位不放,把s2的第d位变为0,并右移1位}

同时我们再进一步思考,我们这个dfs是对于dp数组的每一个位置都要进行dfs拓展,我们能否进行一些修改?我们发现,在这种dfs的情况下,因为当前行状态已经确定,所以很多的状态会被直接排除。这样就会有很多的时间浪费(虽然这题的情况下也许并不多)。那么我们其实可以去掉当前行的状态,直接枚举行进行dfs,dfs的时候两个状态同时移动。然后搜到一个边界就赋值。(也许有点抽象,但是笔者的程序丢失了= = 也许会再更新吧)

例题五拓展

(1)给出n*m(1≤n、m≤9)的方格棋盘,用1*2的矩形的骨牌和L形的(2*2的去掉一个角)骨牌不重叠地覆盖,求覆盖满的方案数。
(2)给出n*m(n,m≤10)的方格棋盘,用1*r的长方形骨牌不重叠地覆盖这个棋盘,求覆盖满的方案数。
ps:这两个拓展比较复杂,仍然可以用前面的思想来做。但是这里dfs的细节比较复杂,大家可以来挑战一下,这里入门学习就不怎么讲了,代码可以私戳我。

例题6 noip2017 D2T2宝藏

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。

新开发一条道路的代价是:L×K

L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。

你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。

这道题刚看到也许我们会没有思绪,也有很多人看出来了状态压缩(n很小),但是却无从下手,为什么?很多人都卡在了这个k会不断的变,赋值的时候就不知道怎么处理。

这道题的状态倒是不怎么复杂,直接用f[i]表示开发了i这个状态下的代价。

状态转移方程也很容易可以写出来:
f[x]=minf[1|2n1+ndeep[n]](x中第i位为0)

我们继续思考之前的问题。

我们其实不如暴力的思考,因为n很小,我们不如枚举一开始挖的点(起点),赋起点深度为0,然后dfs,枚举与当前点连着的点,然后更新答案继续dfs即可。

这个算法正确性显然,但是我们思考,这道题有些显然不可能的情况他会继续dfs到底,那么对于n较大的点会有很多浪费的dfs,也会超时。所以肯定还得加优化。

我们考虑,显然对于一种确定了根的状态,枚举的下一个点无法更新dp值的话,继续dfs下去仍然不会更新dp的值。那么我们不妨在没有更新的时候,不进行dfs。

代码如下:

void dfs(int x){     for(int i=1;i<=n;++i)  //i是可以选择的点      {       if( (x&(1<<i-1))!=0)       for(int j=1;j<=n;++j)//j是拓展的点         if( (x&(1<<j-1))==0&&f[i][j]!=168430090 )//168430090是初值         if( (dp[x]+f[i][j]*(deep[i]+1))<dp[x|1<<(j-1)])          {           deep[j]=deep[i]+1;           dp[x|(1<<(j-1))]=dp[x]+f[i][j]*deep[j];           dfs(x|(1<<(j-1)) );          }       }      return; }

ps:这种题目的dp数组千万别开小了,我就认识一个大佬今年noip因为数组开小少了20分。

例题七 noip2016 D2T3愤怒的小鸟

**Kiana最近沉迷于一款神奇的游戏无法自拔。
简单来说,这款游戏是在一个平面上进行的。**

有一架弹弓位于(0,0)处,每次Kiana可以用它向第一象限发射一只红色的小鸟,小鸟们的飞行轨迹均为形如y=ax^2+bxy=ax 2 +bx的曲线,其中a,b是Kiana指定的参数,且必须满足a < 0。

当小鸟落回地面(即x轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有n只绿色的小猪,其中第i只小猪所在的坐标为(xi,yi)。

如果某只小鸟的飞行轨迹经过了(xi,yi),那么第i只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过(xi,yi),那么这只小鸟飞行的全过程就不会对第i只小猪产生任何影响。

例如,若两只小猪分别位于(1,3)和(3,3),Kiana可以选择发射一只飞行轨迹为y=-x^2+4xy=−x 2 +4x的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪

这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个游戏。这些指令将在【输入格式】中详述。

假设这款游戏一共有T个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。由于她不会算,所以希望由你告诉她。

这道题毕竟也算是压轴题了,当初也卡了很多人。

这道题很容易看出是状压,但是具体怎么写呢?

首先我们可以知道选择两只小猪就能确定出这条抛物线了。那么同样我们能杀死的猪也能算出来了。

那么我们就可以开一个数组f[ i ][ j ]表示选择 i、j两只小猪可以杀死的猪的状态。那么我们只要用二元一次方程组的公式求出这个抛物线的解析式,然后枚举其他的猪代入判断能否经过。

要注意的一个坑点是这道题会卡精度,要加个判断精度,要1e-6才能ac。还有一个坑点是解方程的时候除数不能为0,否则也许会出现玄学错误。(不是re)还还有一个是要求a<0,不判会gg。

这道题dp的状态只需要一个猪的状态即可。那么dp[x]就表示击杀x状态猪下的所要用的最少鸟数

那么dp[x]=mini,jf[xg[i][j]](g[i][j]!=0i,jx)

所以我们在一个状态下,只要枚举还没有射的两个猪然后求最小值就行。 代码如下:

void Dp(int x){    for(int i=1;i<=n;++i)     {      if((x|(1<<i-1))!=x) continue;      for(int j=i;j<=n;++j)       {       if((x|(1<<j-1))!=x) continue;        else if(i==j) continue;          else if(g[i][j]==-1) continue;          else           {            int k=(g[i][j]|x)-g[i][j];            if(dp[k]==168430090) Dp(k);             dp[x]=min(dp[x],dp[k]+1);                   }       }     }    if(dp[x]==168430090) dp[x]=cot[x];    return;}
原创粉丝点击