一文带你入门并吃透状态压缩DP

【本文比较适合有一定动态规划和位运算基础的童鞋阅读】

首先先讲讲什么是状态压缩

状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1

我们都知道二进制可以用来枚举子集,例如某个问题有8种情况,那么我们可以一个循环,从0到2^3-1,将所有情况枚举出来,这里拓展一个位运算的技巧(i>>j&1): 用来求十进制下的数i第j位是否为1,我们规定如果当前位为1就说明这一位应当被选中

动态规划的问题

状态压缩DP常见问题大概可以分为两类

1.棋盘式(基于连通性)DP

2.集合式DP

个人总结的状态压缩dp三部曲:

  1. 考虑如何状态压缩

  2. 确定状态表示和状态转移方
  3. 根据实际问题确定筛选条件 最关键一步!!!合理情况的筛选一定要考虑完全

解释一下第三步的原因,我们状态压缩是对所有情况进行枚举,而实际的题目中会有限制,我们根据题目要求,先对所有情况进行预处理,用一个bool数组,将不合法的数值标记为false即可

例题引入: P1879 [USACO06NOV]Corn Fields G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意:每一个是1的都可以种植草地,每个草地的上下左右不能有草地,求方案数

第一步:分析如何状态压缩

规定1表示种植草地,0表示不种值,所以每一行的状态由[0,2^m-1]表示,m表示列数 

第二步:状态表示和状态转移方程

初状态 f[0][0] = 1(默认一行也不摆也是一种方案)

f[i][j]表示摆完了前i行,第i行的状态是j的方案数

第i行是在前i-1的基础上加上的所以状态转移方程很容易得出:f[i][j] += f[i-1][k]; 

第三步: 根据实际情况筛选合理情况

本题中要求若中间确定种植草地,则上下左右不允许出现草地,即不允许出现1

我们可以通过位运算来确定是否合法

1.行内合法 位运算技巧: !(i & i >> 1)为真,则为合法情况

技巧讲解: i>>1后i-1位来到了i位,i位来到了i+1位的情况,如果i&i>>1等于0不就是说明i-1和i,i和i+1都是不相等的状态吗,也就是我们当前第i位是1,那么i-1位和i+1位都是0,就保证了左右没有草地

2.行间是否合法 

我们假设第i-1行的状态是a,第i行的状态是b,那么需要满足的条件是!(a&b),就是两行间不能有相邻的草地,

3.枚举的状态数是否符合实际的条件

本题的实际条件就是不能超过本行的草地数量

例如一个3行5列的土地,当前行只有两列是可以种植草地的,所以当前土地的草地数为g[i],那么应该满足a&g[i] = a,就是a应该是g[i]的一个子集,相当于a∩g[i] = a

#include<iostream>
using namespace std;
const int mod = 1e9;
int g[14];
int s[1<<14];//s数组用来存储状态,所以列数[1,12],所以可以开到2^14
int f[14][1<<14];f数组是动态规划的数组,第一维表示行数,第二维表示当前状态
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            int x;
            cin>>x;
            g[i] = (g[i] << 1) + x;//位运算
        }
    }
    int cnt = 0;
    for(int i = 0;i < (1<<m);i++)
    {
        if(!(i&i>>1)) s[cnt++] = i;//预处理,将符合条件的状态存下来
    }
    f[0][0] = 1;//初始
    for(int i = 1;i <= n;i++)
    {
        for(int a = 0;a < cnt;a++)
        {
            for(int b = 0;b < cnt;b++)
            {
                if((g[i]&s[a]) == s[a] && !(s[a] &s[b]))//过滤行间和行内要满足的条件
                  f[i][a] = (f[i-1][b] + f[i][a]) % mod; //状态转移
            }
        }
    }
    long long ans ;
    for(int i = 0;i < cnt;i++) //遍历所有状态,将第n行的合理状态加起来
    {
        ans =(ans + f[n][i]) % mod;
       
    }
    cout<<ans;
}

最后遍历所有的合理状态也可以根据状态转移方程,第n+1的方案是由第n行转移过来的,

所以f[n+1][0] = (f[n][a1] + f[n][a2] + .....f[n][acnt]),也就是相当于遍历了一遍,所以我们也可以这样写

for(int i = 1;i <= n+1;i++)//循环到第n+1行
    {
        for(int a = 0;a < cnt;a++)
        {
            for(int b = 0;b < cnt;b++)
            {
                if((g[i]&s[a]) == s[a] && !(s[a] &s[b]))
                  f[i][a] = (f[i-1][b] + f[i][a]) % mod; 
            }
        }
    }
    cout<<f[n+1][0];

