状态压缩DP

目录

一、前言

二、状态压缩DP与例题

1、糖果(2019年第十届省赛,lanqiaoOJ题号186)

2、矩阵计数(2019年第十届国赛,lanqiaoOJ题号246)

3、回路计数(2021年第十二届省赛填空题,lanqiaoOJ题号1462)

三、状态压缩DP的原理

1、原理

2、用位运算做集合操作


一、前言

本文讲了状态压缩的例题以及位运算做集合操作的原理。

二、状态压缩DP与例题

1、糖果(2019年第十届省赛,lanqiaoOJ题号186)

【题目描述】

糖果店的老板一共有 M 种口味的糖果出售。为了方便描述,我们将 M 种口味编号 1~M。小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是望 K 颗一包整包出售。幸好糖果包装上注明了其中 K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。给定 N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。

【输入】

第一行包含三个整数 N、M和K。接下来 N 行每行 K 个整数 T1, T2, ..., TK,代表一包糖果的口味 (对于30%的评测用例, 1<=N<=20。对于所有评测样例,1<=N<=100, 1<=M<=20, 1<=K<=20, 1<=Ti<=M。)

【输出】

一个整数表示答案。如果小明无法品尝所有口味,输出-1。

【输入样例】

6 5 3

1 1 2

1 2 3

1 1 3

2 3 5

5 4 2

5 1 2

【输出样例】

2

【暴力法】

  • 对 n 包糖果做任意组合,找到其中一种组合,能覆盖所有口味,并且需要的糖果包数量最少。
  • n 包糖果的组合共有 2^n 种,暴力法能通过 30% 的测试。n=100 时,严重超时。

【DP】

1)定义状态 dp[i]:表示得到口味组合 i 所需要的最少糖果包数量。

2)状态转移。往口味组合 i 中加入一包糖果,设得到新的口味组合 j,说明从 i 到 j 需要糖果包数量 dp[i]+1。若原来的 dp[j] 大于 dp[i]+1,说明原来得到 j 的方法不如现在的方法,更新dp[j] = dp[i]+1。  

【关键问题:如何表示口味组合】

一个方法:为每一包糖果定义一个大小为 m 的数组,记录它的口味。

n 包糖果的口味是一个 n*m 的二维数组,定义为 kw[n][m]。

例:设共有 m=10 种口味, kw[1][] = {0,0,0,0,0,1,0,1,1,0},表示第一包糖果的口味有种。

更简单的方法:记录一包糖果的口味,用状态压缩。

【例子说明状态压缩】

  • 例:一包里面有 3 颗糖果,分别是 “2, 3, 5” 三种口味,用一个二进制数 “10110” 表示,这个二进制数的每一位表示一种口味。
  • 使用状态压缩之后,原来需要用二维数组 kw[n][m] 才能表示 n 包糖果的口味,现在只需要一个一维数组 kw[n]。例如 kw[1]=10110,表示第一包的口味是 “2, 3, 5” 三种。
  • 状态压缩 DP 的代码写起来很简洁,因为可以用位运算来简化代码。

1)用状态压缩表示糖果口味

例:输入一包糖果的 “2, 3, 5” 三种口味。

把这 3 个口味压缩成二进制数 10110,做“移位”和“或”操作。

1)定义初始值 tmp = 0。

2)输入口味 “2”。

先移位 1<<(2-1),得二进制数 10;然后再与 tmp 或,得 tmp = tmp|10 = 10

3)输入口味 “3”。先移位 1<<(3-1),得二进制数 100;然后再与 tmp 或,得 tmp = tmp|100 = 110

4)输入口味 “5”。先移位 1<<(5-1),得二进制数 10000;然后再与 tmp 或,得 tmp = tmp|100 = 10110。

代码:tmp = (1<<x-1)

