Leetcode刷题笔记——剑指offer II (二)【动态规划】

动态规划

状态定义非常重要!!!,这只能靠积累

  1. 先定义,
  2. 再想base 边界、
  3. 最后想动态转移
  • 基础DP
  • 背包DP
  • 树型DP

基础DP

一维dp

剑指 Offer II 088. 爬楼梯的最少成本

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。

请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例 1:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
 示例 2:

输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。
 

提示:

2 <= cost.length <= 1000
0 <= cost[i] <= 999

一维DP:
注意本题要跳到第n层,即超出了cost数组的范围,由于第n层没有花销,因此我们最后只需要返回第n-1层与第n-2层花销,二者之中花销最小的即可

class Solution {
public:
	int minCostClimbingStairs(vector<int>& cost) {
		int n = cost.size();
		if (n == 1) return cost[0];
		int dp0 = 0, dp1 = cost[0], dp2 = cost[1];
		for (int i = 2; i < n; i++) {
			dp0 = dp1;
			dp1 = dp2;
			dp2 = min(dp1, dp0) + cost[i];
		}
		return min(dp1, dp2);
	}
};
d p [ i ] dp[i] dp[i]:第 i i i 个位置时)剑指 Offer II 089. 房屋偷盗

一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:nums = [1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

输入:nums = [2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
 

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 400

d p [ i ] dp[i] dp[i]定义:第 i i i 个位置时,能取得的最大收益。
因此, d p [ i ] = m a x ( d p [ i − 1 ] , d p [ i − 2 ] + n u m s [ i ] ) dp[i] = max(dp[i-1], dp[i-2]+nums[i]) dp[i]=max(dp[i1],dp[i2]+nums[i])

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n<2: return nums[0]
        
        dp = [0] * len(nums)
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, len(nums), 1):
            dp[i] = max(dp[i-1], dp[i-2]+nums[i])
        return max(dp[n-1], dp[n-2])
剑指 Offer II 090. 环形房屋偷盗

一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:

输入:nums = [0]
输出:0
 

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 1000

本题的 h e l p help help 函数,与上一题一样。
该题属于上题变体,只要判断 h e l p ( n u m s [ 0 : l e n ( n u m s ) − 1 ] ) help(nums[0:len(nums)-1]) help(nums[0:len(nums)1]),与 h e l p ( n u m s [ 1 : l e n ( n u m s ) ] ) ) help(nums[1: len(nums)])) help(nums[1:len(nums)])) 谁更大即可

class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) < 2: return nums[0]
        def help(n1:list)->int:
            n = len(n1)
            if n<2: return n1[0]
            
            dp = [0] * len(n1)
            dp[0] = n1[0]
            dp[1] = max(n1[0], n1[1])
            for i in range(2, len(n1), 1):
                dp[i] = max(dp[i-1], dp[i-2]+n1[i])
            return max(dp[n-1], dp[n-2])

        return max(help(nums[:len(nums)-1]), help(nums[1: len(nums)])) 
467. 环绕字符串中唯一的子字符串

给定字符串s=“abcdefghijklmnopqrstuvwxyzabcd…”

输入一个字符串 p,输出 p 的子串的数量,满足条件
1,子串同时是 s 的子串
2,重复的子串不计数
3,子串在 p 中是连续的
4,子串在 s 中是连续的
5,子串是非空的

注意:p 不是无限循环字符串的根

举例
输入 p="a",p 的子串有 "", "a"
满足条件的子串的数量是 1

输入 p="cac",p的子串有 "", "c", "a", "ca", "ac", "cac"
满足条件的子串的数量是 2,也就是说 "ca"、"ac"、"cac"不是 s 的子串

输入 p="zab",p的子串有 "", "z", "a", "b", "za", "ab", "zab"
满足条件的子串的数量是 6,也就是说 "z", "a", "b", "za", "ab", "zab" 都是 s 的子串

提示:

1 <= p.length <= 105
p 由小写英文字母构成

官解:DP
由于 s s s 是周期字符串,对于在 s s s 中的子串,只要知道子串的第一个字符(或最后一个字符)和子串长度,就能确定这个子串。例如子串以 ‘d’ \text{`d'} ‘d’ 结尾,长度为 3 3 3,那么该子串为 “bcd” \text{``bcd''} “bcd”

题目要求不同的子串个数,那么对于两个以同一个字符结尾的子串,长的那个子串必然包含短的那个。例如 “abcd” \text{``abcd''} “abcd” “bcd” \text{``bcd''} “bcd” 均以 ‘d’ \text{`d'} ‘d’ 结尾, “bcd” \text{``bcd''} “bcd” “abcd” \text{``abcd''} “abcd” 的子串。

据此,我们可以定义 $ dp [ α ] \textit{dp}[\alpha] dp[α] 表示 p p p 中以字符 α \alpha α 结尾且在 s s s 中的子串的最长长度,知道了最长长度,也就知道了不同的子串的个数。

如何计算 dp [ α ] \textit{dp}[\alpha] dp[α] 呢?我们可以在遍历 p p p 时,维护连续递增的子串长度 k k k。具体来说,遍历到 p [ i ] p[i] p[i] 时,如果 p [ i ] p[i] p[i] p [ i − 1 ] p[i-1] p[i1] 在字母表中的下一个字母,则将 k k k 加一,否则将 k k k 置为 1 1 1,表示重新开始计算连续递增的子串长度。然后,用 k k k 更新 dp [ p [ i ] ] \textit{dp}[p[i]] dp[p[i]] 的最大值。

【关键!!!!】遍历结束后, p p p 中以字符 c c c 结尾且在 s s s 中的子串有 dp [ c ] \textit{dp}[c] dp[c]。例如 dp [ ‘d’ ] = 3 \textit{dp}[\text{`d'}]=3 dp[‘d’]=3 表示子串 “bcd” \text{``bcd''} “bcd” “cd” \text{``cd''} “cd” “d” \text{``d''} “d”

最后答案为
∑ α = ‘a’ ‘z’ dp [ α ] \sum_{\alpha=\text{`a'}}^{\text{`z'}}\textit{dp}[\alpha] α=‘a’‘z’dp[α]

我的方法:找出以每个单词做结尾的最大长度
dp[i]记录以i位置为结尾的最长连续字符串长度,要么是1,要么是dp[i-1]+1。在动规的过程中,用一个长度为26的int数组记录以对应字母为结尾的最长连续字符串的长度。最后把这26个值加起来即可

  1. 字符之差为 1 或 -25。(p[i] - p[i - 1] + 26) % 26 == 1
  2. 如何去掉可能重复的子串,保留最长的 freq[p[i]] = max(freq[p[i]], dp);
    • 例如: abcd的字串对应的每个字符的最大长度为[1,2,3,4],因此子串个数为:1+2+3+4 = 10个,其中的含义是:{a}, {ab, b}, {abc, bc, c}, {abcd, bcd, cd, d}
inline bool fsishelper(char& a, char& b) { return b - a == 1 || b - a == -25; }
int findSubstringInWraproundString(string p) {
	int n = p.size();
	int ans = 0;
	int freq[256] = {}; // 记录每个字母的为结尾的最大长度
	int dp = 1; // 记录当前连续长度
	freq[p[0]] = 1;
	for (int i = 1; i < n; i++) {
		if (fsishelper(p[i - 1], p[i])) dp = dp + 1;
		else dp = 1;
		freq[p[i]] = max(freq[p[i]], dp);
	}
	for (int i = 0; i < 26; i++) ans += freq[i+'a'];
	return ans;
}
(dp[i]:以 i 结尾的符合条件数)32. 最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:

输入:s = ""
输出:0
 

提示:

0 <= s.length <= 3 * 104
s[i] 为 '(' 或 ')'

官解
定义 dp [ i ] \color{red}\textit{dp}[i] dp[i] 表示以下标 i i i 字符结尾的最长有效括号的长度。

为何如此定义,主要是由于 条件2,我们需要通过 dp 获得 当前位置的情况

    1. ‘(’ \text{‘(’} ‘(’ 结尾的子串对应的 dp \textit{dp} dp 值必定为 0 0 0
    1. s [ i ] = ‘)’ s[i] = \text{‘)’} s[i]=‘)’
    • s [ i − 1 ] = ‘(’ s[i - 1] = \text{‘(’} s[i1]=‘(’,也就是字符串形如 “ … … ( ) ” “……()” ……(),我们可以推出:
      d p [ i ] = d p [ i − 2 ] + 2 dp[i]=dp[i−2]+2 dp[i]=dp[i2]+2
    • s [ i − 1 ] = ‘)’ s[i - 1] = \text{‘)’} s[i1]=‘)’,也就是字符串形如 “ … … ) ) ” “……))” ……)),我们可以推出:
      d p [ i ] = d p [ i − 1 ] + d p [ i − d p [ i − 1 ] − 2 ] + 2 dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2 dp[i]=dp[i1]+dp[idp[i1]2]+2
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        if n < 2: return 0
        dp = [0] * n
        dp[1] = 2 if s[:2] == "()" else 0
        for i in range(2, n):
            if s[i] == '(':
                dp[i] = 0
            else:
                if s[i - 1] == '(':
                    dp[i] = dp[i - 2] + 2
                else:
                    if i - dp[i - 1] >= 1 and s[i - dp[i - 1] - 1] == '(':
                        dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2]
        return max(dp)
(dp[i]: 以 i 结尾的)符合条件数)940. 不同的子序列 II

给定一个字符串 s,计算 s 的 不同非空子序列 的个数。因为结果可能很大,所以返回答案需要对 10^9 + 7 取余 。

字符串的 子序列 是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个新字符串。

例如,“ace” 是 “abcde” 的一个子序列,但 “aec” 不是。

示例 1:

输入:s = "abc"
输出:7
解释:7 个不同的子序列分别是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"。
示例 2:

输入:s = "aba"
输出:6
解释:6 个不同的子序列分别是 "a", "b", "ab", "ba", "aa" 以及 "aba"。
示例 3:

输入:s = "aaa"
输出:3
解释:3 个不同的子序列分别是 "a", "aa" 以及 "aaa"。
 

提示:

1 <= s.length <= 2000
s 仅由小写英文字母组成

方法一:二叉树回溯
当前位置选,或者不选。

    def distinctSubseqII(self, s: str) -> int:
        ans = set()
        def backtrace(cur: int, strings: str):
            if strings!="":
                ans.add(strings)
            if cur==len(s):
                return
            backtrace(cur+1, strings+s[cur])
            backtrace(cur+1, strings)
        backtrace(0, "")
        return len(ans)

