状压 dp

定义

在动态规划中,可能存在以“集合”为状态的动态规划,应为集合不易表示,所以通常用一个二进制数来表示集合。具体的,二进制数的第 i 位即表示当前集合是否包含第 i 个元素。

技巧

因为许多位运算的运算优先级很迷,所以搞不明白就尽量用括号。

二进制操作

  • n 位二进制数全部赋值为 1s=(1<<n)-1;

  • i 赋值 1s=(s|(1<<(i-1)));

  • 位置 i 赋值 0s=(s&(~(1<<(i-1))));

  • 查询位置 i1/0op=((s>>(i-1))&1;

  • 判断 T 是否属于 S 的子集:op=((s|t)==s);

  • 取出 Sk 位:t=(s&((1<<k)-1));

当然还有一些基础的,就不说了。

子集枚举

有两种方法,第一种是枚举集合,再判断是否属于子集。时间复杂度取决于二进制串的长度 m,为 2m

实际上有一种更快捷的方式:

for(int s=now;;s=((s-1)&now)){
	if(!pre)break;//   不会枚举到空子集
	work...
	//if(!pre)break;   会枚举到空子集
}

时间会更快,具体快多少不知道。

元素枚举

如果要枚举集合中的元素,可以枚举每一位在判断是否为 1,时间复杂度取决于串长,已经十分优秀。但为了应对 sb 卡常题目。可以使用 lowbit 运算优化这个过程。

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 的位置
     	...                           
	}                                
}

对于 log2(u),通常可以预处理出一个数组,可以直接id[1<<i]=i+1。不过比较浪费空间,可以选取一个模数,预处理时 id[(1<<i)%mod]=i+1,访问时 id[u%mod]。模数 m 可以一个 100200 的质数,只要能使 20nmodm 各不相同即可,可以多试试,例如 77,177

好像存在完美算法,可以直接算模数,不过不会。

变量

为了避免混淆,一般用 now,pre,s,t 这样的变量名,而不是 i,j,k 这样的

例题

1.[USACO12MAR] Cows in a Skyscraper G

状压模板。

定义 dpS 表示表示当前集合 S 中所有物品均分组时,最小的分组数量,枚举上一组分出物品的集合 T,显然 TS 子集。定义 wS 表示集合 S 中所有物品总重,如果集合 T 中物品可以放到一组,则必须满足 wTW,综上可得:

dpS=maxTS,wTW{dpT+1}

边界:dp0=0

可以用到子集枚举的优化,就可以通过了。

双倍经验

2.最短 Hamilton 路径

dpS,u 表示目前已经经过了 S 中的所有点,且目前位于点 u 上。显然,一个状态如果合法,必然满足 uS

枚举上一步走到是哪一个点,记为点 v,合法的 v 也必须满足 vS。如果上一步走到了 v,那么经过的点集不包含 u,即 S{u}。定义 du,v 表示边 (u,v) 的长。可得

dpS,u=minvS{dpS{u},v+du,v}

边界:dp{1},1=0,然后没了。

三倍经验

3.蒙德里安的梦想

注意到骨牌有两种摆放方式,分别是竖着放和横着放。如果横着放,那么状态只和这一行有关,如果竖着放,则和上一行有关,我们着重注意这一点设计状态。

我们记 dpi,S 为当前第 i 行中,骨牌上半部分所在下标构成集合 S 时,铺满的方案数。我们考虑第 i 行和第 i1 行的关系。

如果第 i,i1 行的状态合法,必须满足第 i1 行所记录的骨牌的上半部分与第 i 行的下半部分一一对应,即必须满足:

  • i 行和 i1 不能有同一个位置均为骨牌的上半部分。

  • 竖着放的骨牌外必须能够用横着放的骨牌填满。

对于第 1 点,我们将压缩的二进制串相与,如果存在某个位置为 1,则必然不是合法的。对于第 2 点,将压缩的二进制串相或,则此时任何一个 1 的位置均被某个竖着放的骨牌覆盖,只需检查相邻 1 之间的 0 的个数是否为偶数个。

可以预处理出第二点中的合法集合的集合,记为 L。综上可得:

dpi,S=ST=,STL{dpi1,T}

不过本题的边界不好设置,可以特变 i=1,更简单的做法是将边界设为 dp0,0=1

4.[SCOI2005] 互不侵犯

和上一题类似,甚至更简单一点。

注意掉 i,i1 行的国王不能处在同一列,且中间至少相隔一列,预处理出相邻 1 之间不少于 10 的二进制串,记为 L,然后就转换为上一题了,连转移方程都一模一样

统计答案就是 SLdpn,S

更简单的一题

这题与上题的区别是,联通范围从八个方向变成了四个方向,且增加了“地形”限制,只需在输入时将“地形”压缩起来。在动态规划枚举状态时,只需判断是否属于对应行压缩串的子串即可。

当然,也可以子集枚举,具体看技巧。

5.[NOI2001] 炮兵阵地

其实还是和上面是同一道题。

不过略有不同,这道题中当前阶段不止依赖于上一阶段,还依赖于上上个阶段。这样的话我们就要同时记录两个阶段的状态。

dpi,S,T 表示第 i 行放置的炮兵构成 S,第 i1 行构成 T。这样的话就可以从 dpi1,T,G 进行转移。无论是 ST,都必须满足题目中给出的“地形条件”。且不能有同一个位置都放置了炮兵。具体的,记 Vi 表示第 i 行可防止炮兵位置。合法的状态必须满足 SVi,TVi1,ST=

其实这里和上一题一模一样。直接写出转移方程(默认状态是合法的)

dpi,S,T=maxSG={dpi1,T,G+|G|}

如果暴力存储状态的话,会空间爆炸。我们发现很多状态都是无用的,预处理出所有相邻 1 之间不少于 20 的二进制串,发现及时当 n=10 时,满足条件的二进制数不超过 75 个。我们记录这 75 个数就可以。

点击查看代码
#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

很恶心的一道题。

我们记当前扔掉的卡片构成的集合为 S,扔掉 S 中所有卡片成功的方案数为 dpS,记集合 S 能走的路程为 wS,我们枚举上一步扔掉了那个卡片。

可以写出转移方程:

dpS={0wS=b1 or wS=b2uS,T=S{u}dpTother

这样转移为 TLE。常数太大,可以利用 lowbit 优化转移的写法

然后就过了,不是很会做卡常题。

posted @   zuoqingyuan111  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示