动态规划——状态压缩dp

Source

概述

状态压缩

状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1;当然如果有三种状态用三进制来表示也未尝不可。

使用条件

从问题类型一般分为:棋盘类问题集合类问题
从状态压缩的特点来看,这个算法适用的题目符合以下的条件:

解法需要保存一定的状态数据(表示一种状态的一个数据值),每个状态数据通常情况下是可以通过2进制来表示的。这就要求状态数据的每个单元只有两种状态,比如说棋盘上的格子,放棋子或者不放,或者是硬币的正反两面。这样用0或者1来表示状态数据的每个单元,而整个状态数据就是一个一串0和1组成的二进制数。
解法需要将状态数据实现为一个基本数据类型,比如int,long等等,即所谓的状态压缩。状态压缩的目的一方面是缩小了数据存储的空间,另一方面是在状态对比和状态整体处理时能够提高效率。这样就要求状态数据中的单元个数不能太大,比如用int来表示一个状态的时候,状态的单元个数不能超过32(32位的机器),所以题目一般都是至少有一维的数据范围很小。

状压dp

状压DP,顾名思义,就是使用状态压缩的动态规划。

动态规划问题通常有两种,一种是对递归问题的记忆化求解,另一种是把大问题看作是多阶段的决策求解。这里用的便是后一种,这带来一个需求,即存储之前的状态,再由状态及状态对应的值推演出状态转移方程最终得到最优解

位运算

在这里插入图片描述

棋盘(基于连通性)类问题

概述

棋盘类型的题目一般要求我们依照一定的约束条件,摆放棋子,一般问棋子的数量、摆放的方案数。。。

约束条件可以分为两个角度来看
1、单一的行的约束,如单一行某些列不允许放棋子或是一行连续几列不允许同时摆放棋子。
2、行与行之间的约束,前几行的摆放很可能约束着当前行的摆放,可以预处理状态与状态之间的关系解决这类问题。

例题

蒙德里安的梦想

求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。

例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

如下图所示:

2411_1.jpg

输入格式
输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N 和 M。

当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。

输出格式
每个测试用例输出一个结果,每个结果占一行。

数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205

  1. 思路
    核心:先放横着的,再放竖着的。总方案数,等于只放横着小方块的合法方案数。
    问:如何判断当前方案是否合法?
    答:所有剩余位置能否填充满竖着的小方块,即可以按列来看,每一列内部所有连续空着的小方块个数需要是偶数个

在这里插入图片描述
f[i, j]表示已经摆完i列,并且第i + 1列状态为j的方案数
若此时摆完i-1列,并且第i列状态为k,这种情况下的方案数为f[i - 1, k]。求f[i, j]总的方案数就是将可以从i列的k状态推导出i + 1列j状态的所有方案数累加起来。

  1. 代码
N, M = 12, 1 << 12

def check(num) : #检验是否有奇数个连续的0
	cnt = 0
	for i in range(n) :
		if num >> i & 1 == 0 :
			cnt += 1
		else : 
			if cnt & 1 :
				return False
	return cnt & 1 == 0

while True :
	n, m = map(int, input().split())
	if n == 0 and m == 0 : break
	f = [[0] * M for _ in range(N)]
	st = [False] * M
	head = [[] for _ in range(M)]
	
	for i in range(1 << n) : #记录每一层合法状态
		if check(i) :
			st[i] = True
	
	for i in range(1 << n) : #每一层的状态由两列状态组合而来
		for j in range(1 << n) :
			if i & j == 0 and st[i | j] :
				head[i].append(j)
	
	f[0][0] = 1 #第0列摆放为0的方案数为1
	for i in range(1, m + 1) :
		for j in range(1 << n) :
			for k in head[j] :
				f[i][j] += f[i - 1][k]
	print(f[m][0])#第m列无需摆放的方案,就是第m-1列摆放方法的和

注意!!!由于每一层都是由两列状态组合而来,可能两个非法状态可以组合为一个合法状态。

小国王

在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。

输入格式
共一行,包含两个整数 n 和 k。