复杂度 O ( 2 n ) O(2^n) O(2n)
注意到 s的长度最大到2000,则必然超时,下面用dp做

方法二:dp
定义:dp[i] 表示 以 i 结尾的,符合条件数

	def distinctSubseqII(self, s: str) -> int:
        MOD = int(1e9+7)
        dp = [0]*len(s)
        dp[0] = 1
        for i in range(1, len(s)):
            for j in range(i-1, -1, -1):
                dp[i] += dp[j]
                if s[i]==s[j]:
                    break
            else:
                dp[i] += 1
        return sum(dp)%MOD
  • 复杂度 O ( n ∣ Σ ∣ ) O(n∣Σ∣) O(nΣ):其中 n n n 是字符串 s s s 的长度, Σ \Sigma Σ 是字符集,在本题中 ∣ Σ ∣ = 26 |\Sigma|=26 ∣Σ∣=26

二维dp

(一维+有限状态)剑指 Offer II 091. 粉刷房子

假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。

当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。

例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。

请计算出粉刷完所有房子最少的花费成本。

示例 1:

输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
     最少花费: 2 + 5 + 3 = 10。
示例 2:

输入: costs = [[7,6,2]]
输出: 2
 

提示:

costs.length == n
costs[i].length == 3
1 <= n <= 100
1 <= costs[i][j] <= 20

我的方法:二维dp=一维+3个状态
d p [ i ] [ j ] dp[i][j] dp[i][j] 定义:当前位置的最优值
动态转移方程:
d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ ( j − 1 ) % 3 ] , d p [ i − 1 ] [ ( j − 2 ) % 3 ] ) + c o s t s [ i ] [ j ] dp[i][j] = min(dp[i-1][(j-1)\%3], dp[i-1][(j-2)\%3]) + costs[i][j] dp[i][j]=min(dp[i1][(j1)%3],dp[i1][(j2)%3])+costs[i][j]
其中 (j-1)%3(j-2)%3 保证了相邻的房子颜色不同

class Solution:
    def minCost(self, costs: List[List[int]]) -> int:
        m, n = len(costs), len(costs[-1])
        dp = [[0 for _ in range(n)] for i in range(m)]
        for i in range(3):
            dp[0][i] = costs[0][i]
        for i in range(1, m):
            for j in range(3):
                dp[i][j] = min(dp[i-1][(j-1)%3], dp[i-1][(j-2)%3]) + costs[i][j]
        return min(dp[m-1])
    int minCost(vector<vector<int>>& costs) {
        int m = costs.size(), n = costs.back().size();
        int dp[100][3] = {};
        for (int i=0; i<n; i++) dp[0][i] = costs[0][i];
        for (int i=1; i<m; i++){
            for(int j = 0; j<n; j++){
                dp[i][j] = min(dp[i-1][(j+1)%3], dp[i-1][(j+2)%3]) + costs[i][j];
            }
        }
        return min(dp[m-1][0], min(dp[m-1][1], dp[m-1][2]));
    }
(一维+有限状态) 剑指 Offer II 092. 翻转字符

如果一个由 ‘0’ 和 ‘1’ 组成的字符串,是以一些 ‘0’(可能没有 ‘0’)后面跟着一些 ‘1’(也可能没有 ‘1’)的形式组成的,那么该字符串是 单调递增 的。

我们给出一个由字符 ‘0’ 和 ‘1’ 组成的字符串 s,我们可以将任何 ‘0’ 翻转为 ‘1’ 或者将 ‘1’ 翻转为 ‘0’。

返回使 s 单调递增 的最小翻转次数。

示例 1:

输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.
示例 2:

输入:s = "010110"
输出:2
解释:我们翻转得到 011111,或者是 000111。
示例 3:

输入:s = "00011000"
输出:2
解释:我们翻转得到 00000000。
 

提示:

1 <= s.length <= 20000
s 中只包含字符 '0' 和 '1'

我的方法:二维dp(一维+2个状态)
把字符串想象成一个 拥有两个状态的 dp矩阵
请添加图片描述
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 第 i i i 个字符 转变为 字符 j 时,所需的最小翻转次数
因此,动态转移方程为:
d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] + I ( s [ i ] = ‘ 1 ’ ) d p [ i ] [ 1 ] = m i n ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] ) + I ( s [ i ] = ‘ 0 ’ ) ​ \begin{aligned} dp[i][0] &=dp[i−1][0]+I(s[i]=‘1’) \\ dp[i][1] &=min(dp[i−1][0],dp[i−1][1])+I(s[i]=‘0’) \end{aligned} ​ dp[i][0]dp[i][1]=dp[i1][0]+I(s[i]=‘1’)=min(dp[i1][0],dp[i1][1])+I(s[i]=‘0’)

class Solution:
    def minFlipsMonoIncr(self, s: str) -> int:
        lst = " ".join(s).split()
        dp = [[0, 0] for _ in range(len(lst))]
        if lst[0]=='0': # 初始状态 dp[0]
            dp[0][1] = 1
        else:
            dp[0][0] = 1
        for i in range(1, len(lst)):
            if lst[i] == '0':
                dp[i][0] = dp[i-1][0]
                if dp[i-1][0]==0:
                    dp[i][1] = 1
                else:
                    dp[i][1] = dp[i-1][1] + 1
            elif lst[i] == '1':
                if dp[i-1][0]==0:
                    dp[i][1]==0
                else: dp[i][1] = min(dp[i-1][1], dp[i-1][0])
                dp[i][0] = dp[i-1][0] + 1
        return min(dp[len(lst)-1])

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是字符串 s s s 的长度。需要遍历字符串一次,对于每个字符计算最小翻转次数的时间都是 O ( 1 ) O(1) O(1)
  • 空间复杂度:O(n)。使用空间优化的方法,可以降低至 O(1)。
( 数位DP ) 902. 最大为 N 的数字组合

给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = [‘1’,‘3’,‘5’],我们可以写数字,如 ‘13’, ‘551’, 和 ‘1351315’。

返回 可以生成的小于或等于给定整数 n 的正整数的个数 。

示例 1:

输入:digits = ["1","3","5","7"], n = 100
输出:20
解释:
可写出的 20 个数字是:
1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.
示例 2:

输入:digits = ["1","4","9"], n = 1000000000
输出:29523
解释:
我们可以写 3 个一位数字,9 个两位数字,27 个三位数字,
81 个四位数字,243 个五位数字,729 个六位数字,
2187 个七位数字,6561 个八位数字和 19683 个九位数字。
总共,可以使用D中的数字写出 29523 个整数。
示例 3:

输入:digits = ["7"], n = 8
输出:1
 

提示:

1 <= digits.length <= 9
digits[i].length == 1
digits[i] 是从 '1' 到 '9' 的数
digits 中的所有值都 不同 
digits 按 非递减顺序 排列
1 <= n <= 109

我的思路:dfs,暴力超时

方法二:数位动态规划

数位DP用于处理一些与数位有关的问题,主要是计数问题。

  • 求出在给定区间 [ A , B ] [A, B] [A,B] 内,符合条件 f ( i ) f(i) f(i) 的数 i i i 的个数。条件 f ( i ) f(i) f(i) 一般与数的大小无关,而与数的组成有关

我们称满足 x ≤ n x \le n xn 且仅包含 digits \textit{digits} digits 中出现的数字的 x x x 为合法的,则本题需要找出所有合法的 x x x 的个数。
n n n 是一个十进制的 k k k 位数,所有数字位数小于 k k k 且由 digits \textit{digits} digits 组成的数字则一定是小于 n n n 的。
定义: dp [ i ] [ 0 ] \color{red}定义:\textit{dp}[i][0] 定义:dp[i][0] 表示由 digits \textit{digits} digits 构成且 小于 n n n 的前 i i i 位的数字的个数,
定义: d p [ i ] [ 1 ] \color{red}定义:dp[i][1] 定义:dp[i][1] 表示由 digits \textit{digits} digits 构成且 等于 n n n 的前 i i i 位的数字的个数,可知 dp [ i ] [ 1 ] \textit{dp}[i][1] dp[i][1] 的取值只能为 0 0 0 1 1 1

例如: n = 2345 , digits = [“1",“2",“3",“4"] n = 2345, \textit{digits} = \text{[``1",``2",``3",``4"]} n=2345,digits=[“1",“2",“3",“4"]

dp [ 1 ] [ 0 ] , dp [ 2 ] [ 0 ] , dp [ 3 ] [ 0 ] , dp [ 4 ] [ 0 ] \textit{dp}[1][0], \textit{dp}[2][0], \textit{dp}[3][0], \textit{dp}[4][0] dp[1][0],dp[2][0],dp[3][0],dp[4][0] 分别表示小于 2 , 23 , 234 , 2345 2, 23, 234, 2345 2,23,234,2345 的合法数的个数,
dp [ 1 ] [ 1 ] , dp [ 2 ] [ 1 ] , dp [ 3 ] [ 1 ] , dp [ 4 ] [ 1 ] \textit{dp}[1][1], \textit{dp}[2][1], \textit{dp}[3][1], \textit{dp}[4][1] dp[1][1],dp[2][1],dp[3][1],dp[4][1] 分别表示等于 2 , 23 , 234 , 2345 2, 23, 234, 2345 2,23,234,2345 的合法数的个数。

