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

这篇具有很好参考价值的文章主要介绍了一文带你入门并吃透状态压缩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

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

#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.不能在山地上布署文章来源地址https://www.toymoban.com/news/detail-493676.html

#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;
}

到了这里,关于一文带你入门并吃透状态压缩DP的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Web前端】一文带你吃透CSS(中篇)

    前端学习路线小总结 : 基础入门: HTML CSS JavaScript 三大主流框架: VUE REACT Angular 深入学习: 小程序 Node jQuery TypeScript 前端工程化

    2023年04月25日
    浏览(49)
  • 【Web前端】一文带你吃透CSS(完结篇)

    前端学习路线小总结 : 基础入门: HTML CSS JavaScript 三大主流框架: VUE REACT Angular 深入学习: 小程序 Node jQuery TypeScript 前端工程化

    2024年01月20日
    浏览(50)
  • 【Spring】一文带你吃透基于注解的DI技术

    个人主页: 几分醉意的CSDN博客_传送门 基于注解的DI:使用spring提供的注解,完成java对象创建,属性赋值。 注解使用的核心步骤: 1.在源代码加入注解,例如@Component。 2.在spring的配置文件,加入组件扫描器的标签。 @Component: 表示创建对象,对象放到容器中。 作用是 声明组件

    2024年02月03日
    浏览(89)
  • 【Spring】一文带你吃透AOP面向切面编程技术(上篇)

    个人主页: 几分醉意的CSDN博客_传送门 什么是AOP? AOP(Aspect Orient Programming):面向切面编程 Aspect:表示切面,给业务方法增加的功能,叫做切面。切面一般都是非业务功能,而且切面功能一般都是可以复用的。例如日志功能,事务功能,权限检查,参数检查,统计信息等等

    2024年01月16日
    浏览(60)
  • 一文带你吃透JSP,增删改查实战案例详细解读

    不得不说,JSP 现在已经是一门十分老旧的技术了,学习编程时,不仅要学习优秀的前言技术,还要对基础有一定的把握,所以学习 JSP 时,我们只做了解,不用刨根问底花费大量的时间,得不偿失。 我们主要从以下几个方面学习 JSP 技术: 理解 JSP 及其原理 学会使用 EL 表达

    2024年02月03日
    浏览(46)
  • 【C++】一文带你吃透string的模拟实现 (万字详解)

    (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是 Scort 🎓 🌍博客主页:张小姐的猫~江湖背景 快上车🚘,握好方向盘跟我有一起打天下嘞! 送给自己的一句鸡汤🤔: 🔥真正的大师永远怀着一颗学徒的心 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏 🎉🎉欢迎持续关注! 🎨传统写

    2024年02月03日
    浏览(203)
  • 如何使用JDBC操作数据库?一文带你吃透JDBC规范

    大家好,我是橙子。最近又肝了几个大夜,总结了 JDBC 完整版的基础教程和实战案例训练。快来看看这些 Java 基础性的代码你有没有忘记? 在 Java 开发中,使用 Java 语言操作数据库是非常重要的一部分,那么 Java 语言是如何操作数据库的呢? 我们需要使用不同厂商的数据库

    2024年02月03日
    浏览(191)
  • 【Kubernetes 系列】一文带你吃透 K8S 应用pod结点

    作者:半身风雪 上一节:创建K8s集群项目 简介:上一节我们一起学习了,如何去部署一个K8S 的应用程序,这一节,我们主要讲解一下,K8S 应用的框架结构。 本节我将和大家一起学习Kubernetes 应用中的pod结点 了解 Kubernetes Pod。 了解 Kubernetes 工作节点。 对已部署的应用故障排

    2023年04月08日
    浏览(106)
  • C++ 动态规划 状态压缩DP 最短Hamilton路径

    给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。 Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。 输入格式 第一行输入整数 n 。 接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[

    2024年02月19日
    浏览(38)
  • acwing算法基础之动态规划--数位统计DP、状态压缩DP、树形DP和记忆化搜索

    暂无。。。 暂无。。。 题目1 :求a~b中数字0、数字1、…、数字9出现的次数。 思路:先计算1~a中每位数字出现的次数,然后计算1~b-1中每位数字出现的次数,两个相减即是最终答案。 那么,如何计算1~a中每位数字出现的次数呢? 首先,将a的每一位存入向量num中,例如a=123

    2024年02月04日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包