8.18-8.20
鉴于这几天都讲的是DP所以放一起写了,写完后面有时候会对前面有所启发。
最大的感受就是时间不够,算法往往只学了个模板,题在新学过的前提下不是能立刻完成的。通常需要多写几遍模板,多看几篇有用的题解,多翻几遍书才有独立完成一道黄、绿题的能力。
事实上这些DP都有一些明显的特质,但又有一些共性,这些会在分别讲每个算法的时候阐述。
状压DP
首先是状压,但在写之前有一个前置算法位运算
位运算:能够非常迅速的运算,可以取出修改二进制下任意位,二进制数可以便捷的表示单一状态,这些是在状压DP下的用处。
一个数表示成二进制,如5=101、7=111……这个数本身的值在这里并不重要,它的二进制数的第i位可以用来表示第i个……的状态(只能是01,如取或不取)
我学习状压DP的第一感受是:“原来状态还可以这样表示?!” 用一个整数的二进制形式来紧凑地表示一个复杂的状态集合。
- 状态的01:状压DP处理的问题中,关键的状态信息通常是一些“是/否”、“有/无”、“开/关”的二元选择,比如:
一个集合中哪些元素被选中了。
一个网格的某一行(或某一列)上是否放置了物品(或其放置方式符合某种约束)。
某些设备(灯、资源、任务)是否被占用/激活/完成。
图中某个点是否被访问过。
- 整数的二进制位映射:将以上这些二元状态映射到一个整数 state 的各个二进制位上。通常:
第 i 位为 1:表示第 i 个元素/位置/设备处于“是”(被选中/放置了/占用/已访问等)状态。
第 i 位为 0:表示处于“否”状态,反之。
例如:state = 5(二进制 101)可以表示选中了第 0 个和第 2 个元素(假设最低位是第 0 位)。
- 状态空间的压缩:一个 N 位的二进制数可以表示 2^N 种不同的状态组合。原本需要 N 维布尔数组(或者更高维)才能表示的状态空间,被压缩成了一个 [0, 2^N - 1] 范围内的整数!这就是“压缩”名称的由来,极大地减小了我们需要遍历和存储的状态数量,非常的节省空间(确信)。
状压DP的特征:
-
规模较小 (关键!):这是状压DP的显著标志。题目中的关键对象数量 N(如需要被选择的元素、需要放置物品的行/列数、图中的点数等)往往在 20 左右,甚至更小 (N <= 16 很常见)
-
状态是离散的布尔组合:如前面所述,状态需要能用多个“是/否”的二元信息来描述。
-
状态转移依赖于局部布尔信息:当前状态如何转移到下一个状态,通常取决于当前状态中某几个位(对应某几个具体元素/位置)的值以及下一步要做的决策涉及的位。
-
常见的题型模板:
◦ 棋盘/网格放置类:在 N x M 的网格(特别是 M <= N 且较小)上放置棋子或其他物体,要求物体之间满足一定的约束(如不能相邻、不能同列等)。行与行之间的约束通常通过列的状态(用位掩码表示前一行的放置情况)来传递。◦ 图的Hamilton路径/环:要求访问所有节点恰好一次。状态 state 表示当前已经访问过的节点集合(一个位掩码),dp[state][i] 表示从起点(或某个特定点)出发,访问完 state 包含的所有节点,且最后一个访问的节点是 i 的最小代价/方案数等。
◦ 集合划分/子集覆盖类:选择满足某些条件的不重叠子集去覆盖所有元素,或者将元素划分成若干满足条件的组。
◦ 旅行商问题 (TSP):访问所有城市并返回起点,求最短路。这就是经典的需要状态 state(已访问城市集合)和当前所在城市 i 的状压DP。
状压DP的状态设计与转移
状态 dp[state] 或 dp[state][i] 表示的含义需要根据具体问题设计:
• 单一状态变量 state:通常包含了解决整个子问题所需的所有“布尔”信息。转移时考虑所有能通过一个“动作”添加某些位或修改某些位达到的合法新状态 next_state。状态转移方程形如:
dp[next_state] = min/max/sum(dp[next_state], dp[state] + cost / ...)
或
dp[state] = f( dp[ sub_state1 ], dp[ sub_state2 ], ... ) (某些子集划分问题)。
• 状态 state + 附加信息 i:当 state 本身不足以唯一确定转移到下一个状态的代价或规则时,需要增加维度 i。最常见的就是在 TSP 或 Hamilton 路径中,dp[state][i] 表示访问完 state 中的城市后,最后停留在城市 i 时的最优值。转移时枚举下一个要去的城市 j (需要 j 不在 state 中),计算 state 中状态加上 j 后的新状态 state_new,状态转移方程形如:
dp[state_new][j] = min/max/sum( dp[state_new][j], dp[state][i] + w[i][j] / ... )
状态转移中的位运算操作:这是状压DP与普通DP最鲜明的区别。
• 检查状态 state 中第 i 位是否为 1:(state >> i) & 1 == 1
• 检查状态 state 中第 i 位是否为 0:((state >> i) & 1) == 0 (或者有时可用 !(state & (1 << i)))
• 设置状态 state 的第 i 位为 1:state | (1 << i)
• 设置状态 state 的第 i 位为 0:state & ~(1 << i)
• 判断状态 A 是否是状态 B 的子集:(A & B) == A
• 判断状态 A 和 B 是否有交集:(A & B) != 0
• 枚举状态 state 的所有子集:
sub = state
while sub:
# sub 是 state 的一个子集
...
sub = (sub - 1) & state
• 枚举所有大小为 k 的子集:可以使用 Gosper's Hack 等位操作技巧(当 n 较小时非常高效)。
• 判断状态自身是否满足某些约束:例如,检查 state 的二进制位中是否没有两个相邻的 1:(state & (state >> 1)) == 0。检查一行的放置状态是否满足列条件 mask:(state & mask) == state 或 (state | ~mask) == ~mask? (取决于 mask 的定义,通常是合法位置为 1)。
重点!!!!!!!!!!!!
理解 “为什么这个状态信息能用位表示?它压缩了什么?转移时如何操作位来对应状态的变化?”
学习经验: 从经典例题入手反复琢磨:比如 n 个点的哈密顿路径计数(最简单模板)、n x n 棋盘放置国王(不能攻击)、n x m 网格放置骨牌(多米诺/铺砖)、TSP。把每道题的状态设计(dp数组含义)、有效状态判断、状态转移方程(特别是位运算部分)、边界初始化、答案提取彻底搞明白。自己独立写几遍。
模拟小样例:选 n=3, 4 这样的极小规模,手动计算 dp 数组的值,验证自己的状态表示和转移逻辑是否正确。打印程序的 dp 表与手动计算对比。
思考状态设计的通用性:状压DP的状态本质是一个集合(位图表示的集合),它记录的是哪些元素(位置/点)已经被“处理/覆盖/选择/访问”。再往上叠加的维度(如 dp[state][i] 里的 i)通常是关于“当前处理到的位置”、“最后处理的一个元素”之类的信息。识别题目中的“布尔约束”:在看到规模小的题目时,主动思考:“这个问题的主要约束/状态是否可以分解成若干个只能取 0/1 的小部分?这些部分的组合数量是否大约是 2^N(N很小)?”。

浙公网安备 33010602011771号