前言

状态压缩 DP 是一类常用的 DP 方式,思维难度不是很大,但需要一点卡常和实现技巧。比较容易掌握,是一个骗分的好东西。

状态压缩DP

状态压缩 DP 通常用来处理 DP 过程中与具体状态相关的问题。这种情况下,我们通过二进制压缩使状态可以成为 DP 的一个维度,从而进行动态规划。

因此,状态压缩 DP 的时间复杂度大体为 \(O(2^n)\)。故状态压缩 DP 数据范围一般不大,\(n\le25\) 左右。

一些具体的压缩方式,可以参见例题。

位运算基础知识

例题

例题 \(1\) :

P1879 [USACO06NOV] Corn Fields G

由于数据范围很小,考虑状态压缩 DP。不难发现,在当前这一行某一个位置是否可以种草,取决于这块地是否适合种草以及上一行这个位置是否种草。而当前这一行某一个位置是否可以种草可以直接得出,而上一行这个位置是否种草是转移时需要的信息。因此,我们把上一行每个位置是否种草压缩成状态,成为 DP 数组的维度。

具体的,设状态 \(f[x][y]\) 表示当前处于第 \(x\) 行,上一行状态为 \(y\) 的方案数。若 \(y\) 的第 \(i\) 位为 \(0\),则上一行第 \(i\) 个位置没有种草;若 \(y\) 的第 \(i\) 位为 \(1\),则上一行第 \(i\) 个位置种了草。

考虑枚举这一行的状态 \(z\),\(z\) 的定义与 \(y\) 相同。首先,判定 \(z\) 中所有 \(1\) 的位置是否能种草以及是否有 \(1\) 相邻。然后,判定 \(z\) 中所有 \(1\) 的位置是否在 \(y\) 中也为 \(1\)。如果都为 \(1\),根据状态定义,我们发现出现了有公共边的两块草地,不满足题意。

最后,对于每一个合法状态 \(z\),显然有如下转移方程:

\[f[x+1][z]=f[x+1][z]+f[x][y]
\]

最后的答案为 \(\sum_{i=0}^{2^n-1}f[m+1][i]\),时间复杂度为 \(O(m2^n)\)。代码中使用了记忆化搜索的实现方法。

#include <bits/stdc++.h>
using namespace std;
int m,n,map1[20][20],f[20][5000],mod=100000000;
int dfs(int now,int pre)
{
int ans=0;
if(f[now][pre])return f[now][pre];
if(now==m+1)
{
ans=(ans+1)%mod;
return ans;
}
int t=(1<<n);
for(int i=0;i<t;i++)
{
int flag=1;
for(int j=0;j<n;j++)
if(((i>>j)&1)&&((pre>>j)&1||(i>>(j-1)&1)||(i>>(j+1)&1)||(!map1[now][j+1])))flag=0;
if(flag)ans=(ans+dfs(now+1,i))%mod;
}
f[now][pre]=ans;
return ans;
} int main()
{
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
scanf("%d",&map1[i][j]);
printf("%d\n",dfs(1,0));
return 0;
}

例题 \(2\) :

P2704 [NOI2001] 炮兵阵地

与上一道题目类似,但是需要状态压缩上两行的数据。判断转移也与上面基本一样,但需要判定两行。设状态 \(f[x][y][z]\) 表示目前是第 \(x\) 行,上一行状态为 \(y\),上两行状态为 \(z\),若可以由状态 \(f[x-1][z][w]\),则有转移方程:(令 \(p[x]\) 为状态 \(x\) 的二进制位为 \(1\) 的位置数量)

\[f[x][y][z]=\max(f[x][y][z],f[x-1][z][w]+p[y])
\]

使用滚动数组优化空间即可通过。

#include <bits/stdc++.h>
using namespace std;
int m,n,f[2][3000][3000],p[3000],c[200],ans=0,now=0;
char map1[200][200];
int lowbit(int x)
{
return x&(-x);
} int popcnt(int x)
{
int ans=0;
while(x>0)ans++,x-=lowbit(x);
return ans;
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",map1[i]);
for(int j=0;j<m;j++)
if(map1[i][j]=='P')c[i]+=(1<<j);
}
for(int i=1;i<=(1<<m)-1;i++)p[i]=popcnt(i);
for(int i=0;i<=(1<<m)-1;i++)
for(int j=0;j<=(1<<m)-1;j++)
f[now][i][j]=-1e5;
f[now][0][0]=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<=(1<<m)-1;j++)
for(int k=0;k<=(1<<m)-1;k++)
f[now^1][j][k]=-1e5;
for(int j=0;j<=(1<<m)-1;j++)
for(int k=0;k<=(1<<m)-1;k++)
{
if((j&(j<<1))||(j&(j<<2))||(k&(k<<1))||(k&(k<<2))||((c[i]&j)!=j))continue;
for(int l=0;l<=(1<<m)-1;l++)
{
if((l&(l<<1))||(l&(l<<2))||(j&l)||(k&l)||((c[i+1]&l)!=l))continue;
f[now^1][l][j]=max(f[now^1][l][j],f[now][j][k]+p[l]);
}
}
now^=1;
}
for(int i=0;i<=(1<<m)-1;i++)
for(int j=0;j<=(1<<m)-1;j++)
ans=max(ans,f[now][i][j]);
printf("%d\n",ans);
return 0;
}

