状态压缩动态规划 状压DP
总述
状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式
很多棋盘问题都运用到了状压,同时,状压也很经常和BFS及DP连用,例题里会给出介绍
有了状态,DP就比较容易了
举个例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述一下某一行的某种状态:
设n = 9;
有二进制数 100011011(九位),每一位表示该农田是否被占用,1表示用了,0表示没用,这样一种状态就被我们表示出来了:见下表
列 数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
二进制 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
是否用 | √ | × | × | × | √ | √ | × | √ | √ |
所以我们最多只需要 \(2^{n + 1} - 1\) 的十进制数就好(左边那个数的二进制形式是n个1)
现在我们有了表示状态的方法,但心里也会有些不安:上面用十进制表示二进制的数,枚举了全部的状态,DP起来复杂度岂不是很大?没错,状压其实是一种很暴力的算法,因为他需要遍历每个状态,所以将会出现2^n的情况数量,不过这并不代表这种方法不适用:一些题目可以依照题意,排除不合法的方案,使一行的总方案数大大减少从而减少枚举
位运算
有了状态,我们就需要对状态进行操作或访问
可是问题来了:我们没法对一个十进制下的信息访问其内部存储的二进制信息,怎么办呢?别忘了,操作系统是二进制的,编译器中同样存在一种运算符:位运算 能帮你解决这个问题
(基础,这里不打算自己写了,参照这篇博客,以下内容也复制自qxAi的这篇博客,这里谢谢博主)
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’’符号,xy,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如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)\)
感兴趣的读者可以自行证明。
位运算例题(结合BFS):P2622 关灯问题II
题目描述
现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。
现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。
输入输出格式
输入格式:
前两行两个数,n m
接下来m行,每行n个数,a[i][j]表示第i个开关对第j个灯的效果。
输出格式:
一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出-1
这题需要对状压及位运算有一定的了解:首先要判断某一位的灯是开的还是关的,才能进行修改。
具体解法是:对队首的某一状态,枚举每一个开关灯操作,记录到达这一新状态的步数(也就是老状态 + 1),若是最终答案,输出,若不是,压入队列。
也就是说:我们把初始状态,用每个操作都试一遍,就产生了许多新的状态,再用所有操作一一操作新状态,就又产生了新的新状态,我们逐一尝试,直到有目标状态为止,这可以通过BFS实现。
所以现在知道为什么状压比较暴力了吧。
#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;
}
状压 + DP = 状压DP
同样也是一种挺暴力的DP方式,我们直接看题吧
P1879 [USACO06NOV]玉米田Corn Fields
题目描述
Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can't be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.
Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.
农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
输入输出格式
输入格式:
第一行:两个整数M和N,用空格隔开。
第2到第M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。第i+1行描述了第i行的土地,所有整数均为0或1,是1的话,表示这块土地足够肥沃,0则表示这块土地不适合种草。
输出格式:
一个整数,即牧场分配总方案数除以100,000,000的余数。
其实这题是可以减少状态来达到减少复杂度的,可是我写着题的时候还不会。。。关于减少复杂度可以看下面一篇例题
这题也是用二进制来表示状态,先预处理:枚举一行内(不考虑地图因素)每一种状态,看看是否合法,合法的话就打个标记(其实这里可以减状态数的那时候还不知道QAQ),以便后面操作,最后先处理第一行,枚举下面行的时候再枚举一遍上一行,看看两行放置是否合法,合法就累计上一行计数即可。
#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 = 4096,M = 100000000;
int n,m;
int tmap[19][19];
int map[19];
int dp[19][maxn];
bool can[maxn];
int main(){
n = RD(),m = RD();
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
tmap[i][j] = RD();
map[i] = (map[i] << 1) + tmap[i][j];//利用把地图变为二进制的形式可以快速计算是否合法,要对位运算熟悉掌握
}
}
int maxstate = (1 << m) - 1;//最大状态数
for(int i = 0;i <= maxstate;i++){
if((((i << 1) & i) == 0) & (((i >> 1) & i) == 0)){
can[i] = true;//后面有更优的写法,看下一篇题目
}
}
for(int i = 0;i <= maxstate;i++){
if((can[i]) & ((i & map[1]) == i)){
dp[1][i] = 1;//先预处理出第一行(对于某一行的状态,只受上一行影响)
}
}
for(int i = 2;i <= n;i++){
for(int j = 0;j <= maxstate;j++){
if((can[j]) & (j & map[i]) == j){
for(int k = 0;k <= maxstate;k++){
if((k & j) == 0){
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % M;//dp过程
}
}
}
}
}
long long ans = 0;
for(int i = 0;i <= maxstate;i++){
ans = (ans + dp[n][i]) % M;//答案在最后一行
}
cout<<ans<<endl;
return 0;
}
P1896 [SCOI2005]互不侵犯King
题目描述
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。
输入输出格式
输入格式:
只有一行,包含两个数N,K ( 1 <=N <=9, 0 <= K <= N * N)
输出格式:
所得的方案数
题目十分简短:观察数据范围我们可以知道:这题有庞大的状态量,所以我们就用状压DP解决问题
dp思路:三维,第一维表示行数,第二维表示状态(二进制),第三维表示已经放了的棋子数(说实话做题做多了会有套路的,有数量限制的dp一般都要开一维表示用了的数量)
直接上代码:
#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#define ll long long
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;
}
int len,k;
ll dp[19][1024][110];
int need[1024];//表示每种状态用的棋子数
bool can[1024];
int main(){
len = RD();k = RD();
int maxstate = (1 << len) - 1;
for(int i = 0;i <= maxstate;i++){
int temp = i;
while(temp != 0){
if(temp % 2 == 1)need[i] += 1;//处理所需棋子数
temp /= 2;
}
}
for(int i = 0;i <= maxstate;i++){
if(((i << 1) & i) == 0){
can[i] = true;//处理一行内不冲突的情况
}
}
for(int i = 0;i <= maxstate;i++){
if(can[i] & need[i] <= k)dp[1][i][need[i]] = 1;//预处理第一行
}
for(int i = 2;i <= len;i++){
for(int j = 0;j <= maxstate;j++){
if(can[j]){
for(int s = 0;s <= maxstate;s++){
if(can[s] == false)continue;
if((s & j) != 0)continue;//正面上我啊
if(((s << 1) & j) != 0)continue;//左边上我啊
if(((s >> 1) & j) != 0)continue;//右边上我啊
for(int l = k;l >= need[j];l--){
dp[i][j][l] += dp[i - 1][s][l - need[j]];
}
}
}
}
}
ll ans = 0;
for(int i = 0;i <= maxstate;i++){
ans += dp[len][i][k];
}
cout<<ans<<endl;
return 0;
}
通过题目要求减少状态量
这可以说是状压的一大精华了。一般状压的题目会有大量的状态,枚举所有状态则需要大量的时间,时间承受不了,若和dp结合起来,dp数组开个三四维,空间也吃不消。
所以我们可以通过预处理状态,去掉不合法的状态,减少时空的需要
具体实现和STL中的map很相似:我们用一个序号来映射状态,开一个数组INDEX[ ](这里有坑,小写的index会和cstring库冲突,如果给用的话我绝对用小写魔禁万岁!!!(虽然我站上琴) )INDEX[i]表示第i个合法的状态是什么,然后枚举的时候直接枚举INDEX数组就好了
P2704 [NOI2001]炮兵阵地
题目描述
司令部的将军们打算在NM的网格地图上部署他们的炮兵部队。一个NM的地图由N行M列组成,地图的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
输入输出格式
输入格式:
第一行包含两个由空格分割开的正整数,分别表示N和M;
接下来的N行,每一行含有连续的M个字符(‘P’或者‘H’),中间没有空格。按顺序表示地图中每一行的数据。N≤100;M≤10。
输出格式:
仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。
自己推一下就可以发现,判断此行是否合法需要枚举上一行和上两行的状态,(dp要开三维:第一维表示行数,第二维表示现在枚举的状态,第三维表示上一行的状态,所以dp[i][j][k]表示第i行排成j个状态,且上一行状态是k的最大数量),直接枚举所有状态是肯定会超时的,这时候我们就需要通过题目要求减少状态量了。
减少状态量做法上面已经提过了,其他做法与普通状压类似。
总结一下此类题目的dp方法(玉米田也是这类问题):若某个状态可以对下n行的状态造成影响,那么就要预处理前n行合法的,对于n + 1行及以后,判断某状态是否合法需要往上枚举n行,所以dp数组要开n + 1维,第一维表示行数,第二维表示现在的状态,再往后第n维表示上n - 2行的状态(其实不可能出太多行的,时间指数增长)
这样dp就这样进行:
for(所有状态)
for(所有状态)
...{向上枚举n行}
dp[i][j][k][l]...[n + 1] += dp[i - 1][k][l]...[最上面一行];
//求最大方案数就max()
//意会吧,不怎么讲得清楚
AC代码:
#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#define ll long long
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 = 110;
int lenx,leny;
ll dp[110][maxn][maxn];
bool can[maxn];
bool cann[110][maxn];
int tmap[110][19];
int map[maxn];
int put[maxn];
int INDEX[maxn];
int cnt;
char in;
int main(){
lenx = RD();leny = RD();
for(int i = 1;i <= lenx;i++){
for(int j = 1;j <= leny;j++){
cin>>in;
if(in == 'P')tmap[i][j] = 1;
}
}
for(int i = 1;i <= lenx;i++){
for(int j = 1;j <= leny;j++){
map[i] = (map[i] << 1) + tmap[i][j];//和玉米田类似,处理为二进制地图
}
}
int maxstate = (1 << leny) - 1;
for(int i = 0;i <= maxstate;i++){//枚举一行里的状态
if((((i << 1) & i) == 0) & (((i << 2) & i) == 0)){
INDEX[++cnt] = i;//合法的存在INDEX里,最终cnt表示合法方案数
can[cnt] = true;
int temp = i;
while(temp != 0){
if(temp % 2 == 1){put[cnt] += 1;}
temp /= 2;
}
}
}
for(int i = 1;i <= cnt;i++){//第一行
if(can[i] & ((INDEX[i] & map[1]) == INDEX[i])){
cann[1][i] = true;
dp[1][i][0] = put[i];
}
}
for(int i = 1;i <= cnt;i++){
if(can[i] & ((INDEX[i] & map[2]) == INDEX[i])){//选一个第二行合法的
cann[2][i] = true;//标记一下合法,减少再计算
for(int j = 1;j <= cnt;j++){//在第一行找一个
if(!cann[1][j])continue;//要在第一行合法
if((INDEX[i] & INDEX[j]) == 0){//还要不与第二行冲突
dp[2][i][j] = max(dp[2][i][j],dp[1][j][0] + put[i]);
}
}
}
}
for(int i = 3;i <= lenx;i++){
for(int j = 1;j <= cnt;j++){
if(can[j] & ((INDEX[j] & map[i]) == INDEX[j])){
cann[i][j] = true;
for(int k = 1;k <= cnt;k++){//枚举上两行状态
if(!cann[i - 2][k])continue;
if(!((INDEX[j] & INDEX[k]) == 0))continue;
for(int l = 1;l <= cnt;l++){
if(!cann[i - 1][l])continue;//枚举上一行状态
if(((INDEX[j] & INDEX[l]) != 0) || ((INDEX[k] & INDEX[l]) != 0))continue;
dp[i][j][l] = max(dp[i][j][l],dp[i - 1][l][k] + put[j]);
}
}
}
}
}
ll ans = 0;
for(int i = 1;i <= cnt;i++){
for(int j = 1;j <= cnt;j++){
ans = max(ans,dp[lenx][i][j]);
}
}
cout<<ans<<endl;
return 0;
}
状态压缩动态规划 状压DP的更多相关文章
- 状态压缩动态规划(状压DP)详解
0 引子 不要999,也不要888,只要288,只要288,状压DP带回家.你买不了上当,买不了欺骗.它可以当搜索,也可以卡常数,还可以装B,方式多样,随心搭配,自由多变,一定符合你的口味! 在计算机 ...
- hihoCoder 1044 : 状态压缩·一 状压dp
思路:状态压缩,dp(i, j)表示考虑前i个数且[i-m+1, i]的选择情况为j.如果要选择当前这个数并且,数位1的个数不超过q,则dp[i+1][nex] = max(dp[i+1][nex], ...
- hihocoder #1044 : 状态压缩·一 状压DP
http://hihocoder.com/problemset/problem/1044 可以看出来每一位的选取只与前m位有关,我们把每个位置起始的前m位选取状态看出01序列,就可以作为一个数字来存储 ...
- 动态规划---状压dp
状压dp,就是把动态规划之中的一个个状态用二进制表示,主要运用位运算. 这里有一道例题:蓝书P639猛兽军团1 [SCOI2005]互不侵犯 题目: 题目描述 在N×N的棋盘里面放K个国王,使他们互不 ...
- 【bzoj3195】【 [Jxoi2012]奇怪的道路】另类压缩的状压dp好题
(上不了p站我要死了) 啊啊,其实想清楚了还是挺简单的. Description 小宇从历史书上了解到一个古老的文明.这个文明在各个方面高度发达,交通方面也不例外.考古学家已经知道,这个文明在全盛时期 ...
- 【状压DP】bzoj1087 互不侵犯king
一.题目 Description 在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案.国王能攻击到它上.下.左.右,以及左上.左下.右上.右下八个方向上附近的各一个格子,共8个格子. I ...
- 有关状压DP
[以下内容仅为本人在学习中的所感所想,本人水平有限目前尚处学习阶段,如有错误及不妥之处还请各位大佬指正,请谅解,谢谢!] 引言 动态规划虽然已经是对暴力算法的优化,但在某些比较特别的情况下,可以通过一 ...
- 状压DP学习笔记
有的时候,我们会发现一些问题的状态很难直接用几个数表示,这个时候我们就会用到状压dp啦~~. 状压就是状态压缩,就是讲原本复杂难以描述的状态用一个数或者几个数来表示qwq.状态压缩是一个很常用的技巧, ...
- 【题解】洛谷P2704 [NOI2001] 炮兵阵地(状压DP)
洛谷P2704:https://www.luogu.org/problemnew/show/P2704 思路 这道题一开始以为是什么基于状压的高端算法 没想到只是一道加了一行状态判断的状压DP而已 与 ...
随机推荐
- Linux内核同步机制之(五):Read Write spin lock【转】
一.为何会有rw spin lock? 在有了强大的spin lock之后,为何还会有rw spin lock呢?无他,仅仅是为了增加内核的并发,从而增加性能而已.spin lock严格的限制只有一个 ...
- win8.1 AMD 屏幕亮度无法调整
lenovo z465 AMD处理器. win8.1 pro系统 屏幕亮度无法调整解决办法: 1:当然是先去本地服务里禁用"Sensor Monitoring Service&qu ...
- MySQL 数据表创建及管理
use stuinfo; -- 指定当前数据库 CREATE table if not exists student1( -- 创建数据表student1 sNo ) not NULL, sName ...
- elasticsearch系列八:ES 集群管理(集群规划、集群搭建、集群管理)
一.集群规划 搭建一个集群我们需要考虑如下几个问题: 1. 我们需要多大规模的集群? 2. 集群中的节点角色如何分配? 3. 如何避免脑裂问题? 4. 索引应该设置多少个分片? 5. 分片应该设置几个 ...
- apt-get 详解&&配置阿里源
配置apt-get的下载源 1.复制原文件备份 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak 2.编辑源列表文件 sudo vim / ...
- day14- 面向对象基础(一)
今天开始写关于面向对象的内容,面向对象是编程思想中一大思想,由于日常使用中经常会用到,本次主要是对于我个人认为重点的基础知识进行整理,不会特别详细的书写. 1.面向过程与面向对象的区别 2.类 3.类 ...
- Python是如何实现生成器的原理
python中函数调用的实质原理: python解释器(即python.exe)其实是用C语言编写的, 在执行python代码时,实际上是在用一个叫做Pyeval_EvalFramEx(C语言的函数) ...
- MySQL的运算符及其优先级
+++++++++++++++++++++++++++++++++++++++++++标题:MySQL的常见运算符时间:2019年2月23日内容:MySQL的常见运算符重点:主要讲述MySQL常见运算 ...
- Django create和save方法
Django的模型(Model)的本质是类,并不是一个具体的对象(Object).当你设计好模型后,你就可以对Model进行实例化从而创建一个一个具体的对象.Django对于创建对象提供了2种不同的s ...
- mybatis 使用事务处理
mybatis默认开启事务 以前使用JDBC的时候,如果要开启事务,我们需要调用conn.setAutoCommit(false)方法来关闭自动提交,之后才能进行事务操作,否则每一次对数据库的操作都会 ...