状压 dp
定义
在动态规划中,可能存在以“集合”为状态的动态规划,应为集合不易表示,所以通常用一个二进制数来表示集合。具体的,二进制数的第
技巧
因为许多位运算的运算优先级很迷,所以搞不明白就尽量用括号。
二进制操作
-
将
位二进制数全部赋值为 :s=(1<<n)-1;
-
位
赋值 :s=(s|(1<<(i-1)));
-
位置
赋值 :s=(s&(~(1<<(i-1))));
-
查询位置
为 :op=((s>>(i-1))&1;
-
判断
是否属于 的子集:op=((s|t)==s);
-
取出
后 位:t=(s&((1<<k)-1));
当然还有一些基础的,就不说了。
子集枚举
有两种方法,第一种是枚举集合,再判断是否属于子集。时间复杂度取决于二进制串的长度
实际上有一种更快捷的方式:
for(int s=now;;s=((s-1)&now)){
if(!pre)break;// 不会枚举到空子集
work...
//if(!pre)break; 会枚举到空子集
}
时间会更快,具体快多少不知道。
元素枚举
如果要枚举集合中的元素,可以枚举每一位在判断是否为
for(int now=0;now<(1<<n);now++){
for(int s=now,u=(s&-s);s;s^=u,u=(s&-s)){//log2(u) 为 now 中的一个 1 的位置
...
}
}
对于 id[1<<i]=i+1
。不过比较浪费空间,可以选取一个模数,预处理时 id[(1<<i)%mod]=i+1
,访问时 id[u%mod]
。模数
好像存在完美算法,可以直接算模数,不过不会。
变量
为了避免混淆,一般用 now,pre,s,t
这样的变量名,而不是 i,j,k
这样的
例题
1.[USACO12MAR] Cows in a Skyscraper G
状压模板。
定义
边界:
可以用到子集枚举的优化,就可以通过了。
双倍经验。
2.最短 Hamilton 路径
令
枚举上一步走到是哪一个点,记为点
边界:
3.蒙德里安的梦想
注意到骨牌有两种摆放方式,分别是竖着放和横着放。如果横着放,那么状态只和这一行有关,如果竖着放,则和上一行有关,我们着重注意这一点设计状态。
我们记
如果第
-
第
行和 不能有同一个位置均为骨牌的上半部分。 -
竖着放的骨牌外必须能够用横着放的骨牌填满。
对于第
可以预处理出第二点中的合法集合的集合,记为
不过本题的边界不好设置,可以特变
4.[SCOI2005] 互不侵犯
和上一题类似,甚至更简单一点。
注意掉 连转移方程都一模一样。
统计答案就是
这题与上题的区别是,联通范围从八个方向变成了四个方向,且增加了“地形”限制,只需在输入时将“地形”压缩起来。在动态规划枚举状态时,只需判断是否属于对应行压缩串的子串即可。
当然,也可以子集枚举,具体看技巧。
5.[NOI2001] 炮兵阵地
其实还是和上面是同一道题。
不过略有不同,这道题中当前阶段不止依赖于上一阶段,还依赖于上上个阶段。这样的话我们就要同时记录两个阶段的状态。
设
其实这里和上一题一模一样。直接写出转移方程(默认状态是合法的)
如果暴力存储状态的话,会空间爆炸。我们发现很多状态都是无用的,预处理出所有相邻
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=105,M=(1<<10),inf=1e9+10;
int n,m,val[N],p,mk[N],dp[N][N][N],num[M],ans;
char ch;
inline bool check(int i,int s){
return (mk[i]|s)==mk[i]?0:1;
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>ch;
if(ch=='P')mk[i]=(mk[i]<<1)+1;
else mk[i]=(mk[i]<<1);
}
}
for(int now=0;now<(1<<m);now++){
bool f=1;int cnt=0,tmp=2;
for(int i=0;i<m;i++){
if((now>>i)&1){
if(tmp<2){f=0;break;}
else tmp=0;
}
else tmp++;
}
for(int s=now;s>0;s-=(s&-s))cnt++;
if(f)val[++p]=now;
num[now]=cnt;
}
dp[0][0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=p;j++){
if(check(i,val[j]))continue;
for(int k=1;k<=p;k++){
dp[i][j][k]=-inf;
if(val[j]&val[k])continue;
if(check(i-1,val[k]))continue;
for(int l=1;l<=p;l++){
if(check(i-2,val[l]))continue;
if((val[j]&val[l])||(val[k]&val[l]))continue;
dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][l]+num[val[j]]);
}
if(i==n)ans=max(ans,dp[i][j][k]);
}
}
}
cout<<ans<<endl;
return 0;
}
6.yyy loves Maths VII
很恶心的一道题。
我们记当前扔掉的卡片构成的集合为
可以写出转移方程:
这样转移为 TLE。常数太大,可以利用
然后就过了,不是很会做卡常题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探