例题 \(3\) :

P3959 [NOIP2017 提高组] 宝藏

不难发现最后的路径一定会构成一棵树。由于每条路的贡献与与其深度有关,故考虑设计与深度有关的状态。由于数据范围很小,考虑状态压缩 DP,每次扩展一层。

设状态 \(f[i][j]\) 表示目前扩展到第 \(i\) 层,所有节点的状态为 \(j\)。若第 \(k\) 位为 \(1\),则表示节点 \(k\) 已经被打通;若第 \(k\) 位为 \(0\),则表示节点 \(k\) 未被打通。记状态 \(k\) 转移到状态 \(j\) 经过的边权之和为 \(cost(k,j)\),不难得到如下转移方程:

\[f[i][j]=\min(f[i][j],f[i-1][k]+cost(k,j)\times(i-1))
\]

接下来,考虑预处理出两个数组 \(cost(k,j)\) 与 \(pos(k,j)\),表示状态 \(k\) 转移到状态 \(j\) 经过的边权之和为 \(cost(k,j)\) 与可行性。再预处理一个数组 \(ex(i)\),表示状态 \(i\) 可以扩展的所有点(包括原有点)的状态集合。若第 \(k\) 位为 \(1\),则表示节点 \(k\) 可以被扩展;若第 \(k\) 位为 \(0\),则表示节点 \(k\) 不可以被扩展。

\(ex(i)\) 并不难预处理,只需要枚举已有节点的每一条边,能扩展到的标记为 \(1\) 即可。有了 \(ex(i)\) 后,我们发现 \(pos(k,j)\) 也不难预处理。当且仅当 \(k\) 是 \(j\) 的子集且 \(j\) 是 \(ex(k)\) 的子集时,\(pos(k,j)=1\)。判断子集可以用位运算来实现,若 \(j\&k=k\),则 \(k\) 是 \(j\) 的子集。

接下来,考虑求出满足 \(pos(k,j)\) 的 \(cost(k,j)\)。考虑枚举状态 \(j\) 中未扩展的点,枚举从 \(k\) 中的点到这一个点的边,取边权最小值为贡献。最后,将每一个未扩展的点的贡献加和,即为 \(cost(k,j)\)。预处理之后,转移也比较显然。

时间复杂度为 \(O(m2^{n}+n3^n)\),其中 \(3^n\) 是枚举子集的时间复杂度。

#include <bits/stdc++.h>
using namespace std;
int n,m,u,v,d,h[30000],ex[5000],cst[5000][5000],f[20][5000],ans=1e9;
vector<int>to[20],dis[20];
bool pos[5000][5000];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&d);
to[u].push_back(v),dis[u].push_back(d);
to[v].push_back(u),dis[v].push_back(d);
}
for(int i=0;i<=(1<<n)-1;i++)
{
ex[i]=i;
for(int j=0;j<n;j++)
if(i&(1<<j))
{
int s=to[j+1].size();
for(int k=0;k<s;k++)ex[i]|=(1<<(to[j+1][k]-1));
}
}
for(int i=0;i<=(1<<n)-1;i++)
for(int j=0;j<=(1<<n)-1;j++)
if((j|ex[i])==ex[i]&&(i|j)==j&&i!=j)
{
pos[i][j]=1;
for(int k=0;k<n;k++)
if(!(i&(1<<k))&&(j&(1<<k)))
{
int mi=1e9,s=to[k+1].size();
for(int l=0;l<s;l++)
if((i&(1<<(to[k+1][l]-1))))mi=min(mi,dis[k+1][l]);
cst[i][j]+=mi;
}
}
for(int i=0;i<=n;i++)
for(int j=0;j<=(1<<n)-1;j++)
f[i][j]=1e9;
for(int i=1;i<=n;i++)
f[0][(1<<(i-1))]=0;
for(int i=1;i<=n;i++)
for(int j=0;j<=(1<<n)-1;j++)
for(int k=0;k<=(1<<n)-1;k++)
if(pos[k][j]&&f[i-1][k]!=1e9)f[i][j]=min(f[i][j],f[i-1][k]+cst[k][j]*i);
for(int i=0;i<=n;i++)ans=min(ans,f[i][(1<<n)-1]);
printf("%d\n",ans);
return 0;
}

后记

蒹葭苍苍,白露为霜。所谓伊人,在水一方。溯洄从之,道阻且长。溯游从之,宛在水中央。