digits \textit{digits} digits 中的字符数目为 m m m 个,数字 n n n 的前 j j j 位构成的数字为 s [ : j ] s[:j] s[:j],数字 n n n 的第 j j j 个字符为 s [ j ] s[j] s[j],当遍历到 n n n 的第 i i i 位时:

  • 当满足 i > 1 i > 1 i>1 时,此时任意数字 d d d 构成的数字 一定满足 d < s [ : i ] d < s[:i] d<s[:i] ( 个位数 < 十、百 . . . 位数 个位数 < 十、百...位数 个位数<十、百...位数);

  • 设数字 a < s [ : i − 1 ] a < s[:i-1] a<s[:i1],则此时在 a a a 的末尾追加一个数字 d d d 构成的数为 a × 10 + d a \times 10 + d a×10+d,此时可以知道 d d d 0 , 1 , ⋯   , 9 0,1,\cdots,9 0,1,,9 中任意数字均满足小于 a × 10 + d < s [ : i ] = s [ : i − 1 ] × 10 + s [ i ] a \times 10 + d < s[:i] = s[:i-1] \times 10 + s[i] a×10+d<s[:i]=s[:i1]×10+s[i]

  • 设数字 a = num [ i − 1 ] a = \textit{num}[i-1] a=num[i1],则此时在 a a a 的末尾追加一个数字 d d d 构成的数为 a × 10 + d a \times 10 + d a×10+d,此时可以知道 d < s [ i ] d < s[i] d<s[i] 时,才能满足 a × 10 + d < s [ : i ] = s [ : i − 1 ] × 10 + s [ i ] a \times 10 + d < s[:i] = s[:i-1] \times 10 + s[i] a×10+d<s[:i]=s[:i1]×10+s[i]

  • 初始化时令 dp [ 0 ] [ 1 ] = 1 \textit{dp}[0][1] = 1 dp[0][1]=1,如果前 i i i 位中存在某一位 j j j ,对于任意数字 d d d 均不能满足 d = s [ j ] d = s[j] d=s[j],则此时 dp [ i ] [ 1 ] = 0 \textit{dp}[i][1] = 0 dp[i][1]=0

根据上述描述从小到到计算 d p dp dp,设 C [ i ] C[i] C[i] 表示数组 digits \textit{digits} digits 中小于 n n n 的第 i i i 位数字的元素个数,则状态转移方程为:
d p [ i ] [ 0 ] = { C [ i ] , i = 1 m + d p [ i − 1 ] [ 0 ] × m + d p [ i − 1 ] [ 1 ] × C [ i ] , i > 1 ​ dp[i][0] = \begin{cases} C[i], & i = 1 \\ m + dp[i-1][0] \times m + dp[i-1][1] \times C[i], & i > 1 \\ \end{cases} ​ dp[i][0]={C[i],m+dp[i1][0]×m+dp[i1][1]×C[i],i=1i>1

我们计算出前 k k k 位小于 n n n 的数字的个数 dp [ k ] [ 0 ] \textit{dp}[k][0] dp[k][0],前 k k k 位等于 n n n 的数字的个数 dp [ k ] [ 1 ] \textit{dp}[k][1] dp[k][1],最终的答案为 dp [ k ] [ 0 ] + dp [ k ] [ 1 ] \textit{dp}[k][0] + \textit{dp}[k][1] dp[k][0]+dp[k][1]

    def atMostNGivenDigitSet(self, digits: List[str], n: int) -> int:
        m = len(digits)
        s = str(n)
        length = len(s)
        dp = [[0, 0] for _ in range(length+1)]
        dp[0][1] = 1                            
        for i in range(1, length+1):
            for d in digits:
                if d == s[i-1]:
                    dp[i][1] = dp[i-1][1]
                elif d < s[i-1]:
                    dp[i][0] += dp[i-1][1]      # 等于 s[i-1] 的位数的个数
                else:
                    break
            if i>1:
                dp[i][0] += m + dp[i-1][0]*m    # 低于 s[i-1] 的位数的个数
        return sum(dp[-1])
  • 时间复杂度: O ( log ⁡ n × k ) O(\log n \times k) O(logn×k),其中 n n n 为给定数字, k k k 表示给定的数字的种类。需要遍历 n n n 的所有数位的数字,nn 含有的数字个数为 log ⁡ 10 n \log_{10}n log10n,检测每一位时都需要遍历一遍给定的数字,因此总的时间复杂度为 O ( log ⁡ n × k ) O(\log n \times k) O(logn×k)

  • 空间复杂度: O ( log ⁡ n ) O(\log n) O(logn),其中 n n n 为给定数字。需要需要保存每一位上可能数字的组合数目,因此需要的空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

5270. 网格中的最小路径代价

给你一个下标从 0 开始的整数矩阵 grid ,矩阵大小为 m x n ,由从 0 到 m * n - 1 的不同整数组成。你可以在此矩阵中,从一个单元格移动到 下一行 的任何其他单元格。如果你位于单元格 (x, y) ,且满足 x < m - 1 ,你可以移动到 (x + 1, 0), (x + 1, 1), …, (x + 1, n - 1) 中的任何一个单元格。注意: 在最后一行中的单元格不能触发移动。

每次可能的移动都需要付出对应的代价,代价用一个下标从 0 开始的二维数组 moveCost 表示,该数组大小为 (m * n) x n ,其中 moveCost[i][j] 是从值为 i 的单元格移动到下一行第 j 列单元格的代价。从 grid 最后一行的单元格移动的代价可以忽略。

grid 一条路径的代价是:所有路径经过的单元格的 值之和 加上 所有移动的 代价之和 。从 第一行 任意单元格出发,返回到达 最后一行 任意单元格的最小路径代价。

在这里插入图片描述

示例 1:

输入:grid = [[5,3],[4,0],[2,1]], moveCost = [[9,8],[1,5],[10,12],[18,6],[2,4],[14,3]]
输出:17
解释:最小代价的路径是 5 -> 0 -> 1 。
- 路径途经单元格值之和 5 + 0 + 1 = 6 。
- 从 5 移动到 0 的代价为 3 。
- 从 0 移动到 1 的代价为 8 。
路径总代价为 6 + 3 + 8 = 17 。
示例 2:

输入:grid = [[5,1,2],[4,0,3]], moveCost = [[12,10,15],[20,23,8],[21,7,1],[8,1,13],[9,10,25],[5,3,2]]
输出:6
解释:
最小代价的路径是 2 -> 3 。 
- 路径途经单元格值之和 2 + 3 = 5 。 
- 从 2 移动到 3 的代价为 1 。 
路径总代价为 5 + 1 = 6 。
 

提示:

m == grid.length
n == grid[i].length
2 <= m, n <= 50
grid 由从 0 到 m * n - 1 的不同整数组成
moveCost.length == m * n
moveCost[i].length == n
1 <= moveCost[i][j] <= 100

我的解法:二维dp,与剑指 091 粉刷房子类似
d p [ i ] [ j ] dp[i][j] dp[i][j] 定义:当前位置的最优值
动态转移方程:
d p [ i ] [ j ] = (遍历第i-1层的所有k ∈ n ,找到dp[i-1][k]+对应的moveCost 的最小值) + g r i d [ i ] [ j ] dp[i][j] =\text{(遍历第i-1层的所有k}\in n,\text{找到dp[i-1][k]+对应的moveCost 的最小值)} +grid[i][j] dp[i][j]=(遍历第i-1层的所有kn找到dp[i-1][k]+对应的moveCost 的最小值)+grid[i][j]

class Solution:
    def minPathCost(self, grid: List[List[int]], moveCost: List[List[int]]) -> int:
        m, n = len(grid), len(grid[-1])
        dp = [[100000 for _ in range(n)] for i in range(m)]
        for i in range(n):
            dp[0][i] = grid[0][i]
        for i in range(1, m):
            for j in range(n):
                tmp = 10000
                for k in range(n):
                    pre = grid[i-1][k]
                    cur = grid[i][j]
                    cost = moveCost[pre][j]
                    dp[i][j] = min(dp[i][j], cur + cost + dp[i-1][k])
        return min(dp[m-1])
(dp[i][j]:以A[i],A[j]A[i],A[j]结尾)剑指 Offer II 093. 最长斐波那契数列

如果序列 X_1, X_2, …, X_n 满足下列条件,就说它是 斐波那契式 的:

n >= 3
对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。

(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

示例 1:

输入: arr = [1,2,3,4,5,6,7,8]
输出: 5
解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
示例 2:

输入: arr = [1,3,7,11,12,14,18]
输出: 3
解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。
 

提示:

3 <= arr.length <= 1000
1 <= arr[i] < arr[i + 1] <= 10^9

方法一:使用 unordered_set 的暴力法
思路

每个斐波那契式的子序列都依靠两个相邻项来确定下一个预期项。例如,对于 2 , 5 2, 5 2,5,我们所期望的子序列必定以 7 , 12 , 19 , 31 7, 12, 19, 31 7,12,19,31 等继续。

我们可以使用 S e t Set Set 结构来快速确定下一项是否在数组 A A A 中。由于这些项的值以指数形式增长,最大值 ≤ 1 0 9 \leq 10^9 109 的斐波那契式的子序列最多有 43 43 43 项。

算法

对于每个起始对 A [ i ] , A [ j ] A[i], A[j] A[i],A[j],我们保持下一个预期值 y = A [ i ] + A [ j ] y = A[i] + A[j] y=A[i]+A[j] 和此前看到的最大值 x = A [ j ] x = A[j] x=A[j]。如果 y y y 在数组中,我们可以更新这些值 ( x , y ) − > ( y , x + y ) (x, y) -> (y, x+y) (x,y)>(y,x+y)

此外,由于子序列的长度大于等于 3 3 3 只能是斐波那契式的,所以我们必须在最后进行检查 ans >= 3 ? ans : 0

class Solution:
    def lenLongestFibSubseq(self, arr: List[int]) -> int:
        s = set(arr)
        n = len(arr)
        ans = 0
        for i in range(n):
            for j in range(i+1, n):
                x, y = arr[i], arr[j] + arr[i]
                length = 2
                while y in s:
                    x = y-x
                    y = x+y
                    length += 1
                    ans = max(ans, length)
        return ans

复杂度分析

时间复杂度: O ( N 2 log ⁡ M ) O(N^2 \log M) O(N2logM),其中 N N N A A A 的长度, M M M A A A 中的最大值。
空间复杂度: O ( N ) O(N) O(N),集合 ( s e t ) S (set)S setS 使用的空间。

解法二:二维dp + 二分查找
i,j分别表示选择为斐波那契数列的最后两个元素

d p [ i ] [ j ] dp[i][j] dp[i][j]:表示以 A [ i ] , A [ j ] A[i],A[j] A[i],A[j]结尾的斐波那契数列的最大长度
d p [ i ] [ j ] = L e n ( . . . . . . , A [ i ] , A [ j ] ) dp[i][j] = Len(......, A[i],A[j]) dp[i][j]=Len(......,A[i],A[j])

则 状态转移方程为:
d p [ i ] [ j ] = d p [ k ] [ i ] + 1 ,  if 存在k,使得A[k]+A[i]==A[j] d p [ i ] [ j ] = 0 ,  other \begin{aligned} dp[i][j]= dp[k][i] + 1,& \text{ if 存在k,使得{A[k]+A[i]==A[j]}}\\ dp[i][j]= 0, & \text{ other} \end{aligned} dp[i][j]=dp[k][i]+1,dp[i][j]=0, if 存在k,使得A[k]+A[i]==A[j] other

    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        vector<vector<int>> dp(n, vector<int>(n));
        if (arr[0] == arr[2] - arr[1]) dp[1][2] = 3;
        int l, r, m, ans = 0;
        for (int j=3; j<n; j++){
            for (int i=j-1; i>=1; i--){
                l = 0; r = i-1;
                while (l<=r){
                    m = l + (r-l)/2;
                    if (arr[m] == arr[j] - arr[i]){
                        if (dp[m][i]==0) dp[m][i] = 2;
                        dp[i][j] = dp[m][i]+1;
                        ans = max(dp[i][j], ans);
                        break;
                    }else if (arr[m] < arr[j] - arr[i]) l = m+1;
                    else r = m-1;
                }
            }
        }
        return  ans;
    }

时间复杂度: O ( n 2 l o g ( n ) ) O(n^2 log(n)) On2log(n)

解法三:二维DP + 哈希map + 剪枝

  • 空间换时间,使用unordered_map存差值,查找的时间降为O(1)

将 arr 的值-索引,存入字典中,每次查找时,需要判断 是否找到,且 找到的值要 小于 减数

    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        vector<vector<int>> dp(n, vector<int>(n, 2));
        unordered_map<int, int> mp;
        for (int i=0; i<n; i++) mp[arr[i]] = i;
        if (arr[0] == arr[2] - arr[1]) dp[1][2] = 3;
        int ans = 0, diff;
        for (int j=3; j<n; j++){
            for (int i=j-1; i>=1; i--){
                diff = arr[j] - arr[i];
                if (diff >= arr[i] ) break; // 👀 剪枝 当 i 这个位置求得的diff大于arr_i时,前面的i都不用算了
                if (mp.count(diff)){
                    dp[i][j] = dp[mp[diff]][i]+1;
                    ans = max(dp[i][j], ans);
                }
            }
        }
        return  ans;
    }

时间复杂度: O ( n 2 ) O(n^2) O(n2)

剑指 Offer II 098. 路径的数目

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

在这里插入图片描述

示例 1:

输入:m = 3, n = 7
输出:28
示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:

输入:m = 7, n = 3
输出:28
示例 4:

输入:m = 3, n = 3
输出:6
 

提示:

1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109

思路历程:dfs->计算量太大,超时->二维dp->优化->1维dp

定义: dp [ i ] [ j ] \color{red}定义: \textit{dp}[i][j] 定义:dp[i][j] 表示从 [ 0 , 0 ] [0, 0] [0,0] 抵达 [ i , j ] [i, j] [i,j] 位置的总路径。

二维比较好理解

	int uniquePaths(int m, int n) {
		vector<vector<int>> dp(m, vector<int> (n));
		for (int i = 0; i < m; ++i) dp[i][0] = 1;
		for (int i = 0; i < n; ++i) dp[0][i] = 1;

		for (int i = 1; i < m; ++i) {
			for (int j = 1; j < n; ++j) {
				dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
			}
		}

		return dp[m-1][n-1];

复杂度分析

时间复杂度: O ( m n ) O(mn) O(mn)

空间复杂度: O ( m n ) O(mn) O(mn),即为存储所有状态需要的空间。注意到 f ( i , j ) f(i,j) f(i,j) 仅与第 i 行和第 i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O ( n ) O(n) O(n)。此外,由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m 和 n 使得 m ≤ n m \leq n mn,这样空间复杂度降低至 O ( min ⁡ ( m , n ) ) O(\min(m, n)) O(min(m,n))
一维是从二维优化而来的

	int uniquePaths(int m, int n) {
		vector<int> dp0(m);
		for (int i = 0; i < m; ++i) dp0[i] = 1;

		for (int j = 1; j < n; ++j) {
		for (int i = 1; i < m; ++i) {
				dp0[i] = dp0[i] + dp0[i-1];
			}
		}
		return dp0[m-1];
    }
剑指 Offer II 099. 最小路径之和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:一个机器人每次只能向下或者向右移动一步。

在这里插入图片描述

示例 1:



输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12
 

提示:

m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100

我的方法:一维DP
二维dp方程:
d p [ i ] [ j ] = m i n { d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] } + g r i d [ i ] [ j ] dp[i][j] = min\{dp[i-1][j], dp[i][j-1]\} + grid[i][j] dp[i][j]=min{dp[i1][j],dp[i][j1]}+grid[i][j]

typedef short int SI;
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
		SI m = grid.size(), n = grid.back().size();
		vector<SI> dp1(m), dp2(n);
		dp1[0] = dp2[0] = grid[0][0];
		for (SI i = 1; i < m; ++i)
			dp1[i] = dp1[i - 1] + grid[i][0];
		for (SI j = 1; j < n; ++j)
			dp2[j] = dp2[j - 1] + grid[0][j];

		for (SI i = 1; i < m; ++i) {
			dp2[0] = dp1[i];
			for (SI j = 1; j < n; ++j) {
				dp2[j] = min(dp2[j], dp2[j-1]) + grid[i][j];
			}
		}
		return dp2[n-1];
	}
};

