【中级算法】回溯/排序/搜索/动态规划 (中)

目录

四、回溯

4.1 LC 电话号码的字母组合

4.1.1 题求

4.1.2 求解

4.2 LC 括号生成 ☆

4.2.1 题求

4.2.2 求解

4.3 LC 全排列

4.3.1 题求

4.3.2 求解

4.4 LC 子集 ❤

4.4.1 题求

4.4.2 求解

4.5 LC 单词搜索

4.5.1 题求

4.5.2 求解

五、排序和搜索

5.1 LC 颜色分类 ☆

5.1.1 题求

5.1.2 求解

5.2 LC 前 K 个高频元素

5.2.1 题求

5.2.2 求解

5.3 LC 数组中的第K个最大元素

5.3.1 题求

5.3.2 求解

5.4 LC 寻找峰值

5.4.1 题求

5.4.2 求解

5.5 LC 在排序数组中查找元素的第一个和最后一个位置

5.5.1 题求

5.5.2 求解

5.6 LC 合并区间

5.6.1 题求

5.6.2 求解

5.7 LC 搜索旋转排序数组 ❤

5.7.1 题求

5.7.2 求解

5.8 LC 搜索二维矩阵 II

5.8.1 题求

5.8.2 求解

六、动态规划

6.1 LC 跳跃游戏 ☆

6.1.1 题求

6.1.2 求解

6.2 LC 不同路径

6.2.1 题求

6.2.2 求解

6.3 LC 零钱兑换

6.3.1 题求

6.3.2 求解

6.4 LC 最长上升子序列 ☆

6.4.1 题求

6.4.2 求解


四、回溯


4.1 LC 电话号码的字母组合

4.1.1 题求

4.1.2 求解

法一:回溯

# 96.92% - 24ms

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []

        phone = {'2':"abc", '3':"def", '4':"ghi", '5':"jkl", 
                 '6':"mno", '7':"pqrs", '8':"tuv", '9':"wxyz"}
        res = []
        n = len(digits)

        def recur(beg, cur):
            # 长度达标, 记录排列
            if beg == n:
                res.append(''.join(cur))
                return
            # 遍历当前数字对应的所有字母
            for alp in phone[digits[beg]]:
                recur(beg+1, cur+[alp])

        recur(0, [])
        return res

参考资料:

力扣


4.2 LC 括号生成

4.2.1 题求

4.2.2 求解

法一:动态规划+递归

from functools import lru_cache

class Solution:
    @lru_cache(None)
    def generateParenthesis(self, n: int) -> List[str]:
        if n == 0:
            return ['']
            
        res = []
        for i in range(n):
            # 左、右的所有有效组合, 始终满足括号总数为 i + (n-1-i) = n-1
            for left in self.generateParenthesis(i):
                for right in self.generateParenthesis(n-1-i):
                    res.append(f'({left}){right}')
        return res

法二:动态规划+迭代

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        # dp[i] 表示 i 组括号的所有有效组合
        # dp[i] = "(【dp[p]的所有有效组合】) + 【dp[q]对应的有效组合】"
        # p 从 0 遍历到 i-1, 同时 q 则相应从 i-1 到 0, 始终满足 p+q = i-1
        
        # 定义 dp 数组
        dp = [[] for _ in range(n+1)]  # dp[i] 存放第 i 组括号的所有有效组合
        dp[0].append("")               # 初始化 dp[0], 共 0 组括号时无组合
        dp[1].append("()")             # 初始化 dp[1], 共 1 组括号组合唯一

        # i 从 2 遍历到 n
        for i in range(2, n+1):        # 计算 dp[i], 即共有 i 组括号时的所有组合
            # p 从 0 遍历到 i-1
            for p in range(i):    
                # 得到 dp[p] 和 dp[q] 的所有有效组合    
                list1, list2 = dp[p], dp[i-1-p]  
                # 遍历各组合插入到当前 1 组 ( ) 的中、右侧
                for left in list1:
                    for right in list2:
                        # "(" + 【i=p时所有括号的排列组合】+ ")" +【i=q时所有括号的排列组合】
                        # p+q = n-1, 即除了第 1 组 "( )" 外剩下的 n-1 组
                        dp[i].append(f"({left}){right}")  # 各 n 组合 "(" + left + ")" + right
        return dp[n]

