C++ 算法篇 动态规划----状态压缩

一、总述

状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式。

很多棋盘问题都运用到了状压,同时,状压也很经常和BFS及DP连用。

状压dp其实就是将状态压缩成2进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有1或0 ,是另一类非常典型的动态规划

举个例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述一下某一行的某种状态:

设n = 9;

有二进制数 100011011(九位),每一位表示该农田是否被占用,1表示用了,0表示没用,这样一种状态就被我们表示出来了:见下表

技术图片

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

  1. ’&’符号,x&y,会将两个十进制数在二进制下进行与运算(都1为1,其余为0) 然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
  2. ’|’符号,x|y,会将两个十进制数在二进制下进行或运算(都0为0,其余为1) 然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
  3. ’^’符号,x^y,会将两个十进制数在二进制下进行异或运算(不同为1,其余 为0)然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
  4. ’~’符号,~x,按位取反。例如~101=010。
  5. ’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。 ’>>’符号,是右移操作,x>>1相当于给x/2,去掉x二进制下的最右一位

1.判断一个数字x二进制下第i位是不是等于1。(最低第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.将一个数字x二进制下第i位更改成0。   方法:x=x&~(1<<(i−1))

4.把一个数字二进制下最靠右的第一个1去掉。   方法:x=x&(x−1)

二、典型例题

【例题1】[SCOI2005]互不侵犯

题目描述

在 n×n(1<=n<=10) 的棋盘上放 k(0<=k<n×n)个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。

输入格式

输入有多组方案,每组数据只有一行,包含两个整数 n 和 k。

输出格式

每组数据一行为方案总数,若不能够放置则输出 0。

输入样例

3 2

4 4

样例输出

16

79

算法分析:

一道很好的状压 dp 练手题;状压,就是把一种状态压缩成一个数,达到节省空间的目的;如下图:

由于每个格子只有两种状态:放国王 和 不放国王 ,用 1 表示放国王,0 表示不放国王;那么把每个格子的数连起来就是(101001)2 ,这个数在十进制下是:(101001)2​ =(41)10​

所以状压 dp 的套路就是不断的去枚举表示状态的数,去转移即可。

状态设置:

按照状压 dp 的套路,我们设状态:dp[i][j][S] 表示我们已经选到了第 i 行,第 i 行的状态为 S,用了 j 个国王的方案数;

转移方程:

首先看一下国王的攻击范围(以其为中心的九宫格):红色代表国王位置,蓝色代表它的攻击范围:

思考:如果我们第 i−1 行的第 j 列放好国王之后,那么对第 i 行的影响是什么呢?也就是国王在第 i 行上的攻击范围内的格子不能再放国王了:

也就是说,如果第i−1 行的状态S1​(表示状态的二进制数)的第 j 位是 1(放国王)的话,第 i 行的状态 S2​ 的第 j−1,j,j+1 位一定为 0(不能再放国王了),否则就是不合法的状态;

那怎么表示这一条件呢?回归到二进制上来看,假如我们现在正在决定第 i 行的状态:

显然这个红色的 1 是不合法的,但是怎么知道它是不合法的呢?

这里就要运用巧妙的位运算了,此时S1​ & S2 ≠ 0​

那如果是右下方有一个国王呢?

我们可以按照刚刚那样的方法用位运算:

1. 先将 S2​ 向左移一位:

2. 然后这样两个 1 就冲齐了,此时两种状态的 & 运算是不为 0 的;

(S2​<<1) & S1​≠ 0;

在左下方的情况同理:

1. 我们先将 S2​ 向右移一位:

2. 然后发现此时两状态的 & 运算不为 0;

(S2​>>1) & S1≠ 0​;

所以我们就得出了表示这三个位置的方法,那么 S2​ 必须满足什么条件才可能由 S1​ 转移过去呢?

条件:(S2​ & S1​ ==0) && ( (S2​<<1) & S1​==0) && ( (S2​>>1) & S1​==0)

我们发现这样写有点长,还可以这样写:(S2​∣(S2​>>1)∣(S2​<<1)) &S1​==0

当然,我们处理完行间的限制后,接下来就要处理行内的限制了;

一个国王的左右格子内不能再放国王了,这就是行内的限制!

运用上面的第二,三种情况的做法,问题就迎刃而解了:

1. 判断左边有没有国王:

我们将 S2​ 向右移一位,再与原来的 S2​ 做 & 运算:

观察到 1 左边的数右移后跑到了 1 的位置上,所以我们再做一次 & 运算即可:若结果为 0 ,说明那一位是 0(合法);否则为 1(不合法);

2. 判断右边有没有国王:

同理,将 S2​ 左移一位再做 & 运算:

把上述过程转化成代码:

((S2​<<1) & S2​==0) && ((S2​>>1) & S2​==0)

更短的写法:

( (S2​<<1) ∣ (S2​>>1) ) & S2​==0

所以我们下一步的状态 S2​ 要同时满足这两个条件才可以

因为第二个条件只与状态的情况有关,所以我们可以预处理这个东西,dp 的时候将所有满足第二条性质的状态拿出来看看是否再满足第一条性质就好了;考虑第二维怎么转移的:

显然对于第 i−1 行来说,第 i 行多放的国王数就是第 i 行的状态上 1 的个数(在二进制下);

综上,则有 dp[i][j][S2​]+=dp[i−1][j−cnt[S2​]][S1​] ,(其中 S2​ 要满足上述两个条件,cnt[S2​] 表示S2​ 在二进制下有几个 1)

发现转移方程类似于背包 dpdp 时的方程

边界设置

刚开始一个偌大的棋盘,我们啥也不知道,只知道它的第 0 行不能放旗子;

即 dp [ 0 ][ 0 ][ 0 ] =1;

答案输出

最后的情况,肯定是我们已经考虑完 n 行的排放情况,并且在其中一定是只放了 k 个国王了,但对于第 n 行具体摆成什么稀奇古怪的亚子,我们也不知道,所以我们去枚举第 n 行所有可能的情况,统计答案就好了;

细节提示

最后注意开 long long

#include<bits/stdc++.h>
using namespace std;
int n,k,num;
long long cnt[2000],ok[2000];   //cnt[i]:第i种状态的二进制中有几个1   ok[i]:第i个行内不相矛盾(满足条件2:左右国王不相邻)的状态是多少 
long long dp[10][100][2000];    //dp[i][j][s]:我们已经放了i行,用了j个国王,第i行的状态为s的方案数
int main() 
{   cin>>n>>k;                    //n*n的棋盘上放k个国王 
    for(int s=0;s<(1<<n);s++)     //枚举所有可能状态 
    {   int tot=0,s1=s;           //tot:二进制下有多少个1; 
        while(s1)                 //一位一位枚举,直至为0(做法类似于快速幂那样) 
        {   if(s1&1) tot++;       //如果最后一位是1,tot++ 
            s1>>=1;               //右移看下一位 
        }
        cnt[s]=tot;               //预处理这个二进制数有多少个1
        if((((s<<1)|(s>>1))&s)==0) ok[++num]=s; //如果这一状态向左向右都没有重复的话,说明左右不相邻,合法 
    }
    dp[0][0][0]=1;                //第0行一个也不放的方案数
    for(int i=1;i<=n;i++)         //枚举我们已经放到了第几行 
    {   for(int l=1;l<=num;l++)   //枚举第i行的状态,这里我们直接枚举所有满足条件2的状态,算是个优化吧 
        {   int s1=ok[l];             
            for(int r=1;r<=num;r++) //枚举上一行的状态 
            {   int s2=ok[r];
                if(((s2|(s2<<1)|(s2>>1))&s1)==0)  //如果上下,左下右上,左上右下方向都不相邻,合法 
                {  for(int j=0;j<=k;j++) //枚举国王个数 
                      if(j-cnt[s1]>=0)  
                        dp[i][j][s1]+=dp[i-1][j-cnt[s1]][s2];  //状态转移方程       
                }
            }
        }
    }
    long long ans=0;
    for(int i=1;i<=num;i++) ans+=dp[n][k][ok[i]];  //枚举第n行所有可能的情况,统计答案 
    printf("%lld\n",ans);    //输出 
    return 0;
}

 

【例题2】牧场的安排(P1879 [USACO06NOV]玉米田Corn Fields)

Farmer John 新买了一块长方形的牧场,这块牧场被划分成 M列 N 行  (1≤M≤12;1≤N≤12),每一格都是一块正方形的土地。FJ 打算在牧场上的某几格土地里种上美味的草,供他的奶牛们享用。遗憾的是,有些土地相当的贫瘠,不能用来放牧。并且,奶牛们喜欢独占一块草地,于是 FJ 不会选择两块相邻的土地,即:没有哪两块草地有公共边。当然,FJ 还没有决定在哪些土地上种草。 作为一个好奇的农场主,FJ 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择。当然,把新的牧场荒废,不在任何土地上种草,也算一种方案。请你帮 FJ 算一下这个总方案数。

输入格式

第 1行:两个正整数 M 和 N,用空格隔开;第 2到 M+1行:每行包含 N 个用空格隔开的整数,描述了每块土地的状态。输入的第 i+1行描述了第 i行的土地。所有整数均为 0 或 1,1 表示这块土地足够肥沃,0 则表示这块地上不适合种草。

输出格式

第 1 行:输出一个整数,即牧场分配总方案数除以 10^8的余数。

样例输入

2 3

1 1 1

0 1 0

样例输出

9

 题目大意

给N*M的棋盘,每个格子不是0就是1,1代表可以种草,否则不能。相邻两个格子不能同时种草,求种草的方案总数。

思路

状态压缩类动态规划,状压dp一般会有明显的数据范围特征,即n,m一般都在20以内。可将每一排的N个看成一个N位二进制,先预处理出每一行可以运行的状态,这样可以去掉很多无效状态(如110),然后DP处理,枚举当前有效状态和上一行有效状态的关系。

f[i][j] 表示第i行在状态j的时候的方案数,其中j我们用一个二进制数来表示。

转移的时候只要判断与当前行和上一行是否冲突即可,如果不冲突,分f[i][j]=∑f[i−1][k]其中k为不冲突的状态。Ans=∑1≤i≤numf[n][i] 就是最后的答案(num为状态总数)。

初始条件:f[1][i]=1 (1<=i<=a[1].num)

 

  • 20
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值