例题引入 :U204941 蒙德里安的梦想 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

第一步 首先先给出一个明显的结论:横放格子的方案数等于总方案数

当竖放的格子确定好位置后,那么横放的格子也就确定了,因为摆放的方式只有横放或者竖放,当竖放确定后,剩余的坑位也就确定了形状,显然这题就是讨论哪个格子是怎么放置的,我们看它给出的数据范围是[1,11],我们可以考虑按列摆放,那么我们枚举二进制,根据题目实际分析应该是[0,2^m-1],行数是n,那么就有n位。

我们可以规定若某行是1,表示横,某行为0,表示竖放。

第二步

 用f[i][j]记录第i列第j个状态。j状态位等于1表示上一列有横放格子,本列有格子捅出来。转移方程很简单,本列的每一个状态都由上列所有“合法”状态转移过来f[i][j] += f[i - 1][k]

初始化条件:f[0][0] = 1第0列只能是状态0,无任何格子捅出来。

终止状态:f[m+1][0]。第m + 1列不能有东西捅出来。

第三步

分析实际情况:

那么在这个题中我们应该可以想到第i列和第i-1不能为同时为1,因为我们规定有1表示当前的列是横放格子的第二个格子,如果i列为1就说明i-1列应该是竖放格子的第一个

所以条件为!(j&k)为真

第二个条件:设当前列的状态是state,,上一列状态为last,我们知道是把行作为状态数,说明这一行有格子,那么我们没有格子的地方最后是要填满竖放格子的,竖放格子的高为2,也就是我们两个放格子的行之间的行数应该是偶数个,如果是奇数个不能放下竖放格子,肯定会有空格,所以state|last 应该是不能出现连续奇数个0,这个条件我们可以通过预处理,将满足条件的状态存下来,下面通过图来解释

绿色为上一列的格子,蓝色表示这一列的格子,last |state 就是两个状态只要出现1,最后的结果的这一行就为1,因为两个状态的交集在第i列,无论是i-1列的状态出现1还是i列出现1,都有第i列肯定有格子,只有两个状态的当前行都为0,第i列的格子才没有被填,所以我们要找的就是 state|last中是否有连续的0

 

#include<iostream>
#include<cstring>
using namespace std;
const int N = 13,M = 1<<N;
long long f[N][M];
int cnt;
bool st[M];
int main()
{
    
       int n,m;
       while(cin>>n>>m,n||m)
       {
           memset(f,0,sizeof f);
           for(int i = 0;i < (1<<n);i++)
           {
               cnt = 0;
               st[i] = true;
               for(int j = 0;j < n;j++)
               {
                   if(i >> j & 1)
                   {
                      if(cnt & 1)
                      {
                          st[i] = false;
                          cnt = 0;
                      }
                     
                   }
                   else cnt++;
               }
                if(cnt & 1) st[i] = false;
           }
           f[1][0] = 1;//初始条件 第一列不能作为横放格子的第二个格子,只有0一种情况
           for(int i = 2;i <= m+1;i++)
           {
               for(int j = 0;j < (1<<n);j++)
               {
                   for(int k = 0;k <(1<<n);k++)
                   {
                       if(!(j & k)&& st[j|k]) f[i][j] += f[i-1][k];
                   }
               }
           }
           cout<<f[m+1][0]<<endl;//第m+1行应该是没有格子的情况
       }
}

例题引入:P1896 [SCOI2005] 互不侵犯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意很好理解,就是说如果当前点放了国王,以它为中心的九宫格都不能放置国王

二:状态表示:f[i][j][a]:表示前i行放了j个国王,第i行状态为a的方案数

状态转移:用c数组存储当前行的为a状态时的国王数