参考资料:

力扣


4.3 LC 全排列

4.3.1 题求

4.3.2 求解

法一:回溯+递归

# 78.16% - 32ms

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:

        res = []
        n = len(nums)

        def recur(beg, cur):
            if beg >= n:
                res.append(cur)
                return

            for idx in range(beg, n):
                cur[beg], cur[idx] = cur[idx], cur[beg]
                recur(beg+1, cur.copy())
                cur[idx], cur[beg] = cur[beg], cur[idx]

        recur(0, nums)
        return res

法一改:回溯+递归

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:

        res = []
        n = len(nums)

        def recur(beg):
            if beg >= n:
                res.append(nums[:])  # res.append(nums.copy())  # 优化 - 仅在记录结果时浅拷贝
                return

            for idx in range(beg, n):
                nums[beg], nums[idx] = nums[idx], nums[beg]
                recur(beg+1)
                nums[idx], nums[beg] = nums[beg], nums[idx]

        recur(0)
        return res

参考资料:

力扣


4.4 LC 子集 ❤

4.4.1 题求

4.4.2 求解

法一:迭代

# 88.68% - 28ms

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = [[]]
        for i in range(len(nums)):
            # 每次弹出 nums[:i+1] 的元素构成的所有组合并加入一个新数 nums[i]
            res.extend([r+[nums[i]] for r in res])  
        return res

参考资料:

力扣


4.5 LC 单词搜索

4.5.1 题求

4.5.2 求解

法一:DFS + 回溯

# 70.54% - 3808ms

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m, n, k = len(board), len(board[0]), len(word)
        visited = set()  # 已访问坐标集合

        def dfs(x, y, idx):
            # 越界 + 值不匹配 + 坐标已访问 均为非法情况, 直接返回 False
            if x < 0 or x >= m or y < 0 or y >= n or board[x][y] != word[idx] or (x, y) in visited:
                return False
            idx += 1
            visited.add((x, y))     # 加入当前路径到已访问集合 visited 后继续 DFS
            # 但凡有一路成功, 即返回 True
            if idx == k or dfs(x-1, y, idx) or dfs(x+1, y, idx) or dfs(x, y-1, idx) or dfs(x, y+1, idx):
                return True
            visited.remove((x, y))  # 当前路径下 DFS 无果, 从已访问集合 visited 移除

        # 遍历各起点
        for i in range(m):
            for j in range(n):
                if dfs(i, j, 0):
                    return True
                visited.clear()
        return False

参考资料:

力扣


五、排序和搜索


5.1 LC 颜色分类 ☆

5.1.1 题求

5.1.2 求解

法一:双指针

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        p0, p1, p2 = 0, 0, n - 1  # 左、中、右三指针
        while p1 <= p2:
            # 中指针与右指针交换
            while p1 <= p2 and nums[p1] == 2:
                nums[p1], nums[p2] = nums[p2], nums[p1]
                p2 -= 1
            # 中指针与左指针交换
            while p0 <= p1 and nums[p1] == 0:  # if nums[p1] == 0:
                nums[p1], nums[p0] = nums[p0], nums[p1]
                p0 += 1
            # 中指针移位
            p1 += 1

参考资料:

力扣


5.2 LC 前 K 个高频元素

5.2.1 题求

5.2.2 求解

法一:列表排序