【7】状态压缩DP学习笔记的更多相关文章

  1. 学习笔记:状态压缩DP

    我们知道,用DP解决一个问题的时候很重要的一环就是状态的表示,一般来说,一个数组即可保存状态.但是有这样的一些题 目,它们具有DP问题的特性,但是状态中所包含的信息过多,如果要用数组来保存状态的话需要 ...

  2. HDU 3681 Prison Break(状态压缩dp + BFS)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3681 前些天花时间看到的题目,但写出不来,弱弱的放弃了.没想到现在学弟居然写出这种代码来,大吃一惊附加 ...

  3. Victor and World(spfa+状态压缩dp)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=5418 Victor and World Time Limit: 4000/2000 MS (Java/ ...

  4. 状态压缩·一(状态压缩DP)

    描述 小Hi和小Ho在兑换到了喜欢的奖品之后,便继续起了他们的美国之行,思来想去,他们决定乘坐火车前往下一座城市——那座城市即将举行美食节! 但是不幸的是,小Hi和小Ho并没有能够买到很好的火车票—— ...

  5. 数位DP学习笔记

    数位DP学习笔记 什么是数位DP? 数位DP比较经典的题目是在数字Li和Ri之间求有多少个满足X性质的数,显然对于所有的题目都可以这样得到一些暴力的分数 我们称之为朴素算法: for(int i=l_ ...

  6. 状态压缩dp初学__$Corn Fields$

    明天计划上是要刷状压,但是作为现在还不会状压的\(ruoruo\)来说是一件非常苦逼的事情,所以提前学了一下状压\(dp\). 鸣谢\(hmq\ juju\)的友情帮助 状态压缩动态规划 本博文的大体 ...

  7. 树形DP 学习笔记

    树形DP学习笔记 ps: 本文内容与蓝书一致 树的重心 概念: 一颗树中的一个节点其最大子树的节点树最小 解法:对与每个节点求他儿子的\(size\) ,上方子树的节点个数为\(n-size_u\) ...

  8. hoj2662 状态压缩dp

    Pieces Assignment My Tags   (Edit)   Source : zhouguyue   Time limit : 1 sec   Memory limit : 64 M S ...

  9. POJ 3254 Corn Fields(状态压缩DP)

    Corn Fields Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 4739   Accepted: 2506 Descr ...

  10. [知识点]状态压缩DP

    // 此博文为迁移而来,写于2015年7月15日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102w6jf.html 1.前 ...

随机推荐

  1. 想靠RAG提升模型回答质量,那是不可能的

    提供AI咨询+AI项目陪跑服务,有需要回复1 上周写了一篇AI知识库的文章:聊聊与一体机同等级的智商税:AI知识库 事实上,文章对于AI知识库是稍带了点否定的色彩,因为单独的知识库毫无意义,但企业本身 ...

  2. robotframework-python3安装指南

    参考https://blog.csdn.net/ywyxb/article/details/64126927 注意:无论是在线还是离线安装,最好在管理员权限下执行命令 1.安装Python36(32位 ...

  3. web自动化的鼠标操作

    有些场景不适合点击或进行某些操作,可运用action类模拟鼠标操作.在操作一个页面元素时有时需要一连串的动作来配合的时候,可以使用action来完成. Actions actions= new Act ...

  4. AdaBoost算法的原理及Python实现

    一.概述   AdaBoost(Adaptive Boosting,自适应提升)是一种迭代式的集成学习算法,通过不断调整样本权重,提升弱学习器性能,最终集成为一个强学习器.它继承了 Boosting ...

  5. 【经验】VScode 远程 SSH 连接 Ubuntu 或 TrueNas 出错,Could not establish connection

    用VScode常常会碰到以下情况,Could not establish connection. 先介绍一下VScode远程连接和终端SSH连接的区别:终端直接用SSH连接时,只需要开启SSH服务,并 ...

  6. 如何把ASP.NET Core WebApi打造成Mcp Server

    前言 MCP (Model Context Protocol)即模型上下文协议目前不要太火爆了,关于它是什么相信大家已经很熟悉了.目前主流的AI开发框架和AI工具都支持集成MCP,这也正是它的意义所在 ...

  7. Java 数据库开发总结

    数据库连接.设计以及备份技巧集锦 JDBC操作各种数据库经验技巧集萃 Java 数据库连接(JDBC)由一组用 Java 编程语言编写的类和接口组成.JDBC 为工具/数据库开发人员提供了一个标准的  ...

  8. django实例(1)

    Urls.py from django.contrib import adminfrom django.conf.urls import urlfrom cmdb import viewsurlpat ...

  9. 2.2.net core 工作流WorkFlow流程(流程设计)

    流程设计 WikeFlow官网:http://www.wikesoft.com WikeFlow学习版演示地址:http://workflow.wikesoft.com WikeFlow学习版源代码下 ...

  10. 用鼠标画圆点(java GUI)

    话不多说,先看效果 当然你也可以发挥脑洞绘制更更棒的 源码如下: package javaBasic; import java.awt.*; import java.awt.event.*; impo ...