【动态规划】状态压缩DP(状压dp)

还在更新ing

一、引入

在动态规划状态设计中,若状态是一个集合,例如 S= { 1,0,1,1,0 } ,则表示第 124节点被选中(从右往左对应 04 号节点)。若集合的大小不超过 N ,则集合中的每个元素都是小于 K 的正整数,可以把这个集合看作一个 NK 进制数,以一个 [0,KN1]十进制整数作为 DP 状态。可以将 S= { 1,0,1,1,0 } 看作一个 5二进制数 10110 ,其对应的十进制数21
这种将集合作为整数记录状态的一类算法叫作状态压缩 DP 。在状态压缩 DP 中,状态的设计直接决定了程序的效率或者代码长短。我们需要根据问题分析本质,才能更好地找出恰当的状态表示状态转移方程边界条件

二、二进制 & 位运算

尽管用了一个十进制数据储存二进制状态,当因为操作系统是二进制的,所以在编译器中也可采用位运算解决这个问题。

1、基本位操作运算符

在状态压缩 DP 中广泛运用位运算操作,常见的位运算如下 :

A B ~A(差) A&B(与) A|B(或) A ^ B(异或)
0 0 1 0 0 0
0 1 1 0 1 1
1 0 0 0 1 1
1 1 0 1 1 0

如果 A,B 为整数,则可看成长度为 32 位的两个二进制数各个位分别做上面操作。
例如:
A10=(1010)B,B12=(1100)B 时,

A&B=8=(1000)B
A|B=14=(1110)B
A ^ B=6(0110)B
~ A=11(1...10101)B

另外,还有两个位移操作运算符左移(<<)右移(>>)
如:A<<B,表示 A 的所有位都向左移动 B 个,后面 B 位用 0 补充。
注意,对二进制数,位数高低为从右到左依次为 0 位、1 位、2
所以我们可以认为 A<<B 等价于 A×2BA>>B 等价于 A2B

2,常见的操作有:

去掉最后一位 ( 10110110110 ) x >> 1
在最后加一个 0 ( 1011011011010 ) x << 1
在最后加一个 1 ( 1011011011011 ) x << 1 | 1
把最后一位变成 1 ( 101100101101 ) x | 1
把最后一位变成 0 ( 101101101100 ) x | 1 - 1
最后一位取反 ( 101101101100 ) x ^ 1
把右数第 k 位变成 1 ( 101001101101,k=3 ) x | (1 << (k - 1))
把右数第 k 位变成 0 ( 101101101001,k=3) x & not (1 << (k - 1))
右数第 k 位取反 ( 101001101101,k=3 ) x ^ (1 << (k - 1))
取末三位 ( 1101101101 ) x & 7
取末 k 位 ( 11011011101,k=5 ) x & (1 << k - 1)
取右数第 k 位 ( 11011011,k=4 ) x >> (k - 1) & 1
把末 k 位变成 1 ( 101001101111,k=4 ) x | (1 << k-1)
k 位取反 ( 101001100110,k=4 ) x ^ (1 << k-1)
把右边连续的 1 变成 0 ( 100101111100100000 ) x & (x + 1)
把右起第一个 0 变成 1 ( 100101111100111111 ) x | (x + 1)
把右边连续的 0 变成 1 ( 1101100011011111 ) x | (x - 1)
取右边连续的 1 ( 1001011111111 ) (x ^ (x + 1)) >> 1

3、表示集合(状态压缩)

有时我们用 32 位的整型数的各个位表示一个最多 32 个元素的集合,第 i 位为 1 表示第 i 个元素在集合中,为 0 则表示不在集合中。
常见的操作有:
并集操作 A | B
交集操作 A & B
集合的差 A & ~ B
补集 -1 ^ A1 为二进制数 111...1)
加入第 i 个元素 A = A | (1 << (i - 1))
删除第 i 个元素 A = A & ~ (1 << (i - 1))
判断第 i 个元素 (A & (1 << (i - 1))) != 0

4、lowbit

求最低位的 1 的位置,即著名的 lowbit 问题。

lowbit(int x) {
	return x&(-x);
}

三、实现流程

状态压缩 DP 大可分为两类 :

  • 棋盘式(基于连通性)DP
  • 集合式 DP

状态压缩dp三部曲:

  • 考虑如何状态压缩
  • 确定状态表示和状态转移方
  • 根据实际问题确定筛选条件

例题引入

著名的旅行商问题( Traveling Salesman Problem,TSP )指一个旅行商从一个城市出发,经过每一个城市一次且只有一次回到原来的地方,要求经过的距离最短。
在这里插入图片描述
TSP 问题是一个 NP 难题,目前没有多项式时间的高效算法。若采用搜索+剪枝,则该算法的时间复杂度为 O(n!) ,数据量大时,这种方法无法解决,可以尝试采用动态规划解决。
假设已访问的节点集合为 S (将源点 0 当作未被访问的节点,因为从 0 出发,所以要回到 0 ),当前所在的节点为 u

I. 状态表示

fS,u 表示已经访问的节点集合为 S ,从 u 出发走完所有剩余节点回到源点的最短距离。

II. 状态转移方程

若当前 u 的邻接节点 v 未被访问,则 fS,u 由两部分组成,第 1 部分是 uv 的边值,第 2 部分是从 v 出发走完所有剩余节点再回到源点的最短距离。访问完 v 之后,已访问的节点集合变为 S { v } ,若 u 有多个未被访问的邻接点 v ,则取最小值。

fS,u=min(fSv,va+wuv|vS

III. 临界条件

f(1<<n)1,0=0 ,表示若所有节点都被访问,则此时已经没有剩余节点,从 0 节点出发走完所有剩余节点回到源点的最短距离为 0

Code

void Traveling() {    //计算f[S][u]
	f[(1<<n)-1][0]=0;     //1<<n一定要加括号(别问我为什么)
	for(int S=(1<<n)-2;S>=0;S--) {
		for(int u=0;u<n;u++) {
			for(int v=0;v<n;v++) {
				if((u!=0&&!(S>>u&1))||w[u][v]==inf) continue;   //可以加约束条件,不加太多状态
				if(!(S>>v&1)&&f[S][u]>f[(S|1<<v)][v]+w[u][v]) {
					f[S][u]=f[(S|1<<v)][v]+w[u][v];
					path[S][u]=v;     //记录后继节点
				} 
			} 
		}
	}
}

训练 1 :[USACO06NOV] Corn Fields G

源自 洛谷 P1879 [USACO06NOV] Corn Fields G

题目描述

农场主 John 新买了一块长方形的新牧场,这块牧场被划分成 MN(1M12;1N12),每一格都是一块正方形的土地。 John 打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是 John 不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

John 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

输入格式

第一行:两个整数 MN,用空格隔开。

2 到第 M+1 行:每行包含 N 个用空格隔开的整数,描述了每块土地的状态。第 i+1 行描述了第 i 行的土地,所有整数均为 01 ,是 1 的话,表示这块土地足够肥沃,0 则表示这块土地不适合种草。

输出格式

一个整数,即牧场分配总方案数除以 100,000,000 的余数。

样例 #1

样例输入 #1

2 3
1 1 1
0 1 0

样例输出 #1

9

题解

I. 分析题意

本题求在值为 1 的土地上种上植草地,每个草地的上下左右相邻的土地上不能有草地,求所有的种植方案数。

II. 算法设计

Code

posted @   Fireworks_Rise  阅读(230)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示