# 95.99% - 32ms

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # num: freq
        hashmap = defaultdict(int)
        for num in nums:
            hashmap[num] += 1

        # freq: num
        array = [(val, key) for key, val in hashmap.items()]
        array.sort(reverse=True)

        # freq topk 
        return [array[j][-1] for j in range(k)]

法二:堆排序

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # num: freq
        hashmap = defaultdict(int)
        for num in nums:
            hashmap[num] += 1

        # heap sort
        array = []
        for key, val in hashmap.items():
            heapq.heappush(array, (-val, key))  # heapq.heapify(array)

        return [heapq.heappop(array)[-1] for _ in range(k)]

法三:快速排序

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # num: freq
        hashmap = defaultdict(int)
        for num in nums:
            hashmap[num] += 1

        # quick sort - https://blog.csdn.net/qq_39478403/article/details/119035694
        def quick_sort(lhs, rhs):
            if lhs >= rhs:
                return
            # 从大到小排序
            i, j = lhs, rhs
            while i < j:
                # 必须先 j 再 i - 必须先右再左
                while i < j and array[j][0] <= array[lhs][0]:  # <=
                    j -= 1
                while i < j and array[i][0] >= array[lhs][0]:  # >=
                    i += 1
                array[i], array[j] = array[j], array[i]
            array[i], array[lhs] = array[lhs], array[i]    

            quick_sort(lhs, i-1)
            quick_sort(i+1, rhs)
            return

        array = [(val, key) for key, val in hashmap.items()]
        quick_sort(0, len(array)-1)

        return [array[i][-1] for i in range(k)]

参考资料:

力扣


5.3 LC 数组中的第K个最大元素

5.3.1 题求

5.3.2 求解

法一:部分快速排序

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:

        def quick_sort(lhs, rhs):
            '''
            从大到小排序版 - 若要从小到大排序, 则令两个 while 的 >= 和 <= 对调
            '''
            if lhs >= rhs:
                return
            i, j = lhs, rhs
            while i < j:
                while i < j and nums[j] <= nums[lhs]:
                    j -= 1
                while i > j and nums[i] >= nums[lhs]:
                    i += 1
                nums[i], nums[j] = nums[j], nums[i]
            # 中间位置 i 或 j 和参考位置 lhs 交换
            nums[i], nums[lhs] = nums[lhs], nums[i]
            # 仅对指定范围内排序
            if k <= i:
                quick_sort(lhs, i-1)
            else:
                quick_sort(i+1, rhs)
            return

        n = len(nums)
        quick_sort(0, n-1)
        return nums[k-1]

法二:堆排序 API

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        heapq.heapify(nums)
        return heapq.nlargest(k, nums)[-1]

法三:堆排序

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        heapq.heapify(nums)
        for _ in range(len(nums)-k):
            heapq.heappop(nums)
        return nums[0]  # 小顶堆

参考资料:

力扣


5.4 LC 寻找峰值

5.4.1 题求

5.4.2 求解

# 89.72% - 28ms

class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        # 数组长度
        n = len(nums)

        # 左、右边界试探
        if n == 1 or nums[0] > nums[1]:
            return 0
        elif nums[-1] > nums[-2]:
            return n - 1

        # 中央二分搜索
        lhs, rhs = 1, n - 2
        while lhs <= rhs:
            mid = (lhs + rhs) // 2
            if nums[mid] > nums[mid-1] and nums[mid] > nums[mid+1]:
                return mid
            elif nums[mid-1] > nums[mid] > nums[mid+1]:
                rhs = mid - 1
            # elif nums[mid-1] < nums[mid] < nums[mid+1]: 
            #     lhs = mid + 1
            else:
                lhs = mid + 1  # 在谷底时任挑一个方向

参考资料:

力扣


5.5 LC 在排序数组中查找元素的第一个和最后一个位置

5.5.1 题求

5.5.2 求解

法一:二分搜索

