【暖*墟】 #动态规划# 状态压缩DP方法总结

版权声明:本文为博主原创文章,未经博主允许不得转载qwq。https://blog.csdn.net/flora715 https://blog.csdn.net/flora715/article/details/82428710

状压DP

通常把一类以一个集合内的元素信息作为状态

状态总数为指数级别的动态规划成为状态压缩DP。

通常具有以下两个特点:

  1. 数据规模的某一维或几维非常小(<25)。
  2. 具备DP的基本性质:最优子结构+无后效性。

具体解题模式:

  • 【找状态】确定每行的M位二进制数中0、1的表示。
  • 【存已知】存入时把初始每行的二进制状态变为一个十进制的数,便于数位操作。
  • 【预处理】结合输入求出每行的所有满足可行性的M位二进制数。
  • 【判边界】一般行列间的关系在起始行并不适用,要特殊处理第一行的状态。
  • 【列方程】逐层枚举每行列的状态,判断行列关系,列状态转移方程。

   目录

        一. 位运算

        二. 状态压缩典型例题

【例题1】洛谷 p1896 互不侵犯

【例题2】洛谷 p2704 炮兵阵地

【例题3】洛谷 p1879 玉米田

【例题4】洛谷 p3092 没有找零

【例题5】旅行商(TSP / NPC问题)

【例题6】洛谷 p3052 摩天大楼的奶牛

【例题7】洛谷 p3622 动物园


一. 位运算

1 位逻辑运算符

      & (按位   “与”)  and        ^  (按位   “异或”)
      |   (按位    “或”)   or    ;     ~  (按位   “取反”)
2 移位运算符

      <<(左移)       >>(右移) 。

3 优先级 【具体可见 这里

位“与”、位“或”和位“异或”都是双目运算符,其结合性都是从左向右的。

优先级 高于逻辑运算符,低于比较运算符,且从高到低依次为&、^、| 。


二. 状态压缩典型例题

【例题1】洛谷 p1896 互不侵犯

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

/*【洛谷p1896】互不侵犯
国王能攻击到它上下左右、以及左上左下右上右下八个方向上一个格子。
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。*/

//方案数DP+状压DP
//f[i][j][s]表示考虑到了第i行,已经放置了j个国王,上一行的放置情况为s。
//枚举这一行的放置情况t。要满足:t是合法的,并且可以与s作相邻行。
//f[i][j+count(t)][t]+=f[i-1][j][s];
//count(t)表示t的二进制表示下1的数量,就是这一行的国王数。

int n,m,cnt,MAX;
ll f[20][400][1000];
int can[1000],num[2000]; //行能到达的状态,每个状态的国王数

int sum1(int x){ //求出每行状态cnt对应的国王数
    int ret=0; while(x) ret+=(x&1),x>>=1;
    return num[cnt]=ret; //并保存为数组,随时取用
}

int main(){
    int l,x,y; ll ans=0;
    scanf("%d%d",&n,&m);
    MAX=(1<<n)-1; //二进制状态总数
    for(int i=0;i<=MAX;i++) //预处理,cnt为状态编号
        if(!(i&(i<<1))) can[++cnt]=i,f[1][sum1(i)][cnt]=1; 
    for(int i=2;i<=n;i++)
        for(int j=1;j<=cnt;j++){
            x=can[j]; //枚举可能的方案
            for(int k=1;k<=cnt;k++){ 
                y=can[k]; //枚举下一行可能的方案
                if((x&y)||(x&(y<<1))||(x&(y>>1))) continue; //不能相邻
                for(int l=0;l<=m;l++)
                    f[i][num[j]+l][j]+=f[i-1][l][k]; 
                //转移:f[i][count(t)+上行总国王数][状态]+=
                //             f[i-1][上行总国王数][上行状态];
            }
        }
    for(int i=1;i<=cnt;i++) ans+=f[n][m][i]; //总方案数=最后一层的答案相加
    printf("%lld",ans);
}