2)dp[]中状态压缩的处理

  • dp[i]:i表示口味,用状态压缩表示;dp[i] 表示得到口味 i 的最少糖果包数量。
  • 状态转移:同样用到二进制的 “或” 操作。例如 tmp 表示某一包的糖果口味,那么 dp [ i | tmp ] 就表示得到口味 i|tmp 所需要的最少糖果包数量。
n,m,k=map(int,input().split())
tot=(1<<m)-1    #tot:二进制是m个1,表示所有m种口味
dp=[-1 for _ in range(1<<20)]
dp[0]=0
kw=[]
for _ in range(n):
    kw.append([int(i) for i in input().split()])    #kw是二维矩阵
for c in kw:    #用c遍历每个糖果
    tmp=0
    for x in c:
        tmp|=(1<<(x-1))     #用x遍历这包糖果岛口味
    for i in range(tot+1):
        if dp[i]==-1:
            continue        #已存在得到口味i的最少糖果包数量
        newcase=i|tmp
        if dp[newcase]==-1 or dp[newcase]>dp[i]+1:
            dp[newcase]=dp[i]+1
print(dp[tot])              #得到所有口味tot的最少糖果包数量

妙啊,上面这个方法!

2、矩阵计数(2019年第十届国赛,lanqiaoOJ题号246)

【题目描述】

一个 N×M 的方格矩阵,每一个方格中包含一个字符 O 或者字符 X。要求矩阵中不存在连续一行 3 个 X 或者连续一列 3 个 X。问这样的矩阵一共有多少种?

【输入描述】

输入一行包含两个整数 N, M (1<=N, M<=5)

【输出描述】

输出一个整数代表答案。

【分析】

  • 很直接的状态压缩 DP。
  • 把方格中的字符 '0',看成数字 0,字符 'X' 看成数字 1。
  • 把每一行看成一个 m 位的二进制数,例如一行字符 “00X0X” 对应二进制数 “00101”。
  • 一行数字有 2^m 种情况,即范围 [0, 1<<m) 内的数字。这些数字里面只有部分数字符合要求,把这些数存进一个数组 row[]。
  • 要求:这个数字中没有连续的3个1。一个符合要求的数就是这一行的一个合法状态。

定义状态 dp[i][j][k]:第 i 行的合法状态为 j,前一行的合法状态为 k 时,符合条件的矩阵有多少个。

连续 3 行的情况:设第 i 行状态为 j,前一行状态为 k,再前面一行状态为 p,那么 j&k&p 等于0,说明这 3 行没有一列上是3个1。这 3 行是一种合法状态。

状态的递推:

if j&k&p==0:

        dp[i][j][k]+=dp[i-1][k][p]

def check(x):       #检查x行中有没有连续3个1
    num=0
    while x:
        if x&1:
            num+=1  #发现一个1
        else:
            num=0
        if num==3:
            return False    #有连续3个1
        x>>=1           #右移一次
    return True

N,M=list(map(int,input().split()))
s=2**M
row=[]                  #合法的行
for i in range(s):      #i的范围:00000~11111
    if check(i):
        row.append(i)

dp=[[[0 for i in range(s)] \
     for j in range(s)] \
    for k in range(N)]      #dp[i][j][k]:第i行的合法状态为j,前一行的合法状态为k时,符合条件的矩阵有多少个

for i in row:
    dp[0][i][0]=1

for i in range(1,N):
    for j in row:       #在所有行都合法的情况下,检查连续3列是否合法
        for k in row:
            for p in row:           #4个for循环,O(N*2^(3m))
                if j&k&p==0:
                    dp[i][j][k]+=dp[i-1][k][p]  

ans=0
for j in row:
    ans+=sum(dp[N-1][j])
print(ans)

3、回路计数(2021年第十二届省赛填空题,lanqiaoOJ题号1462)

【题目描述】