# 99.97% - 16ms

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        n = len(nums)
        lhs, rhs = 0, n - 1
        while lhs <= rhs:
            mid = (lhs + rhs) // 2
            if nums[mid] > target:
                rhs = mid - 1
            elif nums[mid] < target:
                lhs = mid + 1
            else:
                left = right = mid
                while left >= 1 and nums[left-1] == target:
                    left -= 1
                while right <= n-2 and nums[right+1] == target:
                    right += 1
                return [left, right]
        return [-1, -1]

参考资料:

力扣


5.6 LC 合并区间

5.6.1 题求

5.6.2 求解

法一:排序+遍历

# 94.04% - 32ms

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        # 排序 - x 从小到大, y 从大到小
        new = sorted(intervals, key=lambda x: (x[0], -x[1]))
        res = []
        x, y = new[0]
        for i, j in new:
            # 若左边界不同, 分类讨论
            if x != i:
                # 若区间相交, 取较大者为右边界
                if i <= y:
                    y = max(j, y)
                # 否则, 记录当前区间 [x, y], 开始新区间 [i, j]
                else:
                    res.append([x, y])
                    x, y = i, j
        # 收尾
        res.append([x, y])
        return res

法一改:极致简化

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        # 排序 - x 从小到大
        new = sorted(intervals, key=lambda x: x[0])
        res = []
        x, y = new[0]
        for i, j in new:
            # 当且仅当出现新的左边界 i 且当前右边界 y 小于新左边界 i 
            if x != i and i > y:
                res.append([x, y])
                x, y = i, j
            # 否则, 始终取较大者为当前右边界 y
            else:
                y = max(j, y)
        # 收尾
        res.append([x, y])
        return res

参考资料:

力扣


5.7 LC 搜索旋转排序数组 ❤

5.7.1 题求

5.7.2 求解

法一:二分搜索改

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)
        left, right = 0, n - 1
        
        while left <= right:
            # 中间索引
            mid = (left + right) // 2
            
            # 找到目标索引 mid
            if nums[mid] == target:
                return mid
            
            # nums[0] <= nums[mid] 表明前半段 nums[left:mid+1] 有序 ☆
            elif nums[0] <= nums[mid]:
                # target 在前半段 nums[:mid+1]
                if nums[0] <= target < nums[mid]:
                    right = mid - 1
                # target 在后半段 nums[mid:]
                else:
                    left = mid + 1
                    
            # 否则, 去有序的后半段 nums[mid:right+1] 找 ☆       
            else:
                # target 在后半段 nums[mid:]
                if nums[mid] < target <= nums[n-1]:
                    left = mid + 1  
                # target 在前半段 nums[:mid+1]
                else:
                    right = mid - 1
        return -1

参考资料:

力扣


5.8 LC 搜索二维矩阵 II

5.8.1 题求

5.8.2 求解

法一:模拟二叉搜索树

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        # 从左下角坐标开始遍历作为根节点
        m, n = len(matrix), len(matrix[0])
        x, y = m - 1, 0
        while x >= 0 and y < n:
            if matrix[x][y] == target:
                return True
            elif matrix[x][y] < target:
                y += 1
            else:
                x -= 1
        return False

参考资料:

力扣


六、动态规划


6.1 LC 跳跃游戏 ☆

6.1.1 题求

6.1.2 求解

法一:贪心法

class Solution:
    def canJump(self, nums) :
        # 类似青蛙过河问题
        max_idx = 0
        for idx, num in enumerate(nums):
            # 取当前位置 idx 可达的最大步长 idx + num 为下一可达位置 next_idx
            next_idx = idx + num  
            # 若当前位置 idx 是可达的 且 下一可达位置 next_idx 范围更大
            if max_idx >= idx and next_idx > max_idx:
                # 最大可达位置 max_idx
                max_idx = next_idx
        # 最大可达位置 max_idx 能否覆盖终点位置 idx = n - 1
        return max_idx >= idx

参考资料:

力扣


6.2 LC 不同路径

6.2.1 题求

