通过分析NOIP2011-2018年提高组的试题我们就会发现,考察最多的考点前三名就是模拟,动态规划和贪心算法。今天我们来介绍一下常见的状态压缩dp。 我们都知道在计算机当中,所有数据都是以二进制的形式存储的。一般一个int整形是4个字节,也就是32位bit,我们通过这32位bit上0和1的组合可以表示多大21亿个不同的数。如果我们把这32位bit看成是一个集合,那么每一个数都应该对应集合的一种状态,并且每个数的状态都是不同的。我们用二进制的0和1表示一个二元集合的状态。可以简单认为某个物品存在或者不存在的状态。由于二进制的0和1可以转化成一个int整数,也就是说我们用整数代表了一个集合的状态。这样一来,我们可以用整数的加减计算来代表集合状态的变化。 状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式 很多棋盘问题都运用到了状压,同时,状压也很经常和BFS及DP连用,例题里会给出介绍 有了状态,DP就比较容易了 通过题目要求减少状态量这可以说是状压的一大精华了。一般状压的题目会有大量的状态,枚举所有状态则需要大量的时间,时间承受不了,若和dp结合起来,dp数组开个三四维,空间也吃不消。 所以我们可以通过预处理状态,去掉不合法的状态,减少时空的需要 具体实现和STL中的map很相似:我们用一个序号来映射状态,开一个数组INDEX[ ](这里有坑,小写的index会和cstring库冲突 )INDEX[i]表示第i个合法的状态是什么,然后枚举的时候直接枚举INDEX数组就好了
为了更好的理解状压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) 感兴趣的读者可以自行证明。 一支炮兵部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。 输入文件(cannon.in) 输出文件(cannon.out)
位运算例题(结合BFS):P2622 关灯问题II(来自:Tony_Double_Sky )题目描述 现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。 现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。 输入输出格式 输入格式: 接下来m行,每行n个数,a[i][j]表示第i个开关对第j个灯的效果。 输出格式: 这题需要对状压及位运算有一定的了解:首先要判断某一位的灯是开的还是关的,才能进行修改。 具体解法是:对队首的某一状态,枚举每一个开关灯操作,记录到达这一新状态的步数(也就是老状态 + 1),若是最终答案,输出,若不是,压入队列。 所以现在知道为什么状压比较暴力了吧。 # code #include<iostream> #include<vector> #include<queue> #include<cstdio> #include<cstring> using namespace std; int RD(){ int out = 0,flag = 1;char c = getchar(); while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();} while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();} return flag * out; } const int maxn = 2048; int num,m,numd; struct Node{ int dp,step; }; int vis[maxn]; int map[maxn][maxn]; void BFS(int n){ queue<Node>Q; Node fir;fir.step = 0,fir.dp = n;//初始状态入队 Q.push(fir); while(!Q.empty()){//BFS Node u = Q.front(); Q.pop(); int pre = u.dp; for(int i = 1;i <= m;i++){//枚举每个操作 int now = pre; for(int j = 1;j <= num;j++){ if(map[i][j] == 1){ if( (1 << (j - 1)) & now){ now = now ^ (1 << (j - 1));//对状态进行操作 } } else if(map[i][j] == -1){ now = ( (1 << (j - 1) ) | now);//对状态进行操作 } } fir.dp = now,fir.step = u.step + 1;//记录步数 if(vis[now] == true){ continue; } if(fir.dp == 0){//达到目标状态 vis[0] = true;//相当于一个标记flag cout<<fir.step<<endl;//输出 return ;//退出函数 } Q.push(fir);//新状态入队 vis[now] = true;//表示这个状态操作过了(以后在有这个状态就不用试了) } } } int main(){ num = RD();m = RD(); int n = (1 << (num)) - 1; for(int i = 1;i <= m;i++){ for(int j = 1;j <= num;j++){ map[i][j] = RD(); } } BFS(n); if(vis[0] == false) cout<<-1<<endl; return 0; } |
|