复杂度分析

  • 时间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。需要对整个网格遍历一次,计算 dp \textit{dp} dp 的每个元素的值。
  • 空间复杂度:O(n)。
剑指 Offer II 100. 三角形中最小路径之和

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

示例 1:

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
   2
  3 4
 6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:

输入:triangle = [[-10]]
输出:-10
 

提示:

1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-104 <= triangle[i][j] <= 104
 

进阶:

你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?

二维dp
初始状态 dp[i][0] dp[i][i]i=0,1,...,n
转移方程:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]; 注意,不包括第一列与斜对角元素

typedef short int SI ;
class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
		SI m = triangle.size(), n;
		vector<vector<int>> dp(m, vector<int>(m));
		dp[0][0] = triangle[0][0];
		for (SI i = 1; i < m; ++i) {
			dp[i][0] = dp[i - 1][0] + triangle[i][0];
			dp[i][i] = dp[i - 1][i - 1] + triangle[i][i];
		}
		for (SI i = 2; i < m; ++i) {
			for (SI j = 1; j < triangle[i].size()-1; ++j) {
				dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j];
			}
		}
		auto it = min_element(dp[m - 1].begin(), dp[m - 1].end());
		return *it;
	}
};
(双字符串)剑指 Offer II 095. 最长公共子序列 LCS

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
 

提示:

1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

经典方法:二维DP,双字符串问题

  • d p [ i ] [ j ] \color{red}dp[i][j] dp[i][j]:对于字符串 s1[1,..., i]s2[1,...,j] ,它们的 LCS 长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]
  • 状态转移方程:
    如果 s1[i]==s2[j] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i1][j1]+1
    如果 s1[i]!=s2[j] d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = max(dp[i-1][j], dp[i][j-1]) dp[i][j]=max(dp[i1][j],dp[i][j1])
  • 考虑base
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n1, n2 = len(text1)+1, len(text2)+1
        dp = [[0] * n1 for _ in range(n2)]

        for i in range(1, n2):
            for j in range(1, n1):
                if text1[j-1]==text2[i-1]:
                    dp[i][j] = dp[i-1][j-1]+1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        
        return dp[n2-1][n1-1]

复杂度分析

  • 时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m n n n 分别是字符串 text 1 \textit{text}_1 text1 text 2 \textit{text}_2 text2 的长度。二维数组 dp \textit{dp} dp m + 1 m+1 m+1 行和 n + 1 n+1 n+1 列,需要对 dp \textit{dp} dp 中的每个元素进行计算。
  • 空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m n n n 分别是字符串 text 1 \textit{text}_1 text1 text 2 \textit{text}_2 text2 的长度。创建了 m + 1 m+1 m+1 n + 1 n+1 n+1 列的二维数组 dp \textit{dp} dp
(双字符串)剑指 Offer II 096. 字符串交织

给定三个字符串 s1、s2、s3,请判断 s3 能不能由 s1 和 s2 交织(交错) 组成。

两个字符串 s 和 t 交织 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

s = s1 + s2 + … + sn
t = t1 + t2 + … + tm
|n - m| <= 1
交织 是 s1 + t1 + s2 + t2 + s3 + t3 + … 或者 t1 + s1 + t2 + s2 + t3 + s3 + …
提示:a + b 意味着字符串 a 和 b 连接。

示例 1:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
示例 2:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
示例 3:

输入:s1 = "", s2 = "", s3 = ""
输出:true
 

提示:

0 <= s1.length, s2.length <= 100
0 <= s3.length <= 200
s1、s2、和 s3 都由小写英文字母组成

二维dp
定义: d p [ i ] [ j ] dp[i][j] dp[i][j]:对于字符串 s1[1,..., i], s2[1,...,j]s3[1, i+j],能否满足条件。
状态转移方程:

if dp[i-1][j]==True:
    dp[i][j] = True if s1[i-1]==s3[i+j-1] else False
elif dp[i][j-1]==True:
    dp[i][j] = True if s2[j-1]==s3[i+j-1] else False

考虑base

class Solution:
    def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
        n1, n2 = len(s1)+1, len(s2)+1
        if (n1+n2!=len(s3)+2): return False
        if (n1==1): return s2==s3
        elif (n2==1): return s1==s3
        dp = [[False]*n2 for _ in range(n1)]
        dp[0][0] = True
        for i in range(n1-1):
            if s1[i]==s3[i]:
                dp[i+1][0] = True
            else: break
        for j in range(n2-1):
            if s2[j]==s3[j]:
                dp[0][j+1] = True
            else: break
        for i in range(1, n1):
            for j in range(1, n2):
                if dp[i-1][j]==True:
                    dp[i][j] = s1[i-1]==s3[i+j-1]
                elif dp[i][j-1]==True:
                    dp[i][j] = s2[j-1]==s3[i+j-1]
        return dp[n1-1][n2-1]
(双字符串)剑指 Offer II 097. 子序列的数目

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)

题目数据保证答案符合 32 位带符号整数范围。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit
示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag
 

提示:

0 <= s.length, t.length <= 1000
s 和 t 由英文字母组成