【例题2】洛谷 p2704 炮兵阵地(详情可见 这里

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;

//用每个m位二进制数表示一行的状态:
//每个二进制数的第k位为1表示在第k列上放置部队。

/*【p2704】炮兵阵地
一个N*M的地图,每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示)。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队)。
每个部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
两支炮兵部队之间不能互相攻击,即任何炮兵部队都不在其他部队的攻击范围内。
在整个地图区域内最多能够摆放多少我军的炮兵部队。*/

//预处理出集合S,储存“相邻两个1的距离不小于3”的所有M位二进制数。

int sums[(1<<10)];
//sums[x]表示x的二进制表示中1的个数。(int)

//bool vaild[109][1<<11];
//vaild[i][x]表示满足预处理条件的情况下,x的二进制表示中,
//所有1的位置对应在地图第i行中,都是能使用的平原。(bool)
//↑↑↑【有障碍时】存储行数,判断该行某状态是否成立。
//也可以不使用,用a[109]记录每行的初始可行状态(是二进制数转化为十进制的数)。
//判断的时候,只用在每次循环时特判相对应的S&a[i]。

//int ff[109][1<<11][1<<11]; //2048*2048*109??? ---> 所以要用滚动数组啊qwq
//f[i][j][k]表示第i-1行的状态是j,第i行状态是k时,前i行最多放置的炮兵数。

int f[4][(1<<11)][(1<<11)]={0}; //【滚动数组】对于每一行,只用记录前两行的状态。

int n,m,a[109],anss=0; //a[109]记录每行的初始可行状态(是二进制数转化为十进制的数)

void reads(int &x){
  int fx=1;x=0;char s=getchar();
  while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
  while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
  x*=fx; 
}

int getsum(int S){ //当前状态 S 里面包含几个 1
  int tot=0;
  while(S) {if(S&1) tot++; S>>=1;}
  return tot;
} //【坑点!!】这个一定要写成函数,要不然会TLE QAQ

int main(){
  reads(n); reads(m); char ss;
  
  for(int i=0;i<n;i++) //注意,状压DP中最好都用0开始
    for(int j=0;j<m;j++){ //用a[i]记录每行的初始可行状态
      cin>>ss; a[i]<<=1; //二进制数a[i]每次向前移一位
      a[i]+=(ss=='H'?1:0); //记录障碍的位置
    }

  memset(sums,0,sizeof(sums));

  for(int i=0;i<(1<<m);i++) //对于每个状态,记录每行放置的部队数
    sums[i]=getsum(i); //初始化sum数组

  for(int i=0;i<(1<<m);i++) //【预处理】初始化第一行(行数编号是0)
    if(!(i&a[0] || (i&(i<<1)) || (i&(i<<2)))) //这些条件都不能成立
      f[0][i][0]=sums[i]; //第一行要特殊判定(因为没有1-2=-1行)

  for(int j=0;j<(1<<m);j++) //【预处理】初始化第二行(行数编号是1)
    for(int k=0;k<(1<<m);k++) //j是上一行,k是这一行
      if(!(j&k || j&a[0] || k&a[1] || (j&(j<<1)) || (j&(j<<2)) 
          || (k&(k<<1)) || (k&(k<<2)) ) )
        f[1][j][k]=sums[k]+sums[j]; //第二行要特殊判定(因为没有2-2=0行)

  for(int i=2;i<n;i++) //枚举行数
    for(int j=0;j<(1<<m);j++){ //【预处理】“相邻两个1的距离不小于3”的所有M位二进制数
      if(j&a[i-1] || (j&(j<<1)) || (j&(j<<2))) continue;  //特判
      for(int k=0;k<(1<<m);k++){ //j是上一行,k是这一行
        if(k&a[i] || j&k || (k&(k<<1)) || (k&(k<<2))) continue; //特判
        for(int FL=0;FL<(1<<m);FL++){ //FL是上上一行
          if(FL&j || FL&k || FL&a[i-2] || (FL&(FL<<1)) || (FL&(FL<<2))) continue;   
          f[i%3][j][k]=max(f[i%3][j][k],f[(i-1)%3][FL][j]+sums[k]);
        }
      }
    } 

  for(int j=0;j<(1<<m);j++)
    for(int k=0;k<(1<<m);k++)
      anss=max(anss,f[(n-1)%3][j][k]); //结束状态可以是最后一行(编号n-1)的任何状态
  
  cout<<anss<<endl;
  return 0;
}

【例题3】洛谷 p1879 玉米田

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;

/*【洛谷p1879】玉米田
一块长方形的牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12)。
从中选择一些作为草地,但不会选择两块相邻的土地。
如果不考虑草地的总块数,一共有多少种选择方案?(0也是一种方案)。*/

//【状压DP】
//f[i][j]表示考虑到了第i行,这一行的放置情况为j的方案数。
//枚举上一行的放置情况k。要满足:k是合法的,并且可以与j作相邻行。
//转移方程:f[i][j]+=(sum)f[i-1][k];(方案数DP)

struct node{
    int st[5019],num;
}a[18]; //a[i].st[k]即第i行的状态k

int n,m,f[18][1000]={0};

void get_state(int i,int t){ //第i行的初始状态t
    int nums=0; //↓↓↓预处理出第i行的所有可行状态并储存
    for(int j=0;j<(1<<m);j++)
        if((j&(j<<1))||(j&(j>>1))||(j&t)) continue;
        else a[i].st[++nums]=j; //新的可行状态
    a[i].num=nums; //记录此行的可行状态数量
}

int main(){
    ll ans=0; scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++){
        int t=0,x; //用数t保存一行的状态
        for(int j=1;j<=m;j++) cin>>x, t=(t<<1)+1-x;
        //原题中1表示可以种草,0表示不适合种草,这里将它反过来(便于&)
        get_state(i,t); //第i行中可以用的合法状态
    }

    for(int i=1;i<=a[1].num;i++) f[1][i]=1; //第一行特判
    for(int i=2;i<=n;i++)
        for(int j=1;j<=a[i].num;j++){
            for(int k=1;k<=a[i-1].num;k++){
                if(a[i].st[j]&a[i-1].st[k]) continue;
                f[i][j]+=f[i-1][k];
            }
        }

    for(int j=1;j<=a[n].num;j++) 
        ans=((ll)ans+f[n][j])%100000000;
    cout<<ans<<endl; return 0;
}