输出格式
共一行,表示方案总数,若不能够放置则输出0。

数据范围
1≤n≤10,
0≤k≤n2
输入样例:
3 2
输出样例:
16

  1. 思路
    在这里插入图片描述

    已经摆完了前i行,并且第i行的状态是a,第i-1行的状态为b已经摆了j个国王的方案。对应于已经摆完了前i-1行,并且第i-1行状态为b,已经摆放了j-count(a)个国王的方案。
    所以f[i, j, a] += f[i, j - count(a), b]所有可以从b-》a状态的累加

  2. 代码

N, M = 15, 1 << 15

st = []
head = [[] for _ in range(M)]
cnt = [0] * M
f = [[[0] * M for _ in range(110)] for _ in range(N)]

n, m = map(int, input().split())

def check(num) : #判断num是否连续两位为1
	for i in range(n) :
		if num >> i & 1 and num >> (i + 1) & 1 :
			return False
	return True

def count(num) :#返回num中1的个数
	res = 0
	for i in range(n) :
		res += num >> i & 1
	return res

for i in range(1 << n) :
	if check(i) :
		st.append(i)
		cnt[i] = count(i)

for i in range(len(st)) :
	for j in range(len(st)) :
		a, b = st[i], st[j]
		if a & b == 0 and check(a | b) :
			head[i].append(j)

f[0][0][0] = 1 #初始状态为0
for i in range(1, n + 2) :
	for j in range(m + 1) :
		for a in range(len(st)) :
			c = cnt[st[a]]
			for b in head[a] :
				if j >= c : 
					f[i][j][a] += f[i - 1][j - c][b]
print(f[n + 1][m][0])

玉米田

农夫约翰的土地由 M×N 个小方格组成,现在他要在土地里种植玉米。

非常遗憾,部分土地是不育的,无法种植。

而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。

现在给定土地的大小,请你求出共有多少种种植方法。

土地上什么都不种也算一种方法。

输入格式
第 1 行包含两个整数 M 和 N。

第 2…M+1 行:每行包含 N 个整数 0 或 1,用来描述整个土地的状况,1 表示该块土地肥沃,0 表示该块土地不育。

输出格式
输出总种植方法对 108 取模后的值。

数据范围
1≤M,N≤12
输入样例:
2 3
1 1 1
0 1 0
输出样例:
9

  1. 思路

在这里插入图片描述
已经摆完了前i行,并且第i行状态是a,第i-1行状态是b的方案,对应于已经摆完了前i-1行,并且第i-1行的状态是b的方案。所以已经摆完了前i行,并且第i行状态是a的方案时所有i-1行可以推导出a的方案的和

  1. 代码
N, M = 14, 1 << 14
MOD = int(1e8)

st = []
head = [[] for _ in range(M)]
g = [0] * N
f = [[0] * M for _ in range(N)]

n, m = map(int, input().split())

for i in range(1, n + 1) :#记录废土
	tmp = list(map(int, input().split()))
	for j in range(m) :
		g[i] += (tmp[j] == 0) << j

def check(num) : # 判断是否存在相邻两个1
	for i in range(m) :
		if num >> i & 1 and num >> (i + 1) & 1 :
			return False
	return True

for i in range(1 << m) : #枚举合法状态
	if check(i) :
		st.append(i) 

for i in range(len(st)) : # 枚举状态之间的合法关系
	for j in range(len(st)) :
		a, b = st[i], st[j]
		if a & b == 0 : 
			head[i].append(j)

f[0][0] = 1

for i in range(1, n + 2) :
	for j in range(len(st)) :
		for k in head[j] :
			if st[j] & g[i] : continue
			f[i][j] = (f[i][j] + f[i - 1][k]) % MOD
			
print(f[n + 1][0])

炮兵阵地

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。

一个 N×M 的地图由 N 行 M 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。

在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

在这里插入图片描述

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。

图上其它白色网格均攻击不到。

从图上可见炮兵的攻击范围不受地形的影响。