我的解法:辅助数组+二维dp,固定 t t t,再移动 s s s
定义: dp [ i ] [ j ] \color{red}定义: \textit{dp}[i][j] 定义:dp[i][j] 表示在 s [ : i ] s[:i] s[:i] 的子序列中 t [ : j ] t[:j] t[:j] 出现的个数。 注意,子序列从开始到 i  \color{yellow}\text{\colorbox{black}{注意,子序列从开始到 i }} 注意,子序列从开始到 i 
定义:辅助数组vector<int> = vis(t.size()+1, 0)vis[i]=j表示对于子序列 t [ : i ] t[:i] t[:i],在 s s s 中找到匹配子串所需的最短长度 j j j

base: 见代码

动态转移方程:
dp [ i ] [ j ] = { dp [ i − 1 ] [ j − 1 ] + dp [ i − 1 ] [ j ] , s [ i ] = t [ j ] dp [ i − 1 ] [ j ] , s [ i ] ≠ t [ j ] \textit{dp}[i][j] = \begin{cases} \textit{dp}[i-1][j-1]+\textit{dp}[i -1][j], & s[i]=t[j]\\ \textit{dp}[i -1][j], & s[i] \ne t[j] \end{cases} dp[i][j]={dp[i1][j1]+dp[i1][j],dp[i1][j],s[i]=t[j]s[i]=t[j]

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n, m = len(s)+1, len(t)+1
        dp = [[0]*m for _ in range(n)]
        vis = [0]*(m) # 辅助数组
        l, r = 1, 1
        while l<m:
            while r<n:
                if t[l-1]==s[r-1]:
                    break
                r += 1
            vis[l] = r
            r += 1
            l += 1
        # base
        for i in range(n): dp[i][0] = 1
        
        for j in range(1, m):
            for i in range(j, n):
                if i<vis[j]: continue # base 还未找到第一次匹配子串
                elif i==vis[j]: dp[i][j] = dp[i-1][j-1] # base 找到第一次匹配子串
                else: # 不是第一次匹配的子串
                	# dp
                    if s[i-1]!=t[j-1]: # 当前字符不相等,则
                        dp[i][j] = dp[i-1][j]
                    else: # 当前字符相等,则
                        dp[i][j] = dp[i-1][j] + dp[i-1][j-1]

        return dp[n-1][m-1]

时间、空间复杂度: O ( m n ) O(mn) O(mn)

官解:逆序二维dp,固定 s s s,移动 t t t,可以减少判断条件
假设字符串 s s s t t t 的长度分别为 m m m n n n。如果 t t t s s s 的子序列,则 s s s 的长度一定大于或等于 t t t 的长度,即只有当 m ≥ n m \ge n mn 时, t t t 才可能是 s s s 的子序列。如果 m < n m<n m<n,则 t t t 一定不是 s s s 的子序列,因此直接返回 0 0 0

m ≥ n m \ge n mn 时,可以通过动态规划的方法计算在 s s s 的子序列中 t t t 出现的个数。

创建二维数组 dp \textit{dp} dp 定义: dp [ i ] [ j ] \color{red}定义: \textit{dp}[i][j] 定义:dp[i][j] 表示在 s [ i : ] s[i:] s[i:] 的子序列中 t [ j : ] t[j:] t[j:] 出现的个数。 注意,子序列从 i 到末尾 \color{yellow}\text{\colorbox{black}{注意,子序列从 i 到末尾}} 注意,子序列从 i 到末尾

上述表示中, s [ i : ] s[i:] s[i:] 表示 s s s 从下标 i i i 到末尾的子字符串, t [ j : ] t[j:] t[j:] 表示 t t t 从下标 j j j 到末尾的子字符串。

考虑动态规划的边界情况

j = n j=n j=n 时, t [ j : ] t[j:] t[j:] 为空字符串,由于空字符串是任何字符串的子序列,因此对任意 0 ≤ i ≤ m 0 \le i \le m 0im,有 dp [ i ] [ n ] = 1 \textit{dp}[i][n]=1 dp[i][n]=1

i = m i=m i=m j < n j<n j<n 时, s [ i : ] s[i:] s[i:] 为空字符串, t [ j : ] t[j:] t[j:] 为非空字符串,由于非空字符串不是空字符串的子序列,因此对任意 0 ≤ j < n 0 \le j<n 0j<n,有 dp [ m ] [ j ] = 0 \textit{dp}[m][j]=0 dp[m][j]=0

i < m i<m i<m j < n j<n j<n 时,考虑 dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 的计算:

s [ i ] = t [ j ] s[i]=t[j] s[i]=t[j] 时, dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 由两部分组成:

如果 s [ i ] s[i] s[i] t [ j ] t[j] t[j] 匹配,则考虑 t [ j + 1 : ] t[j+1:] t[j+1:] 作为 s [ i + 1 : ] s[i+1:] s[i+1:] 的子序列,子序列数为 dp [ i + 1 ] [ j + 1 ] \textit{dp}[i+1][j+1] dp[i+1][j+1]

如果 s [ i ] s[i] s[i] 不和 t [ j ] t[j] t[j] 匹配,则考虑 t [ j : ] t[j:] t[j:] 作为 s [ i + 1 : ] s[i+1:] s[i+1:] 的子序列,子序列数为 dp [ i + 1 ] [ j ] \textit{dp}[i+1][j] dp[i+1][j]

因此当 s [ i ] = t [ j ] s[i]=t[j] s[i]=t[j] 时,有 dp [ i ] [ j ] = dp [ i + 1 ] [ j + 1 ] + dp [ i + 1 ] [ j ] \textit{dp}[i][j]=\textit{dp}[i+1][j+1]+\textit{dp}[i+1][j] dp[i][j]=dp[i+1][j+1]+dp[i+1][j]

s [ i ] ≠ t [ j ] s[i] \ne t[j] s[i]=t[j] 时, s [ i ] s[i] s[i] 不能和 t [ j ] t[j] t[j] 匹配,因此只考虑 t [ j : ] t[j:] t[j:] 作为 s [ i + 1 : ] s[i+1:] s[i+1:] 的子序列,子序列数为 dp [ i + 1 ] [ j ] \textit{dp}[i+1][j] dp[i+1][j]

因此当 s [ i ] ≠ t [ j ] s[i] \ne t[j] s[i]=t[j]时,有 dp [ i ] [ j ] = dp [ i + 1 ] [ j ] \textit{dp}[i][j]=\textit{dp}[i+1][j] dp[i][j]=dp[i+1][j]

由此可以得到如下状态转移方程:
dp [ i ] [ j ] = { dp [ i + 1 ] [ j + 1 ] + dp [ i + 1 ] [ j ] , s [ i ] = t [ j ] dp [ i + 1 ] [ j ] , s [ i ] ≠ t [ j ] \textit{dp}[i][j] = \begin{cases} \textit{dp}[i+1][j+1]+\textit{dp}[i+1][j], & s[i]=t[j]\\ \textit{dp}[i+1][j], & s[i] \ne t[j] \end{cases} dp[i][j]={dp[i+1][j+1]+dp[i+1][j],dp[i+1][j],s[i]=t[j]s[i]=t[j]

最终计算得到 dp [ 0 ] [ 0 ] \textit{dp}[0][0] dp[0][0] 即为在 s s s 的子序列中 t t t 出现的个数。

    int numDistinct(string s, string t) {
        int m = s.length(), n = t.length();
        if (m < n) {
            return 0;
        }
        vector<vector<unsigned long long>> dp(m + 1, vector<unsigned long long>(n + 1));
        for (int i = 0; i <= m; i++) {
            dp[i][n] = 1;
        }
        for (int i = m - 1; i >= 0; i--) {
            char sChar = s.at(i);
            for (int j = n - 1; j >= 0; j--) {
                char tChar = t.at(j);
                if (sChar == tChar) {
                    dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
                } else {
                    dp[i][j] = dp[i + 1][j];
                }
            }
        }
        return dp[0][0];
    }

两次dp

(双dp,独立)剑指 Offer II 094. 最少回文分割

给定一个字符串 s,请将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的 最少分割次数 。

示例 1:

输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:

输入:s = "a"
输出:0
示例 3:

输入:s = "ab"
输出:1
 

提示:

1 <= s.length <= 2000
s 仅由小写英文字母组成

一维dp + 二维dp

一维 d p :定义 f [ i ] \color{red}一维dp:定义 f[i] 一维dp:定义f[i] 表示字符串的前缀 s [ 0.. i ] s[0..i] s[0..i] 的最少分割次数。要想得出 f [ i ] f[i] f[i] 的值,我们可以考虑枚举 s [ 0.. i ] s[0..i] s[0..i] 分割出的最后一个回文串,这样我们就可以写出状态转移方程:
f [ i ] = min ⁡ 0 ≤ j < i { f [ j ] } + 1 , 其中  s [ j + 1.. i ]  是一个回文串 f[i] = \min_{0 \leq j < i} \{ f[j] \} + 1, \quad 其中 ~ s[j+1..i] ~是一个回文串 f[i]=0j<imin{f[j]}+1,其中 s[j+1..i] 是一个回文串
,其中 s [ j + 1.. i ] s[j+1..i] s[j+1..i] 是一个回文串

即我们枚举最后一个回文串的起始位置 j + 1 j+1 j+1,保证 s [ j + 1.. i ] s[j+1..i] s[j+1..i] 是一个回文串,那么 f [ i ] f[i] f[i] 就可以从 f [ j ] f[j] f[j] 转移而来,附加 1 1 1 次额外的分割次数。

注意到上面的状态转移方程中,我们还少考虑了一种情况,即 s [ 0.. i ] s[0..i] s[0..i] 本身就是一个回文串。此时其不需要进行任何分割,即:
f [ i ] = 0 f[i] = 0 f[i]=0

那么我们如何知道 s [ j + 1.. i ] s[j+1..i] s[j+1..i] 或者 s [ 0.. i ] s[0..i] s[0..i] 是否为回文串呢?我们可以使用与「剑指 Offer II 086. 分割回文子字符串的官方题解」中相同的预处理方法,将字符串 ss 的每个子串是否为回文串预先计算出来,即:

二维 d p : g ( i , j ) \color{red}二维dp: g(i, j) 二维dpg(i,j) 表示 s [ i . . j ] s[i..j] s[i..j] 是否为回文串,那么有状态转移方程:
g ( i , j ) = { True , i ≥ j g ( i + 1 , j − 1 ) ∧ ( s [ i ] = s [ j ] ) , otherwise ​ g(i, j) = \begin{cases} \texttt{True}, & \quad i \geq j \\ g(i+1,j-1) \wedge (s[i]=s[j]), & \quad \text{otherwise} \end{cases} ​ g(i,j)={True,g(i+1,j1)(s[i]=s[j]),ijotherwise

其中 ∧ \wedge 表示逻辑与运算,即 s [ i . . j ] s[i..j] s[i..j] 为回文串,当且仅当其为空串( i > j i>j i>j),其长度为 1 1 1 i = j i=j i=j),或者首尾字符相同且 s [ i + 1.. j − 1 ] s[i+1..j-1] s[i+1..j1] 为回文串。

这样一来,我们只需要 O ( 1 ) O(1) O(1) 的时间就可以判断任意 s [ i . . j ] s[i..j] s[i..j] 是否为回文串了。通过动态规划计算出所有的 f f f 值之后,最终的答案即为 f [ n − 1 ] f[n-1] f[n1],其中 n n n 是字符串 s s s 的长度。

class Solution:
    def minCut(self, s: str)->int:
        n = len(s)
        hp = [[True] * n for _ in range(n)]

        for i in range(n-1, -1, -1): # hp[i][j] 表示 s[i:j] 是回文串
            for j in range(i+1, n):
                hp[i][j] = s[i]==s[j] and hp[i+1][j-1]

        dp = [float("inf")]*n
        for i in range(n):
            if hp[0][i]:
                dp[i] = 0
            else:
                for j in range(i):
                    if hp[j+1][i]:
                        dp[i]= min(dp[i], dp[j]+1)
        return dp[n-1]

备注:

  1. python中的最大值(不考虑整数、浮点数区分,统一使用),float("int")

复杂度分析

时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是字符串 s s s 的长度。预处理计算 g g g 和动态规划计算 f f f 的时间复杂度均为 O ( n 2 ) O(n^2) O(n2)

空间复杂度: O ( n 2 ) O(n^2) O(n2),数组 g g g 需要使用 O ( n 2 ) O(n^2) O(n2) 的空间,数组 f f f 需要使用 O ( n ) O(n) O(n) 的空间。

10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:

输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:

输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。


提示:

1 <= s.length <= 20
1 <= p.length <= 30
s 只包含从 a-z 的小写字母。
p 只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符

动态规划
我们用 f [ i ] [ j ] f[i][j] f[i][j] 表示 s s s 的前 i i i 个字符与 p p p 中的前 j j j 个字符是否能够匹配。在进行状态转移时,我们考虑 p p p 的第 j j j 个字符的匹配情况:

背包DP

背包DP 属于 线性DP 中的一种经典模型,围绕 容量、物品、体积、价值等关键字展开
求解的是一种符合题设要求的 选择物品 的 方案

背包问题的常见描述:

  • N N N 件物品和一个总容量为 V \red V V 的背包 (有时候,背包的容量需要自己求)

  • i i i 件物品的 体积 v i v_i vi价值 w i w_i wi

  • 找出一种选择物品的 方案,使得选择的物品的总体积不超过 V V V,且 总价值 最大

  • dp定义 d p [ i ] [ j ] dp[i][j] dp[i][j]表示将 i i i 件物品 装进 限重为 j j j 的背包 可以获得的最大价值, 0 < = i < = N , 0 < = j < = V 0<=i<=N, 0<=j<=V 0<=i<=N,0<=j<=V

在这里插入图片描述

然后针对于不同的 背包类型,对于选择物品的方式有着各式各样的 限制性条件,接下来一一列举

解题流程

  • 计算0-1背包容量 V \red V V,一般先对所有物品求和得到arrsum,根据条件(选择某一部分物品放进背包1,然后背包1背包0要达成一种平衡)可以求出 背包的容量值 V \red V V
  • 定义 dp[i][j]函数
  • 确定 base 条件
  • 状态转移方程:
    • 1)当前物品无法放进背包
    • 2)当前物品可以放进背包(2 种 情况):
      • 放,
      • 不放,
  • return d p [ N − 1 ] [ V − 1 ] dp[N-1][V-1] dp[N1][V1]

416. 分割等和子集 (背包容量需要自己算)

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。


示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。


提示:

1 <= nums.length <= 200
1 <= nums[i] <= 100

我的思路:dfs回溯+剪枝

bool ansCanPartition = false;
	bool canPartition(vector<int>& nums) {
		int sum = accumulate(nums.begin(), nums.end(), 0);
		set<int> vis; // 剔除已走路径
		unordered_map<int, bool> mp; // 剪枝
		if (!sum % 2) return false;
		return dfsCanPartition(nums, vis, mp, sum, sum);
	}
	bool dfsCanPartition(vector<int>& select, set<int>& vis, unordered_map<int, bool>& mp, int target, int cur) {
		if (target > 2 * cur) return false;
		if (target == 2 * cur) return true;
		for (int i = 0; i < select.size(); i++) {
			if (vis.count(i) || mp[cur - select[i]]) continue;
			if (ansCanPartition == true) break;
			vis.insert(i);
			ansCanPartition = ansCanPartition || dfsCanPartition(select, vis, mp, target, cur - select[i]);
			mp[cur - select[i]] = true;
			vis.erase(i);
		}
		return ansCanPartition;
	}

我的方法:背包dp
定义: dp [ i ] [ j ] \color{red}\textit{dp}[i][j] dp[i][j] 表示从数组的 [ 0 , i ] [0,i] [0,i] 下标范围内选取若干个正整数,是否存在一种选取方案使得被选取的正整数的和 ( 背包和 \color{red}\text{背包和} 背包和)等于 j。初始时, dp \textit{dp} dp 中的全部元素都是 false \text{false} false。 注意,背包容量为: s u m ( n u m s ) / / 2 sum(nums)//2 sum(nums)//2
对于 d p [ i ] [ j ] dp[i][j] dp[i][j] 的是否能取到 j j j 来说,它有以下几种可能

  •  if dp[i-1][j]==1 \text{ if dp[i-1][j]==1}  if dp[i-1][j]==1:说明,对于前 i − 1 i-1 i1 项,已经能找到满足 背包和 j 背包和j 背包和j 的组合了
  •  if j == nums[i] \text{ if j == nums[i]}  if j == nums[i]:说明,当前 位置 n u m s [ i ] nums[i] nums[i] 恰好满足 背包和 j 背包和 j 背包和j,因此背包只需要放 n u m s [ i ] nums[i] nums[i] 即可
  •  if j > nums[i] \text{ if j > nums[i]}  if j > nums[i]:说明,当前 背包和 j 背包和j 背包和j 有盈余,因此我们需要看,前 i − 1 i-1 i1 项是否有满足 背包和 = j − n u m s [ i ] 背包和=j-nums[i] 背包和=jnums[i] 的组合,即判断 d p [ i − 1 ] [ j − n u m s [ i ] ] dp[i-1][j-nums[i]] dp[i1][jnums[i]]
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n<2: return False

        sumNums = sum(nums)
        if sumNums%2==1 : return False
        else: target = sumNums//2

        dp = [[0]*(target+1) for _ in range(n)]
        for i in range(n):
            cur = nums[i]
            for j in range(target+1):
                if dp[i-1][j]==1: dp[i][j] = 1
                elif j==cur: dp[i][j] = 1
                elif j>cur:
                    if dp[i-1][j-nums[i]]==1:
                        dp[i][j] = 1
        return dp[n-1][target]==1

优化空间:动态规划

这道题可以换一种表述:给定一个只包含正整数的非空数组 nums [ 0 ] \textit{nums}[0] nums[0],判断是否可以从数组中选出一些数字,使得这些数字的和等于整个数组的元素和的一半。因此这个问题可以转换成「0-1 背包问题」。这道题与传统的「0-1 背包问题」的区别在于,传统的「0-1 背包问题」要求选取的物品的重量之和不能超过背包的总容量,这道题则要求选取的数字的和恰好等于整个数组的元素和的一半。类似于传统的「0-1 背包问题」,可以使用动态规划求解。

在使用动态规划求解之前,首先需要进行以下判断。

  • 根据数组的长度 n 判断数组是否可以被划分。如果 n < 2 n<2 n<2,则不可能将数组分割成元素和相等的两个子集,因此直接返回 false \text{false} false

  • 计算整个数组的元素和 sum \textit{sum} sum 以及最大元素 maxNum \textit{maxNum} maxNum如果 sum \textit{sum} sum 是奇数,则不可能将数组分割成元素和相等的两个子集,因此直接返回 false \text{false} false如果 sum \textit{sum} sum是偶数,则令 target = sum 2 \textit{target}=\frac{\textit{sum}}{2} target=2sum,需要判断是否可以从数组中选出一些数字,使得这些数字的和等于 target \textit{target} target。如果 maxNum > target \textit{maxNum}>\textit{target} maxNum>target,则除了 maxNum \textit{maxNum} maxNum 以外的所有元素之和一定小于 target \textit{target} target,因此不可能将数组分割成元素和相等的两个子集,直接返回 false \text{false} false

创建二维数组 dp \textit{dp} dp,包含 n 行 target + 1 \textit{target}+1 target+1 列,其中

定义: dp [ i ] [ j ] \color{red}\textit{dp}[i][j] dp[i][j] 表示从数组的 [ 0 , i ] [0,i] [0,i] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j。初始时, dp \textit{dp} dp 中的全部元素都是 false \text{false} false

在定义状态之后,需要考虑边界情况。以下两种情况都属于边界情况。

  • 如果不选取任何正整数,则被选取的正整数等于 0。因此对于所有 0 ≤ i < n 0 \le i < n 0i<n,都有 dp [ i ] [ 0 ] = true \textit{dp}[i][0]=\text{true} dp[i][0]=true

  • i = = 0 i==0 i==0 时,只有一个正整数 nums [ 0 ] \textit{nums}[0] nums[0] 可以被选取,因此 dp [ 0 ] [ nums [ 0 ] ] = true \textit{dp}[0][\textit{nums}[0]]=\text{true} dp[0][nums[0]]=true

对于 i > 0 i>0 i>0 j > 0 j>0 j>0 的情况,如何确定 dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 的值?需要分别考虑以下两种情况。

  • 如果 j ≥ nums [ i ] j \ge \textit{nums}[i] jnums[i],则对于当前的数字 nums [ i ] \textit{nums}[i] nums[i],可以选取也可以不选取,两种情况只要有一个为 true \text{true} true,就有 dp [ i ] [ j ] = true \textit{dp}[i][j]=\text{true} dp[i][j]=true
    • 如果不选取 nums [ i ] \textit{nums}[i] nums[i],则 dp [ i ] [ j ] = dp [ i − 1 ] [ j ] \textit{dp}[i][j]=\textit{dp}[i-1][j] dp[i][j]=dp[i1][j]
    • 如果选取 nums [ i ] \textit{nums}[i] nums[i],则 dp [ i ] [ j ] = dp [ i − 1 ] [ j − nums [ i ] ] \textit{dp}[i][j]=\textit{dp}[i-1][j-\textit{nums}[i]] dp[i][j]=dp[i1][jnums[i]]
  • 如果 j < nums [ i ] j < \textit{nums}[i] j<nums[i],则在选取的数字的和等于 j 的情况下无法选取当前的数字 nums [ i ] \textit{nums}[i] nums[i],因此有 dp [ i ] [ j ] = dp [ i − 1 ] [ j ] \textit{dp}[i][j]=\textit{dp}[i-1][j] dp[i][j]=dp[i1][j]

状态转移方程如下:
dp [ i ] [ j ] = { dp [ i − 1 ] [ j ]   ∣   dp [ i − 1 ] [ j − nums [ i ] ] , j ≥ nums [ i ] dp [ i − 1 ] [ j ] , j < nums [ i ] \textit{dp}[i][j]=\begin{cases} \textit{dp}[i-1][j]~|~\textit{dp}[i-1][j-\textit{nums}[i]], & j \ge \textit{nums}[i] \\ \textit{dp}[i-1][j], & j < \textit{nums}[i] \end{cases} dp[i][j]={dp[i1][j]  dp[i1][jnums[i]],dp[i1][j],jnums[i]j<nums[i]

最终得到 dp [ n − 1 ] [ target ] \textit{dp}[n-1][\textit{target}] dp[n1][target] 即为答案。

bool canPartition(vector<int>& nums) {
        int n = nums.size();
        if (n < 2) {
            return false;
        }
        int sum = 0, maxNum = 0;
        for (auto& num : nums) {
            sum += num;
            maxNum = max(maxNum, num);
        }
        if (sum & 1) {
            return false;
        }
        int target = sum / 2;
        if (maxNum > target) {
            return false;
        }
        vector<int> dp(target + 1, 0);
        dp[0] = true;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            for (int j = target; j >= num; --j) {
                dp[j] |= dp[j - num];
            }
        }
        return dp[target];
    }

复杂度分析

  • 时间复杂度: O ( n × target ) O(n \times \textit{target}) O(n×target),其中 n 是数组的长度, target \textit{target} target 是整个数组的元素和的一半。需要计算出所有的状态,每个状态在进行转移时的时间复杂度为 O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( target ) O(\textit{target}) O(target),其中 target \textit{target} target 是整个数组的元素和的一半。空间复杂度取决于 dp \textit{dp} dp 数组,在不进行空间优化的情况下,空间复杂度是 O ( n × target ) O(n \times \textit{target}) O(n×target),在进行空间优化的情况下,空间复杂度可以降到 O ( target ) O(\textit{target}) O(target)

2305. (二进制状态压缩、DP)公平分发饼干

给你一个整数数组 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 的分发方案。
 

提示:

2 <= cookies.length <= 8
1 <= cookies[i] <= 105
2 <= k <= cookies.length

阅读提示:请注意「前 i i i 个」和「第 i i i 个」的区别,前者用来表示状态,后者用于参与状态转移。

定义 f [ i ] [ j ] \color{red}f[i][j] f[i][j] 表示 i i i 个孩子 分配的饼干 集合 j j j 时, i i i 个孩子的不公平程度的最小值。

下文中 j ∖ s j \setminus s js 表示从集合 j j j 中去掉集合 s s s 的元素后,剩余元素组成的集合。

考虑给第 i i i 个孩子分配的饼干集合为 s s s,设集合 s s s 的元素和为 sum [ s ] \textit{sum}[s] sum[s],分类讨论:

  • 如果 sum [ s ] > f [ i − 1 ] [ j ∖ s ] \textit{sum}[s] > f[i-1][j \setminus s] sum[s]>f[i1][js],说明给第 i i i 个孩子分配的饼干比前面的孩子多,不公平程度变为 sum [ s ] \textit{sum}[s] sum[s]
  • 如果 sum [ s ] ≤ f [ i − 1 ] [ j ∖ s ] \textit{sum}[s] \le f[i-1][j \setminus s] sum[s]f[i1][js],说明给第 i i i 个孩子分配的饼干没有比前面的孩子多,不公平程度不变,仍为 f [ i − 1 ] [ j ∖ s ] f[i-1][j \setminus s] f[i1][js]

因此,给第 i i i 个孩子分配饼干集合 s s s 后,前 i i i 个孩子的不公平程度为
max ⁡ ( f [ i − 1 ] [ j ∖ s ] , sum [ s ] ) \max(f[i-1][j \setminus s], \textit{sum}[s]) max(f[i1][js],sum[s])
枚举 j j j 的所有子集 s s s,则有
f [ i ] [ j ] = min ⁡ s ⊆ j max ⁡ ( f [ i − 1 ] [ j ∖ s ] , sum [ s ] ) f[i][j]=\min_{s\subseteq j} \max(f[i-1][j \setminus s], \textit{sum}[s]) f[i][j]=sjminmax(f[i1][js],sum[s])

代码实现时,我们可以用一个二进制数来表示集合,其第 i i i 位为 1 1 1 表示分配了第 i i i 块饼干,为 0 0 0 表示未分配第 i i i 块饼干。

此外通过倒序枚举 j,f 的第一个维度可以省略。 sum \textit{sum} sum 也可以通过预处理得到。

踩坑合集:

  1. 获取 二进制x 中 某一位 的元素:
    if j&(1<<p)!=0: dp[0][j] += cookies[p] (左移1)不好
    if (j>>p)&1 ==1: dp[0][j] += cookies[p] (右移x)好 √
  2. 取 集合 j ,子集 p 的补集 s^j

cpp版本

    int distributeCookies(vector<int>& cookies, int k) {
        int n = cookies.size();
        int N = 1 << n;
        vector<int> sum(N);
        /* 求不同子集组合下, 分到的饼干数量 */
        for (int i = 0; i < N; i++) {
            int sumCookies = 0;
            for (int j = 0; j < n; j++) {
                if (((i >> j) & 1) == 1) {
                    sumCookies += cookies[j];
                }
            }
            sum[i] = sumCookies;
        }
        vector<vector<int>> f(n, vector<int>(N));
        f[0] = sum;
        /* dp递推 */
        for (int i = 1; i < k; i++) {
            for (int j = 1; j < N; j++) {
                int mi = INT_MAX;
                for (int s = j; s>0; s = (s - 1) & j) { /* 枚举j的子集s */
                    mi = min(mi, max(f[i-1][j ^ s], sum[s]));
                }
                f[i][j] = mi;
            }
        }
        return f[k-1][N-1];
    }

py版本

    def distributeCookies(self, cookies: List[int], k: int) -> int:
        n = len(cookies)
        m = (1<<n)
        dp = [[0]*(m) for _ in range(k)]
        for j in range(m):
            for p in range(n):
                if j&(1<<p)!=0: dp[0][j] += cookies[p]
                if (j>>p)&1 ==1: dp[0][j] += cookies[p]

        for i in range(1, k):
            for j in range(1,m):
                pre = 100000000
                for p in range(j):
                    pre = min(pre, max(dp[i-1][j ^ p], dp[0][p]))
                dp[i][j] = pre
        return dp[k-1][m-1]

复杂度分析

  • 时间复杂度: O ( k ⋅ 3 n ) O(k\cdot 3^n) O(k3n),其中 n n n cookies \textit{cookies} cookies 的长度。由于元素个数为 i i i 的集合有 C ( n , i ) C(n,i) C(n,i) 个,其子集有 2 i 2^i 2i 个,根据二项式定理, ∑ C ( n , i ) 2 i = ( 2 + 1 ) n = 3 n \sum C(n,i)2^i = (2+1)^n = 3^n C(n,i)2i=(2+1)n=3n,所以枚举所有 j j j 的所有子集 s s s 的时间复杂度为 O ( 3 n ) O(3^n) O(3n)
  • 空间复杂度: O ( 2 n ) O(2^n) O(2n)

剑指 Offer II 102. 加减的目标值

给定一个正整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:

输入:nums = [1], target = 1
输出:1
 

提示:

1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

解法1:二叉树回溯,py超时,cpp通过

    void dfsFTS(vector<int>& nums, int &target, int &cnt, int cur, int res){
        if (res==0 && cur==nums.size()) cnt++;
        else if (cur<nums.size()){
            dfsFTS(nums, target, cnt, cur+1, res+nums[cur]);
            dfsFTS(nums, target, cnt, cur+1, res-nums[cur]);
        }
    }
    int findTargetSumWays(vector<int>& nums, int target) {
        int cnt = 0;
        dfsFTS(nums, target, cnt, 0, target);
        return cnt;
    }

复杂度分析

  • 时间复杂度: O ( 2 n ) O(2^n) O(2n),其中 n n n 是数组 nums \textit{nums} nums 的长度。回溯需要遍历所有不同的表达式,共有 2 n 2^n 2n
    种不同的表达式,每种表达式计算结果需要 O ( 1 ) O(1) O(1) 的时间,因此总时间复杂度是 O ( 2 n ) O(2^n) O(2n)
  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n n n

解法二:背包dp

计算背包重量
记数组的元素和为 sum \textit{sum} sum,添加 - \texttt{-} - 号的元素之和为 neg \textit{neg} neg,则其余添加 + \texttt{+} + 的元素之和为 sum − neg \textit{sum}-\textit{neg} sumneg,得到的表达式的结果为
( sum − neg ) − neg = sum − 2 ⋅ neg = target (\textit{sum}-\textit{neg})-\textit{neg}=\textit{sum}-2\cdot\textit{neg}=\textit{target} (sumneg)neg=sum2neg=target

neg = sum − target 2 \textit{neg}=\dfrac{\textit{sum}-\textit{target}}{2} neg=2sumtarget

由于数组 nums \textit{nums} nums 中的元素都是非负整数, neg \textit{neg} neg 也必须是非负整数,所以上式成立的前提是 sum − target \textit{sum}-\textit{target} sumtarget 是非负偶数。若不符合该条件可直接返回 0 0 0

若上式成立,问题转化成在数组 nums \textit{nums} nums 中选取若干元素,使得这些元素之和等于 neg \textit{neg} neg,计算选取元素的方案数。我们可以使用动态规划的方法求解。

定义二维数组 dp \color{red}定义二维数组 \textit{dp} 定义二维数组dp,其中 dp [ i ] [ j ] \color{red}\textit{dp}[i][j] dp[i][j] 表示在数组 nums \textit{nums} nums i i i个数中选取元素,使得这些元素之和等于 j j j 的方案数。假设数组 nums \textit{nums} nums 的长度为 n n n,则最终答案为 dp [ n ] [ neg ] \textit{dp}[n][\textit{neg}] dp[n][neg]

base:
当没有任何元素可以选取时,元素和只能是 0 0 0,对应的方案数是 1 1 1,因此动态规划的边界条件是:
dp [ 0 ] [ j ] = { 1 , j = 0 0 , j ≥ 1 \textit{dp}[0][j]=\begin{cases} 1, & j=0 \\ 0, & j \ge 1 \end{cases} dp[0][j]={1,0,j=0j1

1 ≤ i ≤ n 1 \le i \le n 1in 时,对于数组 nums \textit{nums} nums 中的第 i i i 个元素 num \textit{num} num i i i 的计数从 1 1 1 开始),遍历 0 ≤ j ≤ neg 0 \le j \le \textit{neg} 0jneg,计算 dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 的值:

  • 如果 j < num j < \textit{num} j<num,则不能选 num \textit{num} num,此时有 dp [ i ] [ j ] = dp [ i − 1 ] [ j ] \textit{dp}[i][j] = \textit{dp}[i - 1][j] dp[i][j]=dp[i1][j]

  • 如果 j ≥ num j \ge \textit{num} jnum,则如果不选 num \textit{num} num,方案数是 dp [ i − 1 ] [ j ] \textit{dp}[i - 1][j] dp[i1][j],如果选 num \textit{num} num,方案数是 dp [ i − 1 ] [ j − num ] d p [ i − 1 ] [ j − n u m ] \textit{dp}[i - 1][j - \textit{num}]dp[i−1][j−num] dp[i1][jnum]dp[i1][jnum],此时有 dp [ i ] [ j ] = dp [ i − 1 ] [ j ] + dp [ i − 1 ] [ j − num ] \textit{dp}[i][j] = \textit{dp}[i - 1][j] + \textit{dp}[i - 1][j - \textit{num}] dp[i][j]=dp[i1][j]+dp[i1][jnum]

因此状态转移方程如下:
dp [ i ] [ j ] = { dp [ i − 1 ] [ j ] , j < nums [ i ] dp [ i − 1 ] [ j ] + dp [ i − 1 ] [ j − nums [ i ] ] , j ≥ nums [ i ] \textit{dp}[i][j]=\begin{cases} \textit{dp}[i - 1][j], & j<\textit{nums}[i] \\ \textit{dp}[i - 1][j] + \textit{dp}[i - 1][j - \textit{nums}[i]], & j \ge \textit{nums}[i] \end{cases} dp[i][j]={dp[i1][j],dp[i1][j]+dp[i1][jnums[i]],j<nums[i]jnums[i]

最终得到 dp [ n ] [ neg ] \textit{dp}[n][\textit{neg}] dp[n][neg] 的值即为答案。

由此可以得到空间复杂度为 O ( n × neg ) O(n \times \textit{neg}) O(n×neg) 的实现。

    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        n = len(nums)
        numSum = sum(nums)
        neg = numSum - target
        if neg<0 or neg%2==1: return 0
        else: neg //= 2
        dp = [[0]*(neg+1) for _ in range(n+1)]
        dp[0][0] = 1

        for i in range(1, n+1):
            for j in range(neg+1):
                if j<nums[i-1]: dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]] 
        return dp[n][neg]
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int& num : nums) {
            sum += num;
        }
        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int n = nums.size(), neg = diff / 2;
        vector<vector<int>> dp(n + 1, vector<int>(neg + 1));
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= num) {
                    dp[i][j] += dp[i - 1][j - num];
                }
            }
        }
        return dp[n][neg];
    }