【例题4】洛谷 p3092 没有找零

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;

/*【洛谷p3092】没有找零
约翰的钱包里有K个硬币,面值的范围是1..100,000,000。
约翰想按顺序买N个物品,第i个物品需要花费c(i)块钱,
在依次进行的购买N个物品的过程中,约翰可以随时停下来付款,
每次付款只用一个硬币,能支付购买的是从上一次支付后开始到现在的这些所有物品。
如果约翰支付的硬币面值大于所需的费用,他不会得到任何找零。
请计算出在购买完N个物品后,约翰最多剩下多少钱。如果无法完成购买,输出-1。 */

void reads(int &x){
  int f=1;x=0;char s=getchar();
  while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
  while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
  x*=f; 
}

int k,n,a[100001],sum[100001];/*物品数组和前缀和*/
int c[17],pre[17],f[(1<<16)+10];/*在某个状态下购买的物品数*/
int ans=-1,tot;

int main(){
  int k,n; reads(k); reads(n);
  for(int i=1;i<=k;i++){
    reads(c[i]); tot+=c[i];
  }
  for(int i=1;i<=n;i++){
    reads(a[i]); sum[i]=sum[i-1]+a[i];
  }
  pre[1]=1; for(int i=2;i<=k;i++) pre[i]=pre[i-1]<<1; 
  //↑↑↑ 初始化:单独选择每个物品对应的状态

  for(int i=0;i<=(1<<k)-1;i++) //循环每个状态,更新f[i]
    for(int j=1;j<=k;j++) //转移:选择j号硬币
      if(pre[j]&i){ //状态中包含这个硬币
        int cnt=f[i^pre[j]]; //该状态不用第j个硬币时,可以购买的物品数
        cnt=upper_bound(sum+1,sum+n+1,sum[cnt]+c[j])-sum; //判断第j个硬币能带来的状态转移
        //sum[cnt]是原先买物品达到i^b[j]状态花的总钱数
        //用upper_bound寻找第一个大于sum[cnt]+c[j]的物品的编号
        f[i]=max(f[i],cnt-1); //cnt-1是已买的物品数量,用来更新状态f[i]
      }
  
  for(int i=0;i<=(1<<k)-1;i++) //循环每个状态,求最多剩余的钱
    if(f[i]==n){ //枚举买全部的n个物品的所有状态
      int cnt=0; //花费的硬币总价值
      for(int j=1;j<=k;j++) //枚举状态中使用的硬币
        if(i&pre[j]) cnt+=c[j]; //计算花费的硬币总价值   
      ans=max(ans,tot-cnt); //用ans记录最多剩余的钱数
    }
  
  cout<<ans<<endl; return 0; //ans初始化为-1,直接输出
}