现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入格式
第一行包含两个由空格分割开的正整数,分别表示 N 和 M;

接下来的 N 行,每一行含有连续的 M 个字符(P 或者 H),中间没有空格。按顺序表示地图中每一行的数据。

输出格式
仅一行,包含一个整数 K,表示最多能摆放的炮兵部队的数量。

数据范围
N≤100,M≤10
输入样例:
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
输出样例:
6

  1. 思路
    在这里插入图片描述

    摆放前i行,且第i行状态为b,第i-1行状态为a,第i-2行状态为c的方案,对应于摆放前i - 1行,且第i - 1行状态为a,第i-2行状态为c的方案。只需要枚举a,c的状态就可以推导出第i行为b状态的方案

  2. 代码

N, M = 14, 1 << 12
g = [0] * 110
st = []
cnt = [0] * M
f = [[[0] * M for _ in range(M)] for _ in range(2)]

n, m = map(int, input().split())

def check(num) : #相邻的三个位置最多存在一个1
	for i in range(m) :
		if num >> i & 1 and (num >> (i + 1) & 1 or num >> (i + 2) & 1) :
			return False
	return True

def count(num) : # 返回num1的个数
	res = 0
	for i in range(m) :
		res += num >> i & 1
	return res

for i in range(1, n + 1) : #记录山脉
	tmp = input()
	for j in range(m) :
		g[i] += (tmp[j] == 'H') << j

for i in range(1 << m) :
	if check(i) :
		st.append(i)
		cnt[i] = count(i)

for i in range(1, n + 3) :
	for j in range(len(st)) :
		for k in range(len(st)) :
			for u in range(len(st)) :
				b, a, c = st[j], st[k], st[u]
				if b & a | a & c | b & c : continue
				if a & g[i] : continue 
				f[i & 1][j][k] = max(f[i & 1][j][k], f[(i - 1) & 1][u][j] + cnt[a])

print(f[(n + 2) & 1][0][0])

集合类问题

概述

对于集合类的问题,一般需要记录当前状态已经包含集合中的哪些元素,转移一般以当前状态作为阶段。再往下细分,根据集合中元素是否可以重复使用分为,不可重复覆盖和重复覆盖问题。一般此类问题使用记忆化搜索比较形象。

例题

最短Hamilton路径

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

输入格式
第一行输入整数 n。

接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式
输出一个整数,表示最短 Hamilton 路径的长度。

数据范围
1≤n≤20
0≤a[i,j]≤107
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18

  1. 思路
    在这里插入图片描述
  2. 代码

递推写法

N = 20
M = 1 << N
INF = int(1e9) + 7

w = [[0] * N for _ in range(N)]
f = [[INF] * N for _ in range(M)]

n = int(input())

for i in range(n) :
	w[i][0 : n] = list(map(int, input().split()))

f[1][0] = 0

for i in range(1 << n) :
	for j in range(n) :
		if i >> j & 1 :
			for k in range(n) :
				if i >> k & 1 :
					f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j])

print(f[(1 << n) - 1][n - 1])

记忆化搜索

N = 20
M = 1 << N
INF = int(1e9) + 7

w = [[0] * N for _ in range(N)]
f = [[INF] * N for _ in range(M)]

n = int(input())

for i in range(n) :
	w[i][0 : n] = list(map(int, input().split()))

f[1][0] = 0

def dfs(state, i) : #标记当前集合状态和当前走到的点
    if state == (1 << n) - 1 :
        return
    for j in range(n) : #枚举未走过的点
        if state >> j & 1 == 0:
            if f[state + (1 << j)][j] <= f[state][i] + w[i][j] : continue #记忆化剪枝
            f[state + (1 << j)][j] = f[state][i] + w[i][j]
            dfs(state + (1 << j), j) #继续搜索
            
dfs(1, 0)

print(f[(1 << n) - 1][n - 1])

愤怒的小鸟

Kiana 最近沉迷于一款神奇的游戏无法自拔。

简单来说,这款游戏是在一个平面上进行的。

