状压dp
全排列型状压
朴素 - 全排列状压
1879. 两个数组最小的异或值之和 - 力扣(LeetCode)
题目描述:
给你两个整数数组 nums1
和 nums2
,它们长度都为 n
。
两个数组的 异或值之和 为 (nums1[0] XOR nums2[0]) + (nums1[1] XOR nums2[1]) + ... + (nums1[n - 1] XOR nums2[n - 1])
(下标从 0 开始)。
- 比方说,
[1,2,3]
和[3,2,1]
的 异或值之和 等于(1 XOR 3) + (2 XOR 2) + (3 XOR 1) = 2 + 0 + 2 = 4
。
请你将 nums2
中的元素重新排列,使得 异或值之和 最小 。
请你返回重新排列之后的 异或值之和 。
示例 1:
输入:nums1 = [1,2], nums2 = [2,3]
输出:2
解释:将 nums2 重新排列得到 [3,2] 。
异或值之和为 (1 XOR 3) + (2 XOR 2) = 2 + 0 = 2 。
示例 2:
输入:nums1 = [1,0,3], nums2 = [5,3,4]
输出:8
解释:将 nums2 重新排列得到 [5,4,3] 。
异或值之和为 (1 XOR 5) + (0 XOR 4) + (3 XOR 3) = 4 + 4 + 0 = 8 。
解答:
O ( n 2 × 2 n ) O(n^2 \times 2^n ) O(n2×2n) 做法: f ( i , s ) f(i, s) f(i,s) 表示考虑完成 n u m s [ 0 : i ] nums[0: i] nums[0:i] ,状态为 s s s 之下,最小异或值之和。
class Solution:
def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int:
n = len(nums2)
f = [[inf] * (1 << n) for _ in range(n + 1)]
for i in range(n + 1): f[i][0] = 0
for i in range(1, n + 1):
x = nums1[i - 1]
for s in range(1, 1 << n):
for j in range(n):
if (s >> j) & 1 == 0: continue
f[i][s] = min(f[i][s], f[i - 1][s ^ (1 << j)] + (x ^ nums2[j]))
return f[n][(1 << n) - 1]
优化:省略前一维度,这是因为 i i i 的信息隐含在 s s s 所含1的个数之中。时间复杂度 O ( n × 2 n ) O(n\times 2^n) O(n×2n)
class Solution:
def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int:
n = len(nums2)
f = [inf] * (1 << n)
f[0] = 0
for s in range(1, 1 << n):
x = nums1[s.bit_count() - 1]
for j in range(n):
if (s >> j) & 1 == 0: continue
f[s] = min(f[s], f[s ^ (1 << j)] + (x ^ nums2[j]))
return f[(1 << n) - 1]
约束型 - 全排列状压
对于带有约束的全排列问题, f [ i ] [ s ] f[i][s] f[i][s] 表示考虑完全排列 p [ 0 : i ] p[0: i] p[0:i] ,已经选择集合状态为 s s s 情况下的合法方案数。一般转移方程: f [ i ] [ s ] = ∑ f [ i − 1 ] [ s − { j } ] , ∀ v a l i d ( j ) f[i][s]=\sum f[i-1][s-\{j\}],~ \forall ~valid(j) f[i][s]=∑f[i−1][s−{j}], ∀ valid(j),初始化 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1。时间复杂度为 O ( n 2 × 2 n ) O(n^2 \times 2^n) O(n2×2n)。
优化思路:由于 s s s 中包含了 i i i 的信息,即 b i n ( s ) . c o u n t ( ′ 1 ′ ) bin(s).count('1') bin(s).count(′1′) ,所以第一维度可以省略。时间复杂度 O ( n × 2 n ) O(n\times 2^n) O(n×2n)
题目描述:
假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm
(下标从 1 开始),只要满足下述条件 之一 ,该数组就是一个 优美的排列 :
perm[i]
能够被i
整除i
能够被perm[i]
整除
给你一个整数 n
,返回可以构造的 优美排列 的 数量 。
示例 1:
输入:n = 2
输出:2
解释:
第 1 个优美的排列是 [1,2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是 [2,1]:
- perm[1] = 2 能被 i = 1 整除
- i = 2 能被 perm[2] = 1 整除
示例 2:
输入:n = 1
输出:1
解答:
f [ i ] [ s ] 考虑完 p e r m [ 1 ] p e r m [ i ] ,已选择状态为 s f[i][s] 考虑完perm[1] ~ perm[i],已选择状态为s f[i][s]考虑完perm[1] perm[i],已选择状态为s
时间复杂度为 O ( n 2 × 2 n ) O(n^2 \times 2^n) O(n2×2n)。
class Solution:
def countArrangement(self, n: int) -> int:
res = 0
# f[i][s] 考虑完perm[1] ~ perm[i],已选择状态为s
m = (1 << n) - 1
f = [[0] * (m + 1) for _ in range(n + 1)]
f[0][0] = 1
for i in range(1, n + 1):
for s in range(m + 1):
for j in range(n):
if (s >> j) & 1 and ((j + 1) % i == 0 or i % (j + 1) == 0):
f[i][s] += f[i - 1][s ^ (1 << j)]
return f[n][m]
优化:省略前一维度。时间复杂度 O ( n × 2 n ) O(n\times 2^n) O(n×2n)
class Solution:
def countArrangement(self, n: int) -> int:
res = 0
# f[i][s] 考虑完perm[1] ~ perm[i],已选择状态为s
m = (1 << n) - 1
f = [1] + [0] * m
for s in range(m + 1):
i = bin(s).count('1')
for j in range(n):
if (s >> j) & 1 and ((j + 1) % i == 0 or i % (j + 1) == 0):
f[s] += f[s ^ (1 << j)]
return f[m]
题目描述:
给你一个下标从 0 开始的整数数组 nums
,它包含 n
个 互不相同 的正整数。如果 nums
的一个排列满足以下条件,我们称它是一个特别的排列:
- 对于
0 <= i < n - 1
的下标i
,要么nums[i] % nums[i+1] == 0
,要么nums[i+1] % nums[i] == 0
。
请你返回特别排列的总数目,由于答案可能很大,请将它对 109 + 7
取余 后返回。
示例 1:
输入:nums = [2,3,6]
输出:2
解释:[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。
示例 2:
输入:nums = [1,4,3]
输出:2
解释:[3,1,4] 和 [4,1,3] 是 nums 两个特别的排列。
解答:
f ( s , i ) f(s,i) f(s,i) 表示当前选择的状态为 s s s ,最后一个位置选择的元素为 n u m s [ i ] nums[i] nums[i] 。对所有在 s s s 中的 i i i ,考虑其所有可能的前一个位置的值 n u m s [ j ] nums[j] nums[j]。 f ( s , i ) = ∑ f ( s ⊕ j , j ) , ∀ valid ( j ) f(s,i)=\sum f(s\oplus j,~j),~\forall \text{valid}(j) f(s,i)=∑f(s⊕j, j), ∀valid(j)。复杂度: O ( n 2 ⋅ 2 n ) O(n^2\cdot 2^n) O(n2⋅2n)。
moder = 10 ** 9 + 7
class Solution:
def specialPerm(self, nums: List[int]) -> int:
n = len(nums)
f = [[0] * n for _ in range(1 << n)]
f[0][0] = 1
for s in range(1 << n):
for i in range(n):
if (s >> i) & 1 == 0: continue
if (s ^ (1 << i)) == 0:
f[s][i] = 1
continue
for j in range(n):
if i == j or (s >> j) & 1 == 0: continue
x, y = nums[i], nums[j]
if x % y == 0 or y % x == 0:
f[s][i] = (f[s][i] + f[s ^ (1 << i)][j]) % moder
res = 0
for i in range(n):
res = (res + f[s][i]) % moder
return res
划分成 k k k 个子集的问题
f ( i , s ) f(i,s) f(i,s) 表示划分到第 i i i 个子集,划分的状态为 s s s 情况下的某个值。 f ( i , s ) = F ( ( f ( i − 1 , s − s u b ) , G ( s u b ) ) ) f(i,s)=F((f(i-1,s-sub),~G(sub) )) f(i,s)=F((f(i−1,s−sub), G(sub)))。
时间复杂度: O ( n ⋅ 3 n ) O(n\cdot 3^n) O(n⋅3n)。由于元素个数为 i i i 的集合个数有 C ( n , i ) C(n,i) C(n,i)个,其子集个数为 2 i 2^i 2i ,根据二项式定理 ( a + b ) n = ∑ i = 0 n C n i a i b n − i (a + b) ^n = \sum_{i=0}^n C_n^ia^ib^{n-i} (a+b)n=∑i=0nCniaibn−i,所以 ∑ i = 0 n C ( n , i ) ⋅ 2 i = ( 2 + 1 ) n = 3 n \sum_{i=0}^{n} C(n,i)\cdot 2^i = (2+1)^n=3^n ∑i=0nC(n,i)⋅2i=(2+1)n=3n,每次需要 O ( n ) O(n) O(n) 时间计算 G G G的情况下,时间复杂度为 O ( n ⋅ 3 n ) O(n\cdot 3^n) O(n⋅3n)
题目描述:
给你一个整数数组 cookies
,其中 cookies[i]
表示在第 i
个零食包中的饼干数量。另给你一个整数 k
表示等待分发零食包的孩子数量,所有 零食包都需要分发。在同一个零食包中的所有饼干都必须分发给同一个孩子,不能分开。
分发的 不公平程度 定义为单个孩子在分发过程中能够获得饼干的最大总数。
返回所有分发的最小不公平程度。
示例 1:
输入:cookies = [8,15,10,20,8], k = 2
输出:31
解释:一种最优方案是 [8,15,8] 和 [10,20] 。
- 第 1 个孩子分到 [8,15,8] ,总计 8 + 15 + 8 = 31 块饼干。
- 第 2 个孩子分到 [10,20] ,总计 10 + 20 = 30 块饼干。
分发的不公平程度为 max(31,30) = 31 。
可以证明不存在不公平程度小于 31 的分发方案。
示例 2:
输入:cookies = [6,1,3,2,2,4,1,2], k = 3
输出:7
解释:一种最优方案是 [6,1]、[3,2,2] 和 [4,1,2] 。
- 第 1 个孩子分到 [6,1] ,总计 6 + 1 = 7 块饼干。
- 第 2 个孩子分到 [3,2,2] ,总计 3 + 2 + 2 = 7 块饼干。
- 第 3 个孩子分到 [4,1,2] ,总计 4 + 1 + 2 = 7 块饼干。
分发的不公平程度为 max(7,7,7) = 7 。
可以证明不存在不公平程度小于 7 的分发方案。
解答:
最小化 k k k 个子集和中的最大值问题。 f ( i , s ) f(i,s) f(i,s) 表示划分到第 i i i 个子集,划分的状态为 s s s 情况下的 i i i 个子集中和最大值的最小值。
考虑 s s s 的所有子集 s u b sub sub , f ( i , s ) = min { max ( f ( i − 1 , s − s u b ) , ∑ s u b ) } f(i,s)=\min \{ \max(f(i-1,s-sub),~\sum sub)\} f(i,s)=min{max(f(i−1,s−sub), ∑sub)}。最终答案为 f ( k , 1 f(k,1 f(k,1<< n − 1 ) n-1) n−1),初始值 f ( 0 , 0 ) = 0 f(0,0)=0 f(0,0)=0。
def distributeCookies(self, cookies: List[int], k: int) -> int:
# f[i][s] 表示当前划分状态为s,s为1表示已经分配
# 划分完第i 个集合,所有集合的最大值 的最小值
# f[i][s] = min(max(f[i - 1][s ^ sub], sum(sub)))
# f[k][1 << n - 1]
n = len(cookies)
f = [[inf] * (1 << n) for _ in range(k + 1)]
f[0][0] = 0
for i in range(1, k + 1):
for s in range(1, 1 << n):
sub = s
while sub:
tot = sum(cookies[j] for j in range(n) if (sub >> j) & 1)
f[i][s] = min(f[i][s], max(f[i - 1][s ^ sub], tot))
sub = (sub - 1) & s
return f[k][(1 << n) - 1]
1723. 完成所有工作的最短时间 - 力扣(LeetCode)
题目描述:
给你一个整数数组 jobs
,其中 jobs[i]
是完成第 i
项工作要花费的时间。
请你将这些工作分配给 k
位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。
返回分配方案中尽可能 最小 的 最大工作时间 。
示例 1:
输入:jobs = [3,2,3], k = 3
输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。
示例 2:
输入:jobs = [1,2,4,7,8], k = 2
输出:11
解释:按下述方式分配工作:
1 号工人:1、2、8(工作时间 = 1 + 2 + 8 = 11)
2 号工人:4、7(工作时间 = 4 + 7 = 11)
最大工作时间是 11 。
解答:
此题是上一题的数据增强版,优化方法:预处理所有子集的和 + 一维滚动状压dp。复杂度: O ( 3 n + n ⋅ 2 n ) O(3^n+ n\cdot 2^n) O(3n+n⋅2n)
def minimumTimeRequired(self, nums: List[int], k: int) -> int:
n = len(nums)
f = [inf] * (1 << n)
f[0] = 0
sum_ = defaultdict(int)
for i, x in enumerate(nums):
for s in range(1 << i):
sum_[(1 << i) | s] = sum_[s] + x
for _ in range(1, k + 1):
for s in range((1 << n) - 1, 0, -1):
sub = s
while sub:
tot = sum_[sub]
if f[s ^ sub] > tot: tot = f[s ^ sub]
if f[s] > tot: f[s] = tot
sub = (sub - 1) & s
return f[(1 << n) - 1]
划分集合每个和不超过 k k k 的最少划分数
1986. 完成任务的最少工作时间段 - 力扣(LeetCode)
题目描述:
你被安排了 n
个任务。任务需要花费的时间用长度为 n
的整数数组 tasks
表示,第 i
个任务需要花费 tasks[i]
小时完成。一个 工作时间段 中,你可以 至多 连续工作 sessionTime
个小时,然后休息一会儿。
你需要按照如下条件完成给定任务:
- 如果你在某一个时间段开始一个任务,你需要在 同一个 时间段完成它。
- 完成一个任务后,你可以 立马 开始一个新的任务。
- 你可以按 任意顺序 完成任务。
给你 tasks
和 sessionTime
,请你按照上述要求,返回完成所有任务所需要的 最少 数目的 工作时间段 。
测试数据保证 sessionTime
大于等于 tasks[i]
中的 最大值 。
示例 1:
输入:tasks = [1,2,3], sessionTime = 3
输出:2
解释:你可以在两个工作时间段内完成所有任务。
- 第一个工作时间段:完成第一和第二个任务,花费 1 + 2 = 3 小时。
- 第二个工作时间段:完成第三个任务,花费 3 小时。
示例 2:
输入:tasks = [3,1,3,1,1], sessionTime = 8
输出:2
解释:你可以在两个工作时间段内完成所有任务。
- 第一个工作时间段:完成除了最后一个任务以外的所有任务,花费 3 + 1 + 3 + 1 = 8 小时。
- 第二个工作时间段,完成最后一个任务,花费 1 小时。
示例 3:
输入:tasks = [1,2,3,4,5], sessionTime = 15
输出:1
解释:你可以在一个工作时间段以内完成所有任务。
解答:
f ( s ) f(s) f(s) 表示到达这个状态需要的最少划分段数。枚举每个子集 s u b sub sub , f ( s ) = min { f ( s − s u b ) + 1 } , ∀ ∑ s u b ≤ k f(s)=\min \{f(s-sub)+1\},~\forall \sum sub\le k f(s)=min{f(s−sub)+1}, ∀∑sub≤k。时间复杂度: O ( 3 n + n ⋅ 2 n ) O(3^n+n\cdot 2^n) O(3n+n⋅2n)
def minSessions(self, nums: List[int], k: int) -> int:
n = len(nums)
f = [inf] * (1 << n)
f[0] = 0
# 预处理所有子集的和
sum_ = defaultdict(int)
for i, x in enumerate(nums):
for sub in range(1 << i):
sum_[sub | (1 << i)] = sum_[sub] + x
for s in range(1, 1 << n):
sub = s
while sub:
if sum_[sub] <= k:
f[s] = min(f[s], f[s ^ sub] + 1)
sub = (sub - 1) & s
return f[(1 << n) - 1]
集合是否能划分成 k 个相等子集
698. 划分为k个相等的子集 - 力扣(LeetCode)
**题目描述:**给定一个整数数组 nums
和一个正整数 k
,找出是否有可能把这个数组分成 k
个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
示例 2:
输入: nums = [1,2,3,4], k = 3
输出: false
解答:
f [ s ] f[s] f[s] 为在压缩状态 s s s 下的余数。考察每一个在集合中的元素 n u m s [ j ] nums[j] nums[j],对于删去其的集合 l s = f [ s ⊕ ( 1 ls=f[s \oplus(1 ls=f[s⊕(1<< j ) ] j)] j)],当且仅当 l s + n u m s [ j ] ≤ s i z ls+nums[j] \le siz ls+nums[j]≤siz 的时候可以更新 f [ s ] f[s] f[s],相当于枚举所有删去一个元素的子集向 f [ s ] f[s] f[s] 转移,能否构造出整数倍的集合。
这种方法会有一定的重复,不妨反过来,对于 f [ s ] f[s] f[s] ,考察其没有出现的每一个元素 n u m s [ j ] nums[j] nums[j],更新 f [ s ∣ n u m s [ j ] ] f[s | nums[j]] f[s∣nums[j]]。这样可以大大减少重复。
时间复杂度:不超过 O ( n ⋅ 2 n ) O(n \cdot 2^n) O(n⋅2n)
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
siz = sum(nums) // k
if sum(nums) % k != 0 or any(x > siz for x in nums): return False
# f[s] 表示 在选择状态为s的情况下,余数是多少
n = len(nums)
m = (1 << n) - 1
f = [0] + [-inf] * m
for s in range(m):
if f[s] == -inf: continue
for j in range(n):
if (s >> j) & 1 == 0:
nx = s | (1 << j)
if f[nx] == 0: continue
if f[s] + nums[j] <= siz:
f[nx] = (f[s] + nums[j]) % siz
return f[m] == 0
多重状压:记忆化搜索
当某些字符、数字可以使用 若干次时,传统的状压不方便表示使用状况。因此可以转用 d f s dfs dfs 的记忆化搜索方式,配合 C o u n t e r Counter Counter 计数器实现状压的代替品。
题目描述:
我们有 n
种不同的贴纸。每个贴纸上都有一个小写的英文单词。
您想要拼写出给定的字符串 target
,方法是从收集的贴纸中切割单个字母并重新排列它们。如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。
返回你需要拼出 target
的最小贴纸数量。如果任务不可能,则返回 -1
。
**注意:**在所有的测试用例中,所有的单词都是从 1000
个最常见的美国英语单词中随机选择的,并且 target
被选择为两个随机单词的连接。
示例 1:
输入: stickers = ["with","example","science"], target = "thehat"
输出:3
解释:
我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。
把贴纸上的字母剪下来并重新排列后,就可以形成目标 “thehat“ 了。
此外,这是形成目标字符串所需的最小贴纸数量。
示例 2:
输入:stickers = ["notice","possible"], target = "basicbasic"
输出:-1
解释:我们不能通过剪切给定贴纸的字母来形成目标“basicbasic”。
解答:
其中剪枝部分,要求 s [ 0 ] s[0] s[0] 一定在 w o r d word word中出现。这是因为如果当前 w o r d word word 中存在能消除 s [ 0 ] s[0] s[0] 的,那么最终解一定至少包含这些部分。否则,没有能消除 s [ 0 ] s[0] s[0] 的,表示当前 d f s ( s ) dfs(s) dfs(s) 的结果不合法,返回 i n f inf inf。这样实际上约束了每次一定转移到最优的方案。
例如对于 s = t h e s=the s=the, 首先寻找所有包含 t t t 的方案并向其转移。时间复杂度接近 O ( n ⋅ m ) O(n\cdot m) O(n⋅m) , n n n 为原始字符串大小, m m m 为可以考虑的字符串数量。
def minStickers(self, words: List[str], target: str) -> int:
words = [Counter(word) for word in words]
# dfs(s) 表示得到s 的最少数量
@lru_cache(None)
def dfs(s):
if s == '': return 0
cs = Counter(s)
res = inf
for word in words:
# 如果word压根无法消除s[0] 可以直接跳过
# 因为再怎么使用也无法完全消除s
# 应该首先考虑将s[0] 能消除的方案
if s[0] not in word: continue
ns = s
for k, v in word.items():
ns = ns.replace(k, '', v)
res = min(res, dfs(ns) + 1)
return res
res = dfs(target)
return res if res < inf else -1
k k k 进制状压
k k k 种颜色染色 n × m n \times m n×m 网格(不允许出现空着的格子)问题
1931. 用三种不同颜色为网格涂色 - 力扣(LeetCode)
题目描述:
给你两个整数 m
和 n
。构造一个 m x n
的网格,其中每个单元格最开始是白色。请你用 红、绿、蓝 三种颜色为每个单元格涂色。所有单元格都需要被涂色。
涂色方案需要满足:不存在相邻两个单元格颜色相同的情况 。返回网格涂色的方法数。因为答案可能非常大, 返回 对 109 + 7
取余 的结果。
解答:
k k k 进制预处理 + 合法状态预处理 + 枚举状压
每一行使用长度为 m m m 的 k k k 进制的串来表示。通过预处理的方式记录在 c o l o r color color 中,键为 k k k 进制的串对应的十进制数,值为对应的 k k k 进制串的列表。相邻两行的约束,通过枚举来预处理。转移方程: f ( i , s ) = ∑ f ( i − 1 , e [ s ] ) f(i,s)= \sum f(i-1, ~e[s]) f(i,s)=∑f(i−1, e[s])
时间复杂度: O ( k 2 m × n ) O(k^{2m}\times n) O(k2m×n)
moder = 10 ** 9 + 7
class Solution:
def colorTheGrid(self, m: int, n: int) -> int:
# 三进制表示每一行的颜色
colors = {}
for b in range(3 ** m):
color = []
x = b
while x:
color.append(x % 3)
x //= 3
color.extend([0] * (m - len(color)))
if any(color[i] == color[i + 1] for i in range(len(color) - 1)):
continue
colors[b] = color[::-1]
e = defaultdict(list)
# 预处理每一种状态可以邻接的状态
for i, u in colors.items():
for j, v in colors.items():
flag = True
for b in range(m):
if u[b] == v[b]:
flag = False
break
if flag: e[i].append(j)
# f[i][s] 表示i行为s的方案数
f = [[0] * (3 ** m) for _ in range(n)]
for b in colors.keys():
f[0][b] = 1
for i in range(1, n):
for s in colors.keys():
for ps in e[s]:
f[i][s] = (f[i - 1][ps] + f[i][s]) % moder
return (sum(f[n - 1])) % moder
其他
题目描述:
求把
N
×
M
的棋盘分割成若干个
1
×
2
的长方形,有多少种方案。
例如当
N
=
2
,
M
=
4
时,共有
5
种方案。当
N
=
2
,
M
=
3
时,共有
3
种方案。
\begin{aligned}&\text{求把 }N\times M\text{ 的棋盘分割成若干个 }1\times2\text{ 的长方形,有多少种方案。}\\&\text{例如当 }N=2\text{,}M=4\text{ 时,共有 }5\text{ 种方案。当 }N=2\text{,}M=3\text{ 时,共有 }3\text{ 种方案。}\end{aligned}
求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
竖方块摆放确定时,横方块摆放一定确定(合法或者恰好填充),所以只需要看竖方块的摆放情况。对于$N\times M $ 的方格, f ( i , j ) 第 i 行形态为 j 时,前 i 行合法切割方案数。 f(i, j) 第~i~行形态为~j时,前~i~行合法切割方案数。 f(i,j)第 i 行形态为 j时,前 i 行合法切割方案数。 j j j 是用十进制记录 M M M 位二进制数,其每位数字为1 表示放竖方块上半部分,为0 表示其他情况。(竖方块下半部分 / 横方块)
f ( i , j ) f(i,j) f(i,j) 能由 f ( i − 1 , k ) f(i-1, k) f(i−1,k) 状态转移的充要条件:1. j & k = 0 j ~\&~ k =0 j & k=0 ,保证同列上下两行不会同时放竖方块的上半部分。2. j ∣ k j ~|~ k j ∣ k 的所有连续的 0 的个数必须是偶数。 j ∣ k j ~|~ k j ∣ k 为 0 当且仅当上下两行都是横方块,所以必须是偶数个。
初始状态对于 f ( 0 ) f(0) f(0) ,不能对第一行产生影响,所以只有 f ( 0 , 0 ) = 1 f(0,~0) = 1 f(0, 0)=1 ,其余为0。最终目标: f ( N , 0 ) f(N, 0) f(N,0) ,状态转移方程: f ( i , j ) = ∑ v a l i d ( f ( i − 1 , k ) ) f(i,j) = \sum valid(f(i-1,k)) f(i,j)=∑valid(f(i−1,k))
对于所有 M M M 位二进制数,预处理其是否满足所有连续0的个数是否是偶数。
N = M = 11
f = [[0] * (1 << M + 1) for _ in range(N + 1)]
def solve(n, m):
# f[n][1 << m]
# 预处理,判断 i 是否含有连续的奇数个0
s = set()
for i in range(1 << m):
c = 0
for j in range(m):
if i >> j & 1:
if c & 1: break
else: c += 1
if c & 1: s.add(i)
f[0][0] = 1
for i in range(1, n + 1):
for j in range(1 << m):
f[i][j] = 0
for k in range(1 << m):
if (j & k == 0 and (j | k not in s)):
f[i][j] += f[i - 1][k]
return f[n][0]
最短哈密顿回路 / 旅行商问题
题目描述:
给定一张
n
n
n个点的带权无向图,点从
0
∼
n
−
1
0\sim n-1
0∼n−1标号,求起点0到终点
n
−
1
n-1
n−1的最短 Hamilton 路径
Hamilton 路径的定义是从0到
n
−
1
n-1
n−1不重不漏地经过每个点恰好一次。
哈密顿回路:无向带权图中经过所有顶点的回路。朴素做法对于 N N N 个顶点,时间复杂度为 O ( n ! ) O(n!) O(n!) ,是 N P − h a r d NP-hard NP−hard 问题(无法在多项式时间复杂度内求解)。
实际上,设已经访问过的点集 S S S,当前节点 j j j ,设 f ( S , j ) f(S,j) f(S,j) 表示路径已经访问过点集 S S S 中的点且当前访问的 j j j 时 的最短路径。有状态转移: f ( S , j ) = min { f ( S − j , k ) + w ( k , j ) , ∀ k ∈ S − j } f(S,j) = \min \{f(S-j, k) + w(k,~j) ~, \forall~k \in S-j \} f(S,j)=min{f(S−j,k)+w(k, j) ,∀ k∈S−j}。可以用二进制来压缩已经访问的点集 S S S。最终问题 f ( 2 N − 1 , N − 1 ) f(2^N -1, N - 1 ) f(2N−1,N−1),初始值 f ( 0 , 0 ) = 0 f(0, 0) = 0 f(0,0)=0。
def solve():
n = int(input())
f = [[inf] * n for _ in range(1 << n)]
w = []
for _ in range(n):
w.append(list(map(int, input().split())))
f[1][0] = 0
for S in range(1, 1 << n):
for j in range(n):
if (S >> j) & 1: # j 在S 中,
for k in range(n):
if ((S ^ (1 << j)) >> k) & 1: # 且 k 在 S - j 中
f[S][j] = min(f[S ^ (1 << j)][k] + w[k][j], f[S][j])
return f[(1 << n) - 1][n - 1]