【例题5】旅行商(TSP / NPC问题)

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;

/*【洛谷p1523】旅行商(NPC难题简化版)
现在笛卡尔平面上有n(n<=1000)个点,每个点的坐标为(x,y)(为整数)。
任意两点之间相互到达的代价为这两点的欧几里德距离,
现要你编程求出最短bitonic tour(双调旅程)。
即从最左点开始,严格地从左到右直至最右点,然后严格地从右到左直至出发点。 */

void reads(int &x){
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; 
}
 
int dist[50][50],n;
int dp[1<<12][20];

int main(){
    while(scanf("%d",&n)!=EOF&&n!=0){
        n++;

        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                scanf("%d",&dist[i][j]);

        for(int k=0;k<n;k++)
            for(int i=0;i<n;i++)
                for(int j=0;j<n;j++)
                    if(dist[i][j]>dist[i][k]+dist[k][j])
                        dist[i][j]=dist[i][k]+dist[k][j];

        for(int i=0;i<(1<<12);i++)
            for(int j=0;j<20;j++) dp[i][j]=INF;

        dp[0][0]=0;
        for(int i=1;i<(1<<n);i++)
            for(int j=0;j<n;j++)
                if(((1<<j)&i))
                    for(int k=0;k<n;k++)
                        if(!(i-(1<<j)) || ((1<<k)&(i-(1<<j))) )
                            dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+dist[k][j]);

        cout<<dp[(1<<n)-1][0]<<endl;
    }
    return 0;
}

//以上是点少的时候的状压解法,洛谷的题目是不能过的

//旅行商问题,不过要求了只能单向走,所以可以简化。
//双向的问题转化为:假设有两个人一起从西往东走,走过的点不能重复。
//f[i][j]表示第一个人走到i,第二个人走到j的最短路径。
//能够发现,第j+1个点不是被第一个人走,就是被第二个人走。
//方程1:f[i][j+1]=min{f[i][j]+d[j][j+1]} 
//方程2:f[j][j+1]=min{f[i][j]+d[i][j+1]}

struct node{
    double x,y;
}a[1019];

double d[1019][1019],f[1019][1019];

bool cmp(node a,node b){ return a.x<b.x; }

double dist(node a,node b){ 
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); 
}

int main(){
    int n; reads(n);
    for(int i=0;i<n;++i)
        scanf("%lf%lf",&a[i].x,&a[i].y);
    sort(a,a+n,cmp);
    for(int i=0;i<n;++i)
        for(int j=i+1;j<n;++j){ //预处理+初始化
            d[i][j]=dist(a[i],a[j]); f[i][j]=1e30;
        }
    f[0][1]=d[0][1]; //dp起点
    for(int i=0;i<n;++i)
        for(int j=i+1;j<n;++j){ //j+1点不是被第一个人走,就是被第二个人走
            f[i][j+1]=min(f[i][j+1],f[i][j]+d[j][j+1]); //被j走
            f[j][j+1]=min(f[j][j+1],f[i][j]+d[i][j+1]); //被i走
        }
    double ans=1e30;
    for(int j=0;j<n-1;++j) //当一个人走到终点时,枚举另一个人的最后一步
        ans=min(ans,f[j][n-1]+d[j][n-1]);
    printf("%.2lf\n",ans);
    return 0;
}

【例题6】洛谷 p3052 摩天大楼的奶牛

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;

/*【洛谷p3052】摩天大楼的奶牛
给出n个物品,体积分别为w[i],现把其分成若干组,
要求每组总体积<=W,问最小分组。(n<=18)。 */

/*【分析】状压DP
f[i][j]表示已经分了i组,n个物品的选择状态是j时,当前组中的min体积。
如果存在d[i][j]这种方案,我们枚举不属于状态j的物品k。
那么就可以转移到d[i][j&(1<<k)]或者d[i+1][j&(1<<k)]
从前往后搜索,得到第一个存在j=2^n-1的方案即可。 */

void reads(int &x){
  int fx=1;x=0;char s=getchar();
  while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
  while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
  x*=fx; 
}