蓝桥学院由 21 栋教学楼组成,教学楼编号 1 到 21。对于两栋教学楼 a 和 b,当 a 和 b 互质时, a 和 b 之间有一条走廊直接相连,两个方向皆可通行,否则没有直接连接的走廊。小蓝现在在第一栋教学楼,他想要访问每栋教学楼正好一次,最终回到第一栋教学楼 (即走一条哈密尔顿回路),请问他有多少种不同的访问方案?两个访问方案不同是指存在某个 i,小蓝在两个访问方法中访问完教学楼 i 后访问了不同的教学楼。

【Hamilton问题】

Hamilton 问题是 NP 问题,没有多项式复杂度的解法。

暴力解法:枚举 n 个点的全排列。共 n! 个全排列,一个全排列就是一条路径。

本题 n=21,21! = 51,090,942,171,709,440,000

用状态压缩 DP,复杂度下降为 2^n

参考: 《算法竞赛》341页,或者百度搜 “罗勇军博客” 找到 “状态压缩DP”

【状态压缩】

路径起点是1,绕一圈回到 1。

问题转化:1 先到 2~21 中的某个点,然后再绕一圈回到 1。

定义DP。设 S 是图的一个子集,用 dp[S][j] 表示 “集合 S 内的 Hamilton 路径个数”,即从起点 1 出发经过 S 中所有点,到达终点 j 时的路径个数。

最后求所有的访问方案:累加所有的 dp[X][2]~dp[X][21]。

其中 X=1<<22-2,X 是除了点 1 以外的所有其他点。

【哈密尔顿问题与DP】

如何求 dp[S][j]?从小问题 S-j 递推到大问题 S。

S-j 表示从集合 S 中去掉 j,即不包含 j 点的集合。

如何从 S-j 递推到 S?设 k 是 S-j 中一个点,把从 1 到 j 的路径分为两部分:(1→...→k)+(k→j)。

以 k 为变量枚举 S-j 中所有的点

状态转移方程:dp[S][j] += dp[S-j][k]

代码:dp[S][j] += dp[S-(1<<j)][k]

from math import gcd
m=1<<22
dp=[[0 for j in range(22)] for i in range(m)]
dist=[[False for j in range(22)] for i in range(22)]
for i in range(1,22):
    for j in range(1,22):
        if gcd(i,j)==1:
            dist[i][j]=True
dp[2][1]=1
for S in range(2,m-1):
    for j in range(1,22):
        if S>>j&i:
            for k in range(1,22):
                if S-(1<<j)>>k & 1 and dist[k][j]:
                    dp[S][j]+=dp[S-(1<<j)][k]
ans=0
for i in range(2,22):
    ans+=dp[m-2][i]
print(ans)
  • 初始化:教学楼编号1到21。教学楼a和b,当a和b互质时,a和b之间有一条走廊直接相连,两个方向皆可通行,否则没有直接连接的走廊。
  • 状态转移:dp[S][j]+=dp[S-(1<<j)][k]
  • 复杂度:3个for循环
  • m(n^2)=(2^21)(21^2)=924,844,032
  • 运行时间长达10分钟
  • 这题用python很慢,如果用C写,只需运行10秒
  • 如果出题,python组中出状态压缩DP大家很可能都会超时(部分老师会选择不在Python组出状态压缩)

三、状态压缩DP的原理

1、原理

  • 应用背景:以集合为状态,且集合可以用二进制来表示,用二进制的位运算来处理。
  • 集合问题一般是指数复杂度的,例如:1)子集问题,设元素无先后关系,那么共有 2^n 个子集;2)排列问题,对所有元素进行全排列,共有 n! 个全排列。
  • 状态压缩DP:集合的状态 (子集或排列),如果用二进制表示状态,并用二进制的位运算来遍历和操作,又简单又快。

2、用位运算做集合操作

判断 a 的第 i 位(从最低位开始数)是否等于1:(1<<(i- 1)) & a

把 a 的第 i 位改成 1:a | (1<<(i-1))

把 a 的第 i 位改成 0:a & (~(1<<i))

把 a 的最后一个1去掉:a & (a-1)

以上,状态压缩DP

祝好

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕飞雨的头发不能秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值