【算法】状压压缩DP问题整理

状压dp

全排列型状压

朴素 - 全排列状压

1879. 两个数组最小的异或值之和 - 力扣(LeetCode)

题目描述:

给你两个整数数组 nums1nums2 ,它们长度都为 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[i1][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)

526. 优美的排列 - 力扣(LeetCode)

题目描述:

假设有从 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]

2741. 特别的排列 - 力扣(LeetCode)

题目描述:

给你一个下标从 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(sj, j), valid(j)。复杂度: O ( n 2 ⋅ 2 n ) O(n^2\cdot 2^n) O(n22n)

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(i1,ssub), G(sub)))

时间复杂度: O ( n ⋅ 3 n ) O(n\cdot 3^n) O(n3n)。由于元素个数为 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=0nCniaibni,所以 ∑ 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(n3n)

2305. 公平分发饼干 - 力扣(LeetCode)

题目描述:

给你一个整数数组 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(i1,ssub), sub)}。最终答案为 f ( k , 1 f(k,1 f(k,1<< n − 1 ) n-1) n1),初始值 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+n2n)

    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 个小时,然后休息一会儿。

你需要按照如下条件完成给定任务:

  • 如果你在某一个时间段开始一个任务,你需要在 同一个 时间段完成它。
  • 完成一个任务后,你可以 立马 开始一个新的任务。
  • 你可以按 任意顺序 完成任务。

给你 taskssessionTime ,请你按照上述要求,返回完成所有任务所需要的 最少 数目的 工作时间段

测试数据保证 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(ssub)+1}, subk。时间复杂度: O ( 3 n + n ⋅ 2 n ) O(3^n+n\cdot 2^n) O(3n+n2n)

    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[snums[j]]。这样可以大大减少重复。

时间复杂度:不超过 O ( n ⋅ 2 n ) O(n \cdot 2^n) O(n2n)

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 计数器实现状压的代替品。

691. 贴纸拼词 - 力扣(LeetCode)

题目描述:

我们有 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(nm) 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)

题目描述:

给你两个整数 mn 。构造一个 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(i1, 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 

其他

291. 蒙德里安的梦想 - AcWing题库

题目描述:
求把  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(i1,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(i1,k))

对于所有 M M M 位二进制数,预处理其是否满足所有连续0的个数是否是偶数。

image.png

    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]

最短哈密顿回路 / 旅行商问题

91. 最短Hamilton路径 - AcWing题库

题目描述:

给定一张 n n n个点的带权无向图,点从 0 ∼ n − 1 0\sim n-1 0n1标号,求起点0到终点 n − 1 n-1 n1的最短 Hamilton 路径
Hamilton 路径的定义是从0到 n − 1 n-1 n1不重不漏地经过每个点恰好一次。

哈密顿回路:无向带权图中经过所有顶点的回路。朴素做法对于 N N N 个顶点,时间复杂度为 O ( n ! ) O(n!) O(n!) ,是 N P − h a r d NP-hard NPhard 问题(无法在多项式时间复杂度内求解)。

实际上,设已经访问过的点集 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(Sj,k)+w(k, j) , kSj}。可以用二进制来压缩已经访问的点集 S S S。最终问题 f ( 2 N − 1 , N − 1 ) f(2^N -1, N - 1 ) f(2N1,N1),初始值 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]

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值