树型DP(实际上跟背包dp类似,不是按顺序遍历)

树型DP即在上进行DP。

树是无环图,顺序可以是从叶子到根节点,也可以从根到叶子节点。

一般树型DP的特征很明显,即状态可以表示为树中的节点,每个节点的状态可以由其子节点状态转移而来(从叶子到根的顺序),或是由其父亲节点转移而来(从根到叶节点的顺序),也可是两者结合。

找出状态和状态转移方程仍然是树型DP的关键。

剑指 Offer II 103. 最少的硬币数目

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1
示例 2:

输入:coins = [2], amount = 3
输出:-1
示例 3:

输入:coins = [1], amount = 0
输出:0
示例 4:

输入:coins = [1], amount = 1
输出:1
示例 5:

输入:coins = [1], amount = 2
输出:2
 

提示:

1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104

该问题可建模为以下优化问题:
min ⁡ x ∑ i = 0 n − 1 x i  subject to ∑ i = 0 n − 1 x i × c i = S \min_{x} \sum_{i=0}^{n - 1} x_i \ \text{subject to} \sum_{i=0}^{n - 1} x_i \times c_i = S xmini=0n1xi subject toi=0n1xi×ci=S

其中, S S S 是总金额, c i c_i ci 是第 i i i 枚硬币的面值, x i x_i xi 是面值为 c i c_i ci 的硬币数量,由于 x i × c i x_i \times c_i xi×ci 不能超过总金额 S S S,可以得出 x i x_i xi 最多不会超过 S c i \frac{S}{c_i} ciS,所以 x i x_i xi 的取值范围为 [ 0 , S c i ] [{0, \frac{S}{c_i}}] [0,ciS] ( 背包容量 \color{red}背包容量 背包容量)。