f[i][j][a] += f[i-1][j-c[k][b];

三.筛选条件

 1.行内合法:与它左右的方格不能同时放1,!(i&i>>1)为真

2.行间合法:和它的上,左上,右上,下都不能同时放1,!(a&b),!(a&b>>1),!(a&b<<1)都为真时,才可以兼容

#include<iostream>
using namespace std;
long long f[14][144][1<<12];
int nums[1<<12];
int s[1<<12];
int n,cnt;
void init()
{
    for(int i = 0;i < (1<<n);i++)
    {
       if(i&(i>>1)) continue;
       s[cnt++] = i;
       for(int j = 0;j < n;j++)
       {
           nums[i] += (i>>j&1);//存储每个状态表示里行间合格的国王数
       }
    }
}
int main()
{
   
    int k;
    cin>>n>>k;
    init();//预处理行间
    f[0][0][0]=1;
    for(int i = 1;i <= n+1;i++)//小技巧:遍历到n+1,那么i+1行的方案数都由i行转移,所有方案都存储在i+1行中
    {
        for(int j = 0;j <= k;j++)
        {
            for(int a = 0;a < cnt;a++)
            {
                for(int b = 0;b < cnt;b++)
                {
                    int c = nums[s[b]];
                    if(j - c >= 0 && !(s[a] & s[b]) && !(s[a] & (s[b]>>1)) && !(s[a]&(s[b]<<1)))
                    {
                        f[i][a][j] += f[i-1][b][j-c];
                    }
                }
            }
        }
    }
  //  cout<<cnt<<endl;
    cout<<f[n+1][0][k];
}

例题引入 P2704 [NOI2001] 炮兵阵地 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这道题和第一题十分相似,但是要求是上下左右各多了一格。

状态表示 f[i][a][b] :第i行的状态为a,i-1行状态为b。

状态转移 f[i][a][b] 是由上一行f[i-1][b][c]转移而来。

筛选条件

1.行内:!(i&i>>2)和!(i&i>>1)同时为真

2.行间  a,b,c相与必须为0

3.不能在山地上布署

#include<iostream>
using namespace std;
int g[110];
int cnt;
int num[1<<10];
int s[1<<10];
int f[2][1<<10][1<<10];

int main()
{
    int n,m;
    cin>>n>>m;
     for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            char ch;
            cin>>ch;
            int x = 0;
            if(ch == 'P') x = 1;
            g[i] = (g[i]<<1) + x;
        }
    }
    for(int i = 0;i < (1<<m);i++)
    {
        if(!(i & i >>1)&&!(i & i >>2)) 
        {
            s[cnt++] = i;
            for(int j = 0;j < m;j++)
            {
                num[i] += (i >> j & 1);
            }
        }
    }
   // cout<<cnt<<endl;
   //f[0][0][0] = 1;
    for(int i = 1;i <= n +2;i++)
    {
        for(int a = 0;a < cnt;a++)
        {
            for(int b = 0;b < cnt;b++)
            {
                for(int c = 0;c < cnt;c++)
                {
                    if(!(s[a]&s[b]) && !(s[a]&s[c]) && !(s[b]&s[c]) && (g[i-1]&s[b])==s[b] &&(g[i]&s[a])==s[a])
                    {
                        f[i&1][a][b] = max(f[i&1][a][b],f[i-1&1][b][c]+num[s[a]]);
                    }
                }
            }
        }
    }
    cout<<f[n+2&1][0][0];
    return 0;
}

  • 15
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
操作系统是计算机系统中的核心组成部分,负责管理和协调计算机硬件和软件资源,提供程序运行环境。在CSDN上有很多关于操作系统的专题文章,以下将从操作系统的基本概念、功能和常见类型等方面简要介绍一下。 首先是操作系统的基本概念。操作系统是一种系统软件,它是计算机硬件和应用软件之间的桥梁,提供给应用程序一系列的服务和资源,同时负责调度和管理系统资源。它为用户屏蔽了底层的硬件差异,提供了一个统一的、易于使用的界面。 操作系统主要有四个基本功能。首先是处理器管理,负责将处理器分配给系统中的各个进程,并进行进程切换,实现多道程序并发执行。其次是内存管理,管理计算机的内存资源,包括分配、回收和保护等操作。再次是文件管理,负责管理文件的存储、命名和保护等操作,提供了文件操作的接口。最后是设备管理,负责管理计算机的各种设备,包括输入输出设备和存储设备等。 常见的操作系统有多种类型。最主流的是Windows、Linux和Mac OS等桌面操作系统。此外还有服务器操作系统,如Windows Server和Linux等,用于管理和部署服务器。还有嵌入式操作系统,如Android和iOS等,用于移动设备和物联网设备。操作系统也有实时操作系统,用于需要实时控制和响应的系统,如工控系统和航空航天系统等。 总之,操作系统是计算机系统中不可或缺的重要组成部分,通过CSDN上的相关文章,我们可以更深入了解操作系统的基本概念、功能和不同类型。这些知识对于理解计算机系统的工作原理和提升编程能力都有着重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值