有一架弹弓位于 (0,0) 处,每次 Kiana 可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 y=ax2+bx 的曲线,其中 a,b 是 Kiana 指定的参数,且必须满足 a<0。

当小鸟落回地面(即 x 轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有 n 只绿色的小猪,其中第 i 只小猪所在的坐标为 (xi,yi)。

如果某只小鸟的飞行轨迹经过了 (xi, yi),那么第 i 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过 (xi, yi),那么这只小鸟飞行的全过程就不会对第 i 只小猪产生任何影响。

例如,若两只小猪分别位于 (1,3) 和 (3,3),Kiana 可以选择发射一只飞行轨迹为 y=−x2+4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。

这款神奇游戏的每个关卡对 Kiana 来说都很难,所以 Kiana 还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。

这些指令将在输入格式中详述。

假设这款游戏一共有 T 个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。

由于她不会算,所以希望由你告诉她。

注意:本题除 NOIP 原数据外,还包含加强数据。

输入格式
第一行包含一个正整数 T,表示游戏的关卡总数。

下面依次输入这 T 个关卡的信息。

每个关卡第一行包含两个非负整数 n,m,分别表示该关卡中的小猪数量和 Kiana 输入的神秘指令类型。

接下来的 n 行中,第 i 行包含两个正实数 (xi,yi),表示第 i 只小猪坐标为 (xi,yi),数据保证同一个关卡中不存在两只坐标完全相同的小猪。

如果 m=0,表示 Kiana 输入了一个没有任何作用的指令。

如果 m=1,则这个关卡将会满足:至多用 ⌈n/3+1⌉ 只小鸟即可消灭所有小猪。

如果 m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊n/3⌋ 只小猪。

保证 1≤n≤18,0≤m≤2,0<xi,yi<10,输入中的实数均保留到小数点后两位。

上文中,符号 ⌈c⌉ 和 ⌊c⌋ 分别表示对 c 向上取整和向下取整,例如 :⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3。

输出格式
对每个关卡依次输出一行答案。

输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。

数据范围
在这里插入图片描述

输入样例:
2
2 0
1.00 3.00
3.00 3.00
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00
输出样例:
1
1

  1. 思路
    一般抛物线方程: y = a x 2 + b x + c y=ax^2+bx+c y=ax2+bx+c
    题目中的抛物线有两个特点:

    1、过原点, 即 c = 0 c=0 c=0
    2、开口向下,即 a < 0 a<0 a<0
    因此抛物线方程为: y = a x 2 + b x y=ax^2+bx y=ax2+bx,有两个未知数,因此两点即可确定一条抛物线。

    因此最多有 n 2 n^2 n2个不同的抛物线。接下来求出所有不同的抛物线,及其能覆盖的所有点的点集。
    此时问题变成了经典的“重复覆盖问题”,即给定01矩阵,要求选择尽量少的行,将所有列覆盖住。
    每个点是集合中的一个元素,可以重复覆盖集合中的元素。状态记录集合中元素的使用情况,每次选取一个状态中没有使用的点,枚举该点构成的抛物线覆盖的点集。标记当成使用情况后,继续搜索。

  2. 代码

递推

N, M = 20, 1 << 20
INF = int(1e9) + 7
eps = 1e-8
T = int(input())

def cmp(x1, x2) :
	if abs(x1 - x2) < eps : return 0
	if x1 < x2 : return -1
	return 1

for t in range(T) :
	n, m = map(int, input().split())
	q = [[0, 0] for _ in range(N)]
	path = [[0] * N for _ in range(N)]
	f = [INF] * M
	for i in range(n) :
		q[i][0], q[i][1] = map(float, input().split())
	for i in range(n) :
		path[i][i] = 1 << i
		for j in range(n) :
			x1, y1 = q[i][0], q[i][1]
			x2, y2 = q[j][0], q[j][1]
			if not cmp(x1, x2) : continue #组成的线是垂线
			a = (y1 / x1 - y2 / x2) / (x1 - x2)
			b = y1 / x1 - a * x1

			if cmp(a, 0) >= 0 : continue #抛物线开口未向下
			state = 0
			for k in range(n) :
				x, y = q[k][0], q[k][1]
				if not cmp(a * x ** 2 + b * x, y) : state += 1 << k
			path[i][j] = state #记录i,j两点组成的抛物线所覆盖的点
	f[0] = 0 #状态为0是抛物线个数为0
	for i in range((1 << n) - 1) :
		x = 0
		for j in range(n) :
			if i >> j & 1  == 0 : #随便找到一个未被覆盖的点
				x = j
				break
		for j in range(n) : #枚举与x组成抛物线的点,尝试覆盖,记录覆盖后状态的最小值
			f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1)

	print(f[(1 << n) - 1])	