方法一:一维背包dp(为什么式)
我们采用自下而上的方式进行思考。
定义 F ( i ) \color{red}定义 F(i) 定义F(i) 为组成金额 (背包容量): i \color{red} (背包容量):i (背包容量):i 所需 最少的硬币数量,假设在计算 F ( i ) F(i) F(i) 之前,我们已经计算出 F ( 0 )   t o   F ( i − 1 ) F(0)\ to\ F(i-1) F(0) to F(i1) 的答案。 则 F ( i ) F(i) F(i) 对应的转移方程应为
F ( i ) = min ⁡ j = 0 … n − 1 F ( i − c j ) + 1 ​ F(i)= \min_{j=0…n−1} F(i-c_j) + 1 ​ F(i)=j=0n1minF(icj)+1​

其中 c j c_j cj 代表的是第 j j j 枚硬币的面值,即我们枚举最后一枚硬币面额是 c j c_j cj,那么需要从 i − c j i-c_j icj 这个金额的状态 F ( i − c j ) F(i-c_j) F(icj) 转移过来,再算上枚举的这枚硬币数量 1 1 1 的贡献,由于要硬币数量最少,所以 F ( i ) F(i) F(i)为前面能转移过来的状态的最小值加上枚举的硬币数量 1 1 1

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        coins.sort()
        dp = [float('inf')]*(amount+1)
        dp[0] = 0
        for i in range(1, amount+1):
            for it in coins:
                if i-it<0: break
                dp[i] = min(dp[i], dp[i-it]+1)
        return dp[amount] if dp[amount]!=float('inf') else -1

