• 状压DP学习笔记


    有的时候,我们会发现一些问题的状态很难直接用几个数表示,这个时候我们就会用到状压dp啦~~。

    状压就是状态压缩,就是讲原本复杂难以描述的状态用一个数或者几个数来表示qwq。状态压缩是一个很常用的技巧,把它运用到动态规划中有时候可以方便节省空间和时间,精简状态,方便状态转移。

    找状态依然是状压dp的核心qwq。

    多数状压dp都是将一个n维,每一维为0或1的状态压缩为一个2n的二进制数,用这个数二进制表示下每一位的值来表示这个状态qwq。(比如说储存一行:011110,每一个数字都表示其对应位置的合法性)

    学好状压DP首先是需要熟练地掌握位运算的qwq,所以这里先介绍几个比较常用的位运算操作:

    • 0&0=0; 0&1=0; 1&1=1;(与运算): &
    • 0$|(0=0; 0)|(1=1; 1)|$1=1; (或运算):  (|)
    • 0^0=0; 0^1=1; 11=0;(异或运算): 
    • 去掉最后一位(相当于/2):   (x>>1)
    • 在最后一位加零(相当于$*$2):  (x<<1)
    • 把最后一位变成1:  (x|1)
    • 把最后一位变成0:   (x|1-1)
    • 最后一位取反:   (x)^(1)

    除此之外还有几个常用的:

    • 判断一个数字x二进制下第i位是不是等于1:(也可以理解成判断第i个点在不在集合中)
      if(((1<<(i-1))&x)>0)
      (就是将1左移i-1位之后,你会发现就只有第i位上面有1,其他位都是0,这时候再和x与,如果是1就是符合条件,0就是不符合)

    • 将一个数字x二进制下第i位更改成1:
      x=x|(1<<(i-1))
      {大概和上面是一样的,就是执行|操作)

    • 把一个数字二进制下最靠右的第一个1去掉:
      x=x&(x-1)

    • 枚举s的子集:
      for(int i=s;i;i=(i-1)&s){}

    • 若s是u的子集,那么s对于u的补集v:
      v=s^u

    在这里先贴上来自 OI Wiki 的状压DP常用格式:

    int maxn=1<<n; //规定状态的上界
    for (int i=0;i<maxn;i++){
        if (i&(i<<1)) continue;//如果i情况不成立就忽略
        Type[++top]=i;//记录情况i到Type数组中
    }
    for (int i=1;i<=top;i++){
        if (fit(situation[1],Type[i]))
            dp[1][Type[i]]=1;//初始化第一层
    }
    for (int i=2;i<=层数(dp上界);i++){
        for (int l=1;l<=top;l++)//穷举本层情况
            for (int j=1;j<=top;j++)//穷举上一层情况(上一层对本层有影响时)
                if (situation[i],Type[l]和Type[j]符合题意)
                    dp[i][l]=dp[i][l]+dp[i-1][j];//改变当前层(i)的状态(l)的方案种数
    }
    for (int i=1;i<=top;i++) ans+=dp[上界][Type[i]];
    

    通过上述代码我们可以了解到基本的状压DP思想和基础操作。
    一般来讲,状压DP处理的数据范围很小,但是一般比爆搜的范围稍微大一些,就是在两位数的范围内。
    下面来以几道典型例题来进一步理解状压DP:

    [USACO06NOV]玉米田Corn Fields

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define mod 100000000
    using namespace std;
    int n,m,ans;
    int f[13][5000],cnt[13],a[13][13],done[5000],maxx;
    //valid是该地方是否合法,maxx是最大状态数(上限)
    int main(){
        scanf("%d%d",&m,&n);
        maxx=1<<n;
        
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++)
                scanf("%d",&a[i][j]);
        //读取每块土地的状态       
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++)
                cnt[i]=(cnt[i]<<1)+a[i][j];
        //cnt[i]表示的是到第i行的状态
        //因为我们是用二进制储存的状态(状压DP思想)
        //所以要把该行的状态整理起来
        
        for(int i=0;i<maxx;i++)
             if(!(i&(i<<1)))
                state[i]=1;
            //我们现在要处理每个状态的合法性
            //因为题目中提到没有两块相邻的土地,所以就是两个1不能相邻,我们通过左移一位并与运算来判断
        f[0][0]=1;
        for(int i=1;i<=m;i++)
            for(int j=0;j<maxx;j++)
                if(done[j]&&((j&cnt[i])==j))
                //这里要判断土地是否贫瘠,还是用到位运算的原理
                    for(int k=0;k<maxx;k++)
                    //寻找上一行的合法情况
                        if((k&j)==0)
                        //该行有选择土地的同列上一行不能再选择,所以直接与就可以判断合法情况了
                            f[i][j]=(f[i][j]+f[i-1][k])%mod;
                            //如果合法要相加答案
        for(int i=0;i<maxx;i++)
            ans=(ans+f[m][i])%mod;
        printf("%d
    ",ans);
        return 0;
    }
    

    上面我们是把所有的情况进行了枚举,然后判断如果不合法的话就跳过,如果合法才进行累计计算qwq,这样的话常数不是特别优秀。

    下面这道题进行了常数优化。

    通过预处理,可以处理出合法情况,然后将其储存起来。

    互不侵犯

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #define MAXN 2000
    using namespace std;
    int n,m,cnt,MAX;
    long long sum[20][MAXN][100];
    int valid[MAXN],num[MAXN];
    int main()
    {
        long long ans=0; 
        scanf("%d%d",&n,&m);
        MAX=(1<<n)-1;
        for(int i=0;i<=MAX;i++) 
    		if (!(i&(i<<1))) 
    		{
    			valid[++cnt]=i;
    			int cur_ans=0,x=i;
        		while(x) 
    			{
    				cur_ans+=(x&1);
    				x>>=1;
    			}
        		num[cnt]=cur_ans;
    			sum[1][cnt][num[cnt]]=1;
    		}
        for (int i=2;i<=n;i++)
            for (int j=1;j<=cnt;j++)
                for (int k=1;k<=cnt;k++)
                {
                    if ((valid[j]&valid[k])||(valid[j]&(valid[k]<<1))||(valid[j]&(valid[k]>>1))) continue;
                    for (int l=0;l<=m;l++) sum[i][j][num[j]+l]+=sum[i-1][k][l]; 
                }
        for(int i=1;i<=cnt;i++) ans+=sum[n][i][m];
        printf("%lld
    ",ans);
        
    }
    

    宝藏

    题解:https://www.cnblogs.com/fengxunling/p/9777606.html

  • 相关阅读:
    深入学习Motan系列(二)——服务发布
    深入学习Motan系列(一)——入门及知识zookeeper储备
    Guava Cache 总结
    Jetty学习(一)
    群发百万邮件
    Project ACRN documentation
    Storage Performance Development Kit
    DPDK Test Plans
    golang 判断前缀后缀、包含关系
    golang multiconfig 示例
  • 原文地址:https://www.cnblogs.com/fengxunling/p/9649789.html
Copyright © 2020-2023  润新知