记忆化搜索

N, M = 20, 1 << 20
INF = int(1e9) + 7
eps = 1e-8
T = int(input())

def cmp(x1, x2) :
	if abs(x1 - x2) < eps : return 0
	if x1 < x2 : return -1
	return 1

def dfs(state) :
	if state == (1 << n) - 1 : return
	for i in range(n) :
		if state >> i & 1 : continue #随便找一个未覆盖的点
		for j in range(n) : #枚举未覆盖的点组成的抛物线与原来状态组成的覆盖状态需要的抛物线数量
			if f[state | path[i][j]] <= f[state] + 1 : continue
			f[state | path[i][j]] = f[state] + 1
			dfs(state | path[i][j])
		break

for t in range(T) :
	n, m = map(int, input().split())
	q = [[0, 0] for _ in range(N)]
	path = [[0] * N for _ in range(N)]
	f = [INF] * M
	for i in range(n) :
		q[i][0], q[i][1] = map(float, input().split())
	for i in range(n) :
		path[i][i] = 1 << i
		for j in range(n) :
			x1, y1 = q[i][0], q[i][1]
			x2, y2 = q[j][0], q[j][1]
			if not cmp(x1, x2) : continue #组成的线是垂线
			a = (y1 / x1 - y2 / x2) / (x1 - x2)
			b = y1 / x1 - a * x1

			if cmp(a, 0) >= 0 : continue #抛物线开口未向下
			state = 0
			for k in range(n) :
				x, y = q[k][0], q[k][1]
				if not cmp(a * x ** 2 + b * x, y) : state += 1 << k
			path[i][j] = state #记录i,j两点组成的抛物线所覆盖的点
	f[0] = 0 #状态为0是抛物线个数为0
	dfs(0)

	print(f[(1 << n) - 1])	

总结

状压dp就此结束,状压dp不过如此 我是一颗菜籽,与我无关,狗头保命。

//                            _ooOoo_  
//                           o8888888o  
//                           88" . "88  
//                           (| -_- |)  
//                            O\ = /O  
//                        ____/`---'\____  
//                      .   ' \\| |// `.  
//                       / \\||| : |||// \  
//                     / _||||| -:- |||||- \  
//                       | | \\\ - /// | |  
//                     | \_| ''\---/'' | |  
//                      \ .-\__ `-` ___/-. /  
//                   ___`. .' /--.--\ `. . __  
//                ."" '< `.___\_<|>_/___.' >'"".  
//               | | : `- \`.;`\ _ /`;.`/ - ` : | |  
//                 \ \ `-. \_ __\ /__ _/ .-` / /  
//         ======`-.____`-.___\_____/___.-`____.-'======  
//                            `=---='  
//  
//         .............................................  
//                  佛祖保佑             永无BUG 
//          佛曰:  
//                  写字楼里写字间,写字间里程序员;  
//                  程序人员写程序,又拿程序换酒钱。  
//                  酒醒只在网上坐,酒醉还来网下眠;  
//                  酒醉酒醒日复日,网上网下年复年。  
//                  但愿老死电脑间,不愿鞠躬老板前;  
//                  奔驰宝马贵者趣,公交自行程序员。  
//                  别人笑我忒疯癫,我笑自己命太贱;  
//                  不见满街漂亮妹,哪个归得程序员?