剑指Offer专项-排序&回溯&动态规划
第十二章 排序
74.合并区间
难度中等
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
res = []
for i in intervals:
# 不合并
if res == [] or res[-1][1] < i[0]:
res.append(i)
# 合并
else:
res[-1] = [res[-1][0], max(i[1],res[-1][1])]
return res
- 时间复杂度:O(nlogn),除去排序的开销只需线性扫描一次,所以主要的时间开销是排序的 O(nlog n)。
- 空间复杂度:O(logn),排序所需要的空间复杂度。
75.数组相对排序(计数排序)
难度简单
给定两个数组,arr1
和 arr2
,
arr2
中的元素各不相同arr2
中的每个元素都出现在arr1
中
对 arr1
中的元素进行排序,使 arr1
中项的相对顺序和 arr2
中的相对顺序相同。未在 arr2
中出现过的元素需要按照升序放在 arr1
的末尾。
class Solution:
def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
res = []
count = [0] * (max(arr1) + 1)
for a1 in arr1:
count[a1] += 1
for a2 in arr2:
res.extend([a2] * count[a2])
count[a2] = 0
for i in range((max(arr1) + 1)):
if count[i] > 0:
res.extend([i] * count[i])
return res
- 时间复杂度:O(m + n)。
- 空间复杂度:O(max(arr1) )。
快速排序
# 快速排序
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
self.sort(nums, 0, len(nums)-1)
return nums
def sort(self, nums, start, end):
if end > start:
mid = self.partition(nums, start, end)
self.sort(nums, start, mid-1)
self.sort(nums, mid+1, end)
return mid
# 分区
def partition(self, nums, start, end):
# 随机选取中间值
randomIdx = choice(range(start, end+1))
# 将中间值移到末尾
nums[randomIdx], nums[end] = nums[end], nums[randomIdx]
# 中间值之前的都是小于中间值,后面都是大于中间值
p1 = start - 1
for p2 in range(start, end+1):
if nums[p2] <= nums[end]:
p1 += 1
nums[p1], nums[p2] = nums[p2], nums[p1]
# 返回中间值的索引
return p1
76.数组中的第 k 大的数字
难度中等
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
- 快速排序 :时间复杂度O(n);空间复杂度O(1)。
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
n = len(nums)
start, end = 0, n - 1
index = self.partition(nums, start, end)
# 返回第k大的值,不需全部排序
# 只需nums[n-k]之前的值比它小,之后比它大。
while index != n - k:
if index < n - k:
start = index + 1
else:
end = index - 1
index = self.partition(nums, start, end)
return nums[index]
# 分区
def partition(self, nums, start, end):
# 随机选取中间值
randomIdx = choice(range(start, end+1))
# 将中间值移到末尾
nums[randomIdx], nums[end] = nums[end], nums[randomIdx]
# 中间值之前的都是小于中间值,后面都是大于中间值
p1 = start - 1
for p2 in range(start, end+1):
if nums[p2] <= nums[end]:
p1 += 1
nums[p1], nums[p2] = nums[p2], nums[p1]
# 返回中间值的索引
return p1
- 堆排序
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
heap = []
for i in nums:
if len(heap) < k:
heapq.heappush(heap, i)
else:
heapq.heappushpop(heap, i)
return heap[0]
- 时间复杂度:O(nlogn)。
- 空间复杂度:O(logn)。
77.链表排序(归并排序)
难度中等
给定链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def sortList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
head1 = head
head2 = self.split(head)
sort1 = self.sortList(head1)
sort2 = self.sortList(head2)
return self.merge(sort1,sort2)
# 从链表中间将链表分为两个链表,并返回后面链表的头节点
def split(self, head):
dummy = ListNode(0, head)
slow = dummy
fast = dummy
while fast and fast.next:
slow = slow.next
fast = fast.next.next
new_head = slow.next
slow.next = None
return new_head
# 合并两个有序链表
def merge(self, head1, head2):
dummy = ListNode()
p = dummy
while head1 and head2:
if head1.val <= head2.val:
p.next = head1
head1 = head1.next
else:
p.next = head2
head2 = head2.next
p = p.next
p.next = head1 if head1 else head2
return dummy.next
-
时间复杂度:O(nlogn)。
-
空间复杂度:O(logn)。
78.合并排序链表
难度困难
给定一个链表数组,每个链表都已经按升序排列。
请将所有链表合并到一个升序链表中,返回合并后的链表。
- 最小堆思路合并k个有序链表
- 时间复杂度:O(nlogk)。
- 空间复杂度:O(k)。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
if lists == []:
return
dummy = ListNode()
p = dummy
priority_queue = []
# 遍历k个链表头结点,插入最小堆
for k in range(len(lists)):
if lists[k]:
heapq.heappush(priority_queue, (lists[k].val, k))
lists[k] = lists[k].next
# 二叉堆中元素构建新链表
while priority_queue:
val, k = heapq.heappop(priority_queue)
p.next = ListNode(val)
p = p.next
if lists[k]:
heapq.heappush(priority_queue, (lists[k].val, k))
lists[k] = lists[k].next
return dummy.next
- 归并排序思路和并k个链表
- 时间复杂度:O(nlogk);
- 空间复杂度:O(logk)。
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
if lists == []:
return
start, end = 0, len(lists)-1
return self.merge(lists, start, end)
def merge(self, lists, start, end):
# 迭代终止条件,只剩一个链表
if start == end:
return lists[start]
# 归并排序思路
mid = start + (end-start) // 2
head1 = self.merge(lists, start, mid)
head2 = self.merge(lists, mid+1, end)
return self.mergeTwoLists(head1, head2)
def mergeTwoLists(self, head1, head2):
dummy = ListNode()
p = dummy
while head1 and head2:
if head1.val < head2.val:
p.next = head1
head1 = head1.next
else:
p.next = head2
head2 = head2.next
p = p.next
p.next = head1 if head1 else head2
return dummy.next
第十三章 回溯法
79.所有子集
难度中等
给定一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def backtrack(start, []):
res.append(path[:])
for i in range(start, n):
path.append(nums[i])
# 递归遍历下一层回溯树
# 避免重复使用元素,参数i加1
backtrack(i+1, [])
path.pop()
res, n = [], len(nums)
backtrack(0, [])
return res
80.含有 k 个元素的组合
难度中等
给定两个整数 n
和 k
,返回 1 ... n
中所有可能的 k
个数的组合。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def backtrack(start, path):
if len(path) == k:
res.append(path[:])
return
for i in range(start, n):
path.append(i+1)
backtrack(i+1, path)
path.pop()
res = []
backtrack(0, [])
return res
81. 允许重复选择元素的组合
难度中等
给定一个无重复元素的正整数数组 candidates
和一个正整数 target
,找出 candidates
中所有可以使数字和为目标数 target
的唯一组合。
candidates
中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的唯一组合数少于 150
个。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(start, target):
# base case,找到目标和,记录结果
if target == 0:
res.append(path[:])
return
# base case,超过目标和,停止向下遍历
if target < 0:
return
for i in range(start, n):
path.append(candidates[i])
# 递归遍历下一层回溯树, 同一元素可重复使用,参数填入i
# 可重复选与不可重复选,只有参数ℹ️那里变化
backtrack(i, target-candidates[i])
path.pop()
n, res = len(candidates), []
path = []
backtrack(0, target)
return res
82.含有重复元素集合的组合
难度中等
给定一个可能有重复数字的整数数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(start, target):
# base case,达到目标和,找到符合条件的组合
if target == 0:
res.append(path[:])
return
# base case,超过目标和,直接结束
if target < 0:
return
for i in range(start, n):
# 剪枝,值相同的树枝,只遍历第一条
if i > start and candidates[i] == candidates[i-1]:
continue
path.append(candidates[i])
backtrack(i+1, target-candidates[i])
path.pop()
# 先排序,让相同的元素靠在一起
candidates.sort()
n, res = len(candidates), []
path = []
backtrack(0, target)
return res
83.没有重复元素集合的全排列
难度中等
给定一个不含重复数字的整数数组 nums
,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def backtrack(path):
if len(path) == n:
res.append(path[:])
return
for i in nums:
if i in path:
continue
path.append(i)
backtrack(path)
path.pop()
res, n = [], len(nums)
backtrack([])
return res
84.含有重复元素集合的全排列
难度中等
给定一个可包含重复数字的整数集合 nums
,按任意顺序 返回它所有不重复的全排列。
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def backtrack(path):
if len(path) == n:
res.append(path[:])
return
for i in range(n):
if used[i] == True:
continue
# 剪枝,同时保证相同元素在排列中的相对位置保持不变
# 注意used[i - 1] == False这个条件
if i > 0 and nums[i] == nums[i-1] and used[i - 1] == False:
continue
path.append(nums[i])
used[i] = True
backtrack(path)
used[i] = False
path.pop()
# 先排序,让相同的元素靠在一起
nums.sort()
res, n = [], len(nums)
# 记录是否使用过该数字
used = [False]*n
backtrack([])
return res
85.生成匹配的括号
难度中等
正整数 n
代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
def backtrack(left, right, path):
if left == n and right == n:
res.append(''.join(path[:]))
return
if right > left or left > n:
return
for i in ['(', ')']:
if i == '(':
path.append('(')
backtrack(left+1, right, path)
path.pop()
else:
path.append(')')
backtrack(left, right+1, path)
path.pop()
res = []
backtrack(0, 0, [])
return res
86.分割回文子字符串
难度中等
给定一个字符串 s
,请将 s
分割成一些子串,使每个子串都是 回文串 ,返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
class Solution:
def partition(self, s: str) -> List[List[str]]:
n = len(s)
# dp[i][j]表示s[i:j+1]是否是回文串
dp = [[False] * n for _ in range(n)]
for i in range(n-1, -1, -1):
dp[i][i] = True
for j in range(i, n):
if s[i] == s[j] and (i+1 >=j or dp[i+1][j-1]):
dp[i][j] = True
def backtrack(start, path):
if start == n:
res.append(path[:])
return
for i in range(start, n):
if dp[start][i]:
path.append(s[start:i+1])
backtrack(i+1, path)
path.pop()
res = []
backtrack(0, [])
return res
87.复原 IP
难度中等
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能从 s
获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 思路:其实和上题没太大区别,只是判断ip合法性那里有区别。
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
# 判断s能否是IP地址的一部分
def isValid(s):
if s[0] == '0':
return True if s == '0' else False
else:
return True if int(s)<=255 else False
def backtrack(start, path, piece):
if start == n and piece == 4:
res.append('.'.join(path))
return
# 提前停止
if piece >= 4:
return
for i in range(start, n):
if isValid(s[start:i+1]) and piece < 4:
path.append(s[start:i+1])
backtrack(i+1, path, piece+1)
path.pop()
n, res = len(s), []
backtrack(0, [], 0)
return res
第十四章 动态规划
88.爬楼梯的最少成本
难度简单
数组的每个下标作为一个阶梯,第 i
个阶梯对应着一个非负数的体力花费值 cost[i]
(下标从 0
开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
- 迭代法:时间复杂度O(n),空间复杂度O(n)。
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0] * (n+1)
for i in range(2, n+1):
dp[i] = min(dp[i-2] + cost[i-2], dp[i-1] + cost[i-1])
return dp[n]
- 优化空间:时间复杂度O(n),空间复杂度O(1)。
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0, 0]
for i in range(2, n+1):
dp[i%2] = min(dp[i%2] + cost[i-2], dp[(i+1)%2]+cost[i-1])
return dp[n%2]
89.房屋偷盗
难度中等
一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums
,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
- 迭代法:时间复杂度O(n),空间复杂度O(n)。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
dp = [0] * n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, n):
# 偷和不偷两种选择
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
return dp[n-1]
- 优化空间:时间复杂度O(n),空间复杂度O(1)。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
dp = [0] * 2
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, n):
# 偷和不偷两种选择
dp[i%2] = max(dp[i%2]+nums[i], dp[(i+1)%2])
return dp[(n-1)%2]
90.环形房屋偷盗
难度中等
一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组 nums
,请计算 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
- 第一个屋子偷不偷两种情况分别求一次,最后取较大的那个
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
dp = [[0] * n for _ in range(2)]
dp[0][0] = nums[0]
dp[1][0] = 0
for i in range(1, n):
dp[0][i] = max(dp[0][i-1], dp[0][i-2]+nums[i])
dp[1][i] = max(dp[1][i-1], dp[1][i-2]+nums[i])
dp[0][n-1] = dp[0][n-2]
return max(dp[0][n-1], dp[1][n-1])
- 定义一个函数
class Solution:
def rob(self, nums: List[int]) -> int:
def helper(start, end):
dp = [0] * n
# 第一个房屋偷和不偷分别有不同的初始化
dp[0] = nums[start]
dp[1] = max(nums[start], nums[start+1])
for i in range(2, n-1):
# 注意这里的i和nums差一位
dp[i] = max(dp[i-2]+nums[start+i], dp[i-1])
# 1.偷第一个房屋最后一个一定不能偷,此时最高金额就是dp[n-2];
# 2.不偷第一个房屋,此时最高金额返回dp最后一个,也是dp[n-2],
# 因为从索引1开始,dp数组长度正好比第一种情况少1
return dp[n-2]
n = len(nums)
if n < 3:
return max(nums)
# 第一个房屋偷和不偷两种情况求最值
return max(helper(0, len(nums)), helper(1, len(nums)))
91.粉刷房子
难度中等
假如有一排房子,共 n
个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3
的正整数矩阵 costs
来表示的。
例如,costs[0][0]
表示第 0 号房子粉刷成红色的成本花费;costs[1][2]
表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
- 迭代法:时间复杂度O(n),空间复杂度O(n)。
class Solution:
def minCost(self, costs: List[List[int]]) -> int:
n = len(costs)
# dp[0][i]、dp[1][i]和dp[2][i]分别表示第i个房子刷红色、蓝色和绿色的最少成本
dp = [[0]* n for _ in range(3)]
# 初始化
for i in range(3):
dp[i][0] = costs[0][i]
for i in range(1, n):
for j in range(3):
# 第i个房子第j个颜色所需最小成本是第i-1个房子刷另外两个颜色的最小成本+本次要用的成本
dp[j][i] = min(dp[(j+1)%3][i-1], dp[(j+2)%3][i-1]) + costs[i][j]
# 最后一个房子分别用三个颜色,返回成本最低值
return min(dp[0][n-1], dp[1][n-1], dp[2][n-1])
- 优化空间:时间复杂度O(n),空间复杂度O(1)。
class Solution:
def minCost(self, costs: List[List[int]]) -> int:
n = len(costs)
# 第i%2个房子刷红色、蓝色和绿色的最少成本用dp[0][i%2]、dp[1][i%2]和dp[2][i%2]分别表示
dp = [[0]* 2 for _ in range(3)]
# 初始化
for i in range(3):
dp[i][0] = costs[0][i]
for i in range(1, n):
for j in range(3):
# 第i个房子第j个颜色所需最小成本是第i-1个房子刷另外两个颜色的最小成本+本次要用的成本
dp[j][i%2]=min(dp[(j+1)%3][(i-1)%2],dp[(j+2)%3][(i-1)%2]) + costs[i][j]
# 最后一个房子分别用三个颜色,返回成本最低值
return min(min(dp[0][(n-1)%2], dp[1][(n-1)%2]), dp[2][(n-1)%2])
92.翻转字符
难度中等
如果一个由 '0'
和 '1'
组成的字符串,是以一些 '0'
(可能没有 '0'
)后面跟着一些 '1'
(也可能没有 '1'
)的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 '0'
和 '1'
组成的字符串 s,我们可以将任何 '0'
翻转为 '1'
或者将 '1'
翻转为 '0'
。
返回使 s 单调递增 的最小翻转次数。
class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
n = len(s)
dp = [[0] * n for _ in range(2)]
# dp[0][i]、dp[1][i]分别表示第i个字符翻转为0、1需要最小翻转次数
dp[0][0] = 0 if s[0] == '0' else 1
dp[1][0] = 0 if s[0] == '1' else 1
for i in range(1, n):
if s[i] == '0':
dp[0][i] = dp[0][i-1]
dp[1][i] = min(dp[0][i-1], dp[1][i-1])+1
elif s[i] == '1':
dp[0][i] = dp[0][i-1]+1
dp[1][i] = min(dp[0][i-1], dp[1][i-1])
return min(dp[0][n-1], dp[1][n-1])
93.最长斐波那契数列
难度中等
如果序列 X 1 , X 2 , . . . , X n X_1, X_2, ..., X_n X1,X2,...,Xn 满足下列条件,就说它是 斐波那契式 的:
n >= 3
- 对于所有
i + 2 <= n
,都有 X i + X i + 1 = X i + 2 X_i + X_{i+1} = X_{i+2} Xi+Xi+1=Xi+2
给定一个严格递增的正整数数组形成序列 arr
,找到 arr
中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
class Solution(object):
def lenLongestFibSubseq(self, A):
n = len(A)
memo = {x:i for i, x in enumerate(A)}
# dp[i][j]表示A[i:j+1]最长的斐波那契式子序列的长度
dp = [[2] * n for _ in range(n)]
res = 2
for i in range(n):
for j in range(i+1, n):
if A[j]-A[i] in memo:
k = memo[A[j]-A[i]]
dp[i][j] = dp[k][i] + 1 if k < i else 2
res = max(dp[i][j], res)
return res if res > 2 else 0
94. 最少回文分割
难度困难
给定一个字符串 s
,请将 s
分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。
class Solution:
def minCut(self, s: str) -> int:
n = len(s)
# isValid[i][j] 表示s从i开始到j是否是回文串
isValid = [[False] * n for _ in range(n)]
for i in range(n-1, -1, -1):
isValid[i][i] = True
for j in range(i, n):
if s[i] == s[j] and (i+1>=j or isValid[i+1][j-1]):
isValid[i][j] = True
# dp[i]表示s[:i+1]的最少分割次数,初始化每个字符都分割
dp = [n] * n
for i in range(n):
if isValid[0][i]:
dp[i] = 0
else:
for j in range(1, i+1):
if isValid[j][i]:
dp[i] = min(dp[i], dp[j-1]+1)
return dp[n-1]
95. 最长公共子序列
难度中等
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n1, n2 = len(text1), len(text2)
# dp[i][j]表示text1[:i]和text2[:j]的最长公共子序列
dp = [[0] * (n2+1) for _ in range(n1+1)]
for i in range(n1):
for j in range(n2):
if text1[i] == text2[j]:
dp[i+1][j+1] = dp[i][j]+1
else:
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
return dp[n1][n2]
96. 字符串交织
难度中等
给定三个字符串 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
连接。
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
n1, n2, n3 = len(s1), len(s2), len(s3)
if n1+n2 != n3:
return False
dp = [[False]*(n2+1) for _ in range(n1+1)]
dp[0][0] = True
for i in range(n1):
if s1[i] == s3[i]:
dp[i+1][0] = True
else:
break
for j in range(n2):
if s2[j] == s3[j]:
dp[0][j+1] = True
else:
break
for i in range(n1):
for j in range(n2):
if s1[i] == s2[j] == s3[i+j+1]:
dp[i+1][j+1] = (dp[i][j+1] or dp[i+1][j])
elif s1[i] == s3[i+j+1]:
dp[i+1][j+1] = dp[i][j+1]
elif s2[j] == s3[i+j+1]:
dp[i+1][j+1] = dp[i+1][j]
return dp[n1][n2]
97. 子序列的数目
难度困难
给定一个字符串 s
和一个字符串 t
,计算在 s
的子序列中 t
出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE"
是 "ABCDE"
的一个子序列,而 "AEC"
不是)
题目数据保证答案符合 32 位带符号整数范围。
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
# dp[i][j]表示s[:i]在t[:j]中出现的次数
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0] = 1
for i in range(m):
for j in range(min(i+1, n)):
if s[i] == t[j]:
dp[i+1][j+1] = dp[i][j+1]+dp[i][j]
else:
dp[i+1][j+1] = dp[i][j+1]
return dp[m][n]
- 优化空间
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
dp = [0] * (n+1)
dp[0] = 1
for i in range(m):
for j in range(min(n-1, i), -1, -1):
if s[i] == t[j]:
dp[j+1] += dp[j]
return dp[n]
98.路径的数目
难度中等
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(n+1):
dp[1][i] = 1
for j in range(m+1):
dp[j][1] = 1
for i in range(m):
for j in range(n):
dp[i+1][j+1] = dp[i][j+1] + dp[i+1][j]
return dp[m][n]
- 优化空间
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1]*n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[n-1]
99.最小路径之和
难度中等
给定一个包含非负整数的 m m m x n n n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。**说明:**一个机器人每次只能向下或者向右移动一步。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid),len(grid[0])
# dp[i][j]表示机器人到(i,j)路径上的最小数字和
dp = [[0] * n for _ in range(m)]
dp[0][0] = grid[0][0]
for i in range(1, n):
dp[0][i] = dp[0][i-1] + grid[0][i]
for j in range(1, m):
dp[j][0] = dp[j-1][0] + grid[j][0]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1])+ grid[i][j]
return dp[m-1][n-1]
- 优化空间
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid),len(grid[0])
# dp[j]表示机器人到(0,j)路径上的最小数字和
dp = [0]*n
dp[0] = grid[0][0]
for j in range(1, n):
dp[j] = dp[j-1] + grid[0][j]
for i in range(1, m):
dp[0] += grid[i][0]
for j in range(1, n):
dp[j] = min(dp[j], dp[j-1])+ grid[i][j]
return dp[n-1]
100.三角形中最小路径之和
难度中等
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle[-1])
dp = [[0]*n for _ in range(n)]
dp[0][0] = triangle[0][0]
for i in range(1, n):
dp[i][0] = dp[i-1][0] + triangle[i][0]
dp[i][i] = dp[i-1][i-1] + triangle[i][i]
for i in range(1, n):
for j in range(1, i):
dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + triangle[i][j]
return min(dp[n-1])
- 优化空间
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle[-1])
dp = [0] * n
dp[0] = triangle[0][0]
for i in range(1, n):
dp[i] = dp[i-1] + triangle[i][i]
for j in range(i-1, 0, -1):
dp[j] = min(dp[j], dp[j-1]) + triangle[i][j]
dp[0] += triangle[i][0]
return min(dp)
101.分割等和子集
难度简单
给定一个非空的正整数数组 nums
,请判断能否将这些数字分成元素和相等的两部分。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 == 0:
target = sum(nums) // 2
else:
return False
n = len(nums)
dp = [[False] * (target+1) for _ in range(n)]
# base case背包容量为0时,什么物品都不选择即可装满
for i in range(n):
dp[i][0] = True
for i in range(1, n):
for j in range(1, target+1):
dp[i][j] = dp[i-1][j]
if dp[i][j] == False and j - nums[i]>=0:
dp[i][j] = dp[i-1][j-nums[i]]
return dp[n-1][target]
- 优化空间
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 == 0:
target = sum(nums) // 2
else:
return False
n = len(nums)
dp = [False] * (target+1)
# base case背包容量为0时,什么物品都不选择即可装满
dp[0] = True
for i in range(1, n):
for j in range(target, 0, -1):
if dp[j] == False and j - nums[i]>=0:
dp[j] = dp[j-nums[i]]
return dp[target]
102.加减的目标值
难度中等
给定一个正整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
n = len(nums)
Sum = sum(nums)
if (Sum+target)%2 == 1 or Sum < target:
return 0
target = (Sum - target) //2
dp = [[0] * (target+1) for _ in range(n+1)]
dp[0][0] = 1
for i in range(1, n+1):
for j in range(target+1):
dp[i][j] = dp[i-1][j]
if j-nums[i-1]>=0:
dp[i][j] += dp[i-1][j-nums[i-1]]
return dp[n][target]
- 优化空间
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
n = len(nums)
Sum = sum(nums)
if (Sum+target)%2 == 1 or Sum < target:
return 0
target = (Sum - target) //2
dp = [0] * (target+1)
dp[0] = 1
for i in range(1, n+1):
for j in range(target, -1, -1):
if j-nums[i-1]>=0:
dp[j] += dp[j-nums[i-1]]
return dp[target]
103. 最少的硬币数目
难度中等
给定不同面额的硬币 coins
和一个总金额 amount
。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
# dp[i]表示目标金额为i时最少需要dp[i]枚硬币
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [amount+1]*(amount+1)
# base case
dp[0] = 0
for i in range(amount+1):
for coin in coins:
if i-coin<0:
continue
dp[i] = min(dp[i],dp[i-coin]+1)
if dp[amount]>amount:#注意dp初始化是amout+1,如果大于amount就不能组成总金额,要返回-1
return -1
return dp[amount]
104. 排列的数目
难度中等
给定一个由 不同 正整数组成的数组 nums
,和一个目标整数 target
。请从 nums
中找出并返回总和为 target
的元素组合的个数。数组中的数字可以在一次排列中出现任意次,但是顺序不同的序列被视作不同的组合。
题目数据保证答案符合 32 位整数范围。
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
# dp[i] 表示选取的元素之和等于i的方案数
dp = [0] * (target+1)
dp[0] = 1
for i in range(1, target+1):
for j in nums:
if i-j>=0:
dp[i] += dp[i-j]
return dp[target]