6.2.2 求解

法一:动态规划 - 2D 数组

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0 for _ in range(n)] for _ in range(m)]
        dp[0][0] = 1
        # 第 0 行第 0 列路径唯一
        for i in range(1, m):
            dp[i][0] = dp[i-1][0]
        for j in range(1, n):
            dp[0][j] = dp[0][j-1]
        # 第 i 行第 j 列取决于左、上路径之和
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[-1][-1] 

法二:动态规划 - 1D 数组

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [1 for _ in range(n)]
        for _ in range(1, m):
            for k in range(1, n):
                dp[k] += dp[k-1]
        return dp[-1]

参考资料: 

力扣


6.3 LC 零钱兑换

6.3.1 题求

6.3.2 求解

法一:动态规划(0/1背包问题)

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # dp[i] 表示凑出 i 元所需的最少硬币数
        dp = [float("inf") for _ in range(amount+1)]
        dp[0] = 0
        # 遍历凑钱数 i
        for i in range(1, amount+1):
            # 遍历所用硬币面值 c
            for c in coins:
                # 状态转移 ☆
                diff = i - c
                if diff >= 0:
                    dp[i] = min(dp[i], dp[diff]+1)  
        # 至多 amount, 再多就是凑不齐
        return dp[-1] if dp[-1] != float("inf") else -1

参考资料:

力扣


6.4 LC 最长上升子序列 ☆

6.4.1 题求

6.4.2 求解

法一:贪心+动态规划

# 96.66% - 40ms

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # 简单贪心 + 二分查找
        # 若要使上升子序列尽可能长,则需让序列上升得尽可能慢,
        # 因此希望每次在上升子序列最后加上的数尽可能小
        
        # dp[i] 表示长度为 i 的最长上升子序列的末尾元素的最小值,其关于 i 单调递增
        # len(dp) 表示目前最长上升子序列的长度
        
        # 最后,依次遍历数组 nums 的每个元素,并更新数组 dp 和 len(dp) 的值
        # 若 num > dp[len(dp)] 则更新 len(dp) += 1,否则在 dp[1 ... len(dp)] 中
        # 找满足 dp[i-1] < num < dp[i] 的下标 i,并更新 dp[i] = num
        
        dp = []  
        for num in nums:
            # 二分查找 - 找到插入位置 i
            i = bisect_left(dp, num) 
            # 若插入位置为 dp 数组末,则表示 num 比 dp[-1] 还大,直接加入
            if i == len(dp):
                dp.append(num)  # len(dp) += 1
            # 否则,更新 dp[i],因为 dp[i-1] < num < dp[i],可使数组上升更慢
            else:
                dp[i] = num
        return len(dp)
    
        '''
        以输入序列 [0,8,4,12,2] 为例:  
            第一步插入 0, dp = [0]       # num=0 > len(dp)=0  新增   
            第二步插入 8, dp = [0,8]     # num=8 > len(dp)=1  新增
            第三步插入 4, dp = [0,4]     # 4 取代 8 使序列上升更慢
            第四步插入 12,dp = [0,4,12]  # num=12 > len(dp)=2 新增 
            第五步插入 2, dp = [0,2,12]  # 2 取代 4 使序列上升更慢
        最终得到最大递增子序列长度为 len(dp) = 3
        '''

法一:实现二

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        dp = []
        for num in nums:
            if not dp or num > dp[-1]:  # 尚无元素 或 num 最大
                dp.append(num)
            else:
                # 二分查找 - num 插入 dp[idx] 左侧
                l, r = 0, len(dp) - 1
                idx = r
                while l <= r:
                    mid = (l + r) // 2
                    if num <= dp[mid] :  # <=: num 欲插入 dp[mid] 实现更小值替代
                        idx = mid
                        r = mid - 1
                    else:
                        l = mid + 1
                dp[idx] = num
        return len(dp)

参考资料:

力扣

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值