方法二:记忆化递归

一个简单的解决方案是通过回溯的方法枚举每个硬币数量子集 [ x 0 …   x n − 1 ] [x_0\dots\ x_{n - 1}] [x0 xn1],针对给定的子集计算它们组成的金额数,如果金额数为 S S S,则记录返回合法硬币总数的最小值,反之返回 − 1 -1 1

该做法的时间复杂度为 O ( S n ) O(S^n) O(Sn),会超出时间限制,因此必须加以优化。

剑指 Offer II 104. 排列的数目

给定一个由 不同 正整数组成的数组 nums ,和一个目标整数 target 。请从 nums 中找出并返回总和为 target 的元素组合的个数。数组中的数字可以在一次排列中出现任意次,但是顺序不同的序列被视作不同的组合。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:

输入:nums = [9], target = 3
输出:0
 

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums 中的所有元素 互不相同
1 <= target <= 1000
 

进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?
如果允许负数出现,需要向题目中添加哪些限制条件?

我的方法:记忆化递归
int dfs(vector<int>& nums, int tgt, vector<int> vis:返回 val=tgt 的结点,能够有多少种符合条件的组合

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        def dfs(nums: List[int], tgt:int, vis:List[int])->int:
            if tgt==0:
                return 1
            can = 0
            for it in nums:
                if tgt-it<0: continue
                if vis[tgt-it]==-1:
                    can += dfs(nums, tgt-it, vis)
                else:
                    can += vis[tgt-it]
                vis[tgt] = can # 父节点 = 所有子节点 符合条件数之和
            return can
        vis = [-1]*(target+1)
        ans = dfs(nums, target, vis)
        return ans

复杂度:O(树的结点个数)
我的法二:背包dp(树型dp)
背包容量:target
dp定义:dp[i] 表示,容量为 i 的背包,能返回的符合题意的最多组合数
转移方程:
d p [ i ] = ∑ j ∈ n u m s d p [ i − j ] , if i >= j dp[i] = \sum_{j \in nums} dp[i-j], \text{if i >= j} dp[i]=jnumsdp[ij],if i >= j

    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0]*(target+1)
        dp[0] = 1
        for i in range(1, target+1):
            for it in nums:
                if i-it>=0:
                    dp[i] += dp[i-it]
        return dp[target]

复杂度分析

  • 时间复杂度: O ( target × n ) O(\textit{target} \times n) O(target×n),其中 target \textit{target} target 是目标值, n n n 是数组 nums \textit{nums} nums 的长度。需要计算长度为 target + 1 \textit{target}+1 target+1 的数组 dp \textit{dp} dp 的每个元素的值,对于每个元素,需要遍历数组 nums \textit{nums} nums 之后计算元素值。
  • 空间复杂度: O ( target ) O(\textit{target}) O(target)。需要创建长度为 target + 1 \textit{target}+1 target+1 的数组 dp \textit{dp} dp

进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?

  • 会导致,target 不是背包容量的上确界。也从而会导致无限长度的排列(只要满足 a+b = 0,无限次重复即可)
  • 如果允许负数出现,则必须限制排列的最大长度,避免出现无限长度的排列,才能计算排列数。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值