int f[20][(1<<20)];
int n,m,w[20];

int main(){
  reads(n); reads(m);
  for(int i=0;i<n;i++) reads(w[i]);
  for(int i=0;i<=n;i++) //分组数
    for(int j=0;j<=(1<<n)-1;j++) //每个状态
      f[i][j]=0x3f3f3f3f; //初始化为+∞
  for(int i=0;i<=n;i++) f[1][1<<i]=w[i]; //边界:第1组放任意一个物品
  for(int i=0;i<=n;i++) //组数从0~n
    for(int j=0;j<=(1<<n)-1;j++)
      if(f[i][j]!=0x3f3f3f3f) //如果该状态存在【存在性DP】
        for(int k=0;k<n;k++){ //枚举不属于状态j的物品k
          if((j&(1<<k))!=0) continue; //k属于j,舍去  ↓↓↓判断是否更新f[i][j|(1<<k)]
          if(f[i][j]+w[k]<=m) f[i][j|(1<<k)]=min(f[i][j|(1<<k)],f[i][j]+w[k]);
          else f[i+1][j|1<<k]=w[k]; //多存一组
          //cout<<"i="<<i<<" j="<<j<<" f[i][j]="<<f[i][j]<<endl;
        }
  for(int i=0;i<=n;i++) //组数从小到大
    if(f[i][(1<<n)-1]!=0x3f3f3f3f){ //只要有满足全选的状态的方案
      printf("%d\n",i); break; //输出组数并退出
    }
  return 0;
}

【例题7】洛谷 p3622 动物园

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;

/*【洛谷p3622】动物园
每个小朋友站在大围栏圈的外面,可以看到连续的 5 个围栏。
你得到了所有小朋友喜欢和害怕的动物信息。
当下面两处情况之一发生时,小朋友就会高兴:
1.至少有一个他害怕的动物被移走
2.至少有一个他喜欢的动物没被移走
输出一个数,表示最多可以让多少个小朋友高兴。 */

/*【分析】【状压DP】【环形问题】
考虑到不同的小朋友看见的围栏范围可能相同,要预处理num[pos][s]:
表示从第pos个开始的五个围栏移走状态为s(全满则为15)时,满意的人数。 
f[i][s]表示枚举到第i个围栏且[i,i+5]的围栏移走状态为s时的最多满意人数。
则f[i][s]可以由第i-1个围栏移走和不移走两种状态转移得来:
f[i][s]=max⁡(f[i−1][(s&15)<<1],f[i−1][(s&15)<<1∣1])+num[i][s];
注意:在dp之前先枚举前五个的状态state。避免环形问题。
那么此时必须满足s=state才是有效状态,更新答案。 */

void reads(int &x){
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; 
}
const int maxn=50010;
int n,m,ans,f[maxn][40],num[maxn][40];

void input(){
    int E,a,b,l,d,t;
    reads(n); reads(m);
    for(int i=1;i<=m;i++){
        reads(E);reads(a);reads(b);
        l=d=0; //分别记录不喜欢的和喜欢的对应的状态
        for(int j=1;j<=a;j++) //不喜欢的,相应位置上为1
        { reads(t); t=(t-E+n)%n; l|=1<<t; }
        for(int j=1;j<=b;j++) //喜欢的,相应位置上为1
        { reads(t); t=(t-E+n)%n; d|=1<<t; }
        for(int j=0;j<32;j++)
            if((j&l)||(~j&d)) //j为要选择移走的状态
                num[E][j]++; //从E开始的5个围栏移走状态为s时,满意的人数
    }
}

int main(){
    input();
    for(int i=0;i<32;i++){
        memset(f[0],128,sizeof(f[0])); //极小值
        f[0][i]=0; //起点
        for(int j=1;j<=n;j++) //枚举围栏数
            for(int s=0;s<32;s++) //移走状态,从上一位置移走和不移走两种状态转移而来
                f[j][s]=max(f[j-1][(s&15)<<1],f[j-1][(s&15)<<1|1])+num[j][s];
        if(ans<f[n][i]) ans=f[n][i]; //更新ans
    }
    printf("%d\n",ans);
    return 0;
}

                                                          ——时间划过风的轨迹,那个少年,还在等你。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/82428710