当前位置: 首页 > news >正文

算法设计三大经典策略:贪心 / 分治 / 动态规划 详解与实战

从局部最优到分而治之,再到全局最优递推,一篇吃透三大算法思想

前言

在算法设计与面试中,有三种策略是绝对绕不开的核心:贪心算法、分治算法、动态规划

它们各自代表了不同的思维方式:

  • 贪心:走一步看一步,选当前最好的

  • 分治:大问题拆小,逐个击破再合并

  • 动态规划:记住过去,用子问题的解推导当前

本文将系统讲解这三种策略的原理、适用条件、经典问题实战,以及它们之间的对比与联系。


目录

  1. 贪心算法

  2. 分治算法

  3. 动态规划

  4. 三大策略对比总结

  5. 如何选择:一张决策图


一、贪心算法 (Greedy)

1.1 核心思想

每一步都选当前看起来最优的选择,期望通过局部最优的累积达到全局最优。

一句话:只顾眼前,不管将来。

1.2 两个关键性质

性质含义
贪心选择性质全局最优解可以通过一系列局部最优选择得到
最优子结构问题的最优解包含子问题的最优解

只有同时满足这两个性质的问题,贪心才能保证最优解。

1.3 经典问题一:活动选择

问题描述:有 n 个活动,每个活动有开始时间 s[i] 和结束时间 f[i]。一个人同一时间只能参加一个活动,求最多能参加多少个活动。

贪心策略:每次选结束时间最早且不冲突的活动。

python

def activity_selection(activities): """ activities: list of (start, end) 返回:最多能参加的活动数量 """ # 按结束时间排序 activities.sort(key=lambda x: x[1]) count = 1 last_end = activities[0][1] for i in range(1, len(activities)): if activities[i][0] >= last_end: count += 1 last_end = activities[i][1] return count # 测试 acts = [(1, 3), (2, 5), (3, 6), (5, 7), (6, 8)] print(activity_selection(acts)) # 输出: 3

为什么正确?结束越早,留给后面的时间越多,可以用交换法证明。

1.4 经典问题二:分数背包

问题描述:有 n 个物品,每个物品有重量 w[i] 和价值 v[i],背包容量为 C。物品可以拆分(拿一部分),求最大价值。

贪心策略:按单位重量价值(v[i]/w[i])从高到低拿。

python

def fractional_knapsack(items, capacity): """ items: list of (value, weight) capacity: 背包容量 """ # 按单位价值降序排序 items.sort(key=lambda x: x[0]/x[1], reverse=True) total_value = 0 remaining = capacity for value, weight in items: if remaining >= weight: # 整个拿走 total_value += value remaining -= weight else: # 拿走剩余容量的一部分 total_value += value * (remaining / weight) break return total_value # 测试 items = [(60, 10), (100, 20), (120, 30)] # (价值, 重量) print(fractional_knapsack(items, 50)) # 输出: 240

1.5 经典问题三:霍夫曼编码

问题描述:给定字符及其出现频率,构造前缀码,使总编码长度最短。

贪心策略:每次合并频率最小的两个节点。

python

import heapq class Node: def __init__(self, freq, ch=None, left=None, right=None): self.freq = freq self.ch = ch self.left = left self.right = right def __lt__(self, other): return self.freq < other.freq def huffman_coding(freq_dict): """ freq_dict: {'a': 45, 'b': 13, ...} 返回:编码字典 """ heap = [Node(freq, ch) for ch, freq in freq_dict.items()] heapq.heapify(heap) while len(heap) > 1: left = heapq.heappop(heap) right = heapq.heappop(heap) parent = Node(left.freq + right.freq, left=left, right=right) heapq.heappush(heap, parent) # 生成编码 codes = {} def dfs(node, code): if node.ch is not None: codes[node.ch] = code return dfs(node.left, code + '0') dfs(node.right, code + '1') dfs(heap[0], '') return codes # 测试 freq = {'A': 45, 'B': 13, 'C': 12, 'D': 16, 'E': 9, 'F': 5} codes = huffman_coding(freq) print(codes)

1.6 贪心的局限性(重要!)

0-1背包问题:物品不能拆分时,贪心失效

python

# 物品:(价值, 重量) items = [(60, 10), (100, 20), (120, 30)] capacity = 50 # 贪心(按单位价值):先拿60(10) + 100(20) = 160,剩余20不够拿120 → 总价值160 # 最优解:不拿60,拿100(20) + 120(30) = 220 print(fractional_knapsack(items, 50)) # 分数背包:240 # 0-1背包最优是220,贪心只得到160,失败!

二、分治算法 (Divide and Conquer)

2.1 核心思想

分而治之:将原问题分解成若干个规模较小、相互独立、与原问题形式相同的子问题,递归求解子问题,然后将子问题的解合并成原问题的解。

一句话:大事化小,小事化了,最后再合并。

2.2 三个步骤

步骤说明
分解 (Divide)将原问题分解成若干个规模较小的子问题
解决 (Conquer)递归地求解子问题。子问题足够小时直接求解
合并 (Combine)将子问题的解合并成原问题的解

2.3 经典问题一:归并排序

python

def merge_sort(arr): if len(arr) <= 1: return arr # 1. 分解 mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) # 2. 合并 return merge(left, right) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result # 测试 arr = [38, 27, 43, 3, 9, 82, 10] print(merge_sort(arr)) # [3, 9, 10, 27, 38, 43, 82]

复杂度:时间 O(n log n),空间 O(n)

2.4 经典问题二:快速排序

python

def quick_sort(arr, left=0, right=None): if right is None: right = len(arr) - 1 if left < right: pivot_index = partition(arr, left, right) quick_sort(arr, left, pivot_index - 1) quick_sort(arr, pivot_index + 1, right) return arr def partition(arr, left, right): pivot = arr[right] # 选最后一个为基准 i = left - 1 for j in range(left, right): if arr[j] <= pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] arr[i + 1], arr[right] = arr[right], arr[i + 1] return i + 1 # 测试 arr = [38, 27, 43, 3, 9, 82, 10] print(quick_sort(arr)) # [3, 9, 10, 27, 38, 43, 82]

2.5 经典问题三:最大子数组和(分治版)

问题:找出数组中连续子数组的最大和。

分治策略:最大子数组要么在左半边,要么在右半边,要么跨越中点。

python

def max_subarray(nums, left, right): if left == right: return nums[left] mid = (left + right) // 2 # 左半边最大 left_max = max_subarray(nums, left, mid) # 右半边最大 right_max = max_subarray(nums, mid + 1, right) # 跨越中点的最大 cross_max = cross_subarray(nums, left, mid, right) return max(left_max, right_max, cross_max) def cross_subarray(nums, left, mid, right): # 从中点向左扩展 left_sum = float('-inf') curr = 0 for i in range(mid, left - 1, -1): curr += nums[i] left_sum = max(left_sum, curr) # 从中点向右扩展 right_sum = float('-inf') curr = 0 for i in range(mid + 1, right + 1): curr += nums[i] right_sum = max(right_sum, curr) return left_sum + right_sum # 测试 nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] print(max_subarray(nums, 0, len(nums) - 1)) # 输出: 6(子数组[4,-1,2,1])

注:这个问题有更高效的 Kadane 算法(动态规划 O(n)),这里仅展示分治思想。

2.6 经典问题四:二分查找

最简单的分治应用:每次将搜索范围减半。

python

def binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1 # 测试 arr = [1, 3, 5, 7, 9, 11] print(binary_search(arr, 7)) # 输出: 3

三、动态规划 (Dynamic Programming)

3.1 核心思想

记住过去,避免重复计算:将原问题分解成若干个子问题,先求解子问题,然后从子问题的解逐步推导出原问题的解。与分治的区别是,子问题之间不是独立的,会有重叠。

一句话:用空间换时间,把重复计算的结果存起来。

3.2 两个关键性质

性质含义
最优子结构问题的最优解包含子问题的最优解
重叠子问题子问题会被反复计算多次

3.3 解题模板(五步法)

text

步骤1:确定dp数组的含义(dp[i]表示什么) 步骤2:确定状态转移方程(如何从子问题推导) 步骤3:初始化边界条件(最小的子问题) 步骤4:确定遍历顺序(从小到大或从上到下) 步骤5:手动演算验证

3.4 经典问题一:斐波那契数列

最简单的 DP 入门问题。

python

def fib(n): if n <= 1: return n # 1. dp数组含义:dp[i] = 第i个斐波那契数 dp = [0] * (n + 1) # 3. 初始化 dp[0], dp[1] = 0, 1 # 4. 遍历顺序:从小到大 for i in range(2, n + 1): # 2. 状态转移方程 dp[i] = dp[i - 1] + dp[i - 2] return dp[n] # 空间优化版(只用两个变量) def fib_optimized(n): if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b print(fib(10)) # 输出: 55

3.5 经典问题二:爬楼梯

问题:每次可以爬 1 或 2 个台阶,爬到 n 阶有多少种不同方法?

python

def climb_stairs(n): if n <= 2: return n dp = [0] * (n + 1) dp[1], dp[2] = 1, 2 for i in range(3, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] print(climb_stairs(5)) # 输出: 8

3.6 经典问题三:0-1背包(最经典的DP)

问题:n 个物品,每个有重量 w[i] 和价值 v[i],背包容量 C,每种物品只能选一次,求最大价值。

python

def knapsack_01(weights, values, capacity): n = len(weights) # dp[i][c] 表示前i个物品,容量c时的最大价值 dp = [[0] * (capacity + 1) for _ in range(n + 1)] for i in range(1, n + 1): w = weights[i - 1] v = values[i - 1] for c in range(1, capacity + 1): if c < w: # 装不下当前物品 dp[i][c] = dp[i - 1][c] else: # 装或不装,取最大值 dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - w] + v) return dp[n][capacity] # 空间优化版(一维数组) def knapsack_01_optimized(weights, values, capacity): n = len(weights) dp = [0] * (capacity + 1) for i in range(n): # 必须倒序遍历,防止重复使用 for c in range(capacity, weights[i] - 1, -1): dp[c] = max(dp[c], dp[c - weights[i]] + values[i]) return dp[capacity] # 测试 weights = [2, 3, 4, 5] values = [3, 4, 5, 6] capacity = 8 print(knapsack_01(weights, values, capacity)) # 输出: 10(选2+3+4? 实际3+5=8? 验证:3+4+5? 容量不对)

3.7 经典问题四:最长递增子序列 (LIS)

问题:找出数组中最长的严格递增子序列的长度。

python

def length_of_lis(nums): if not nums: return 0 n = len(nums) # dp[i] 表示以 nums[i] 结尾的最长递增子序列长度 dp = [1] * n for i in range(n): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) # 更优解法:贪心+二分 O(n log n) def length_of_lis_optimized(nums): import bisect tails = [] for num in nums: pos = bisect.bisect_left(tails, num) if pos == len(tails): tails.append(num) else: tails[pos] = num return len(tails) nums = [10, 9, 2, 5, 3, 7, 101, 18] print(length_of_lis(nums)) # 输出: 4([2,3,7,101]或[2,5,7,101])

四、三大策略对比总结

维度贪心算法分治算法动态规划
核心思想每步选局部最优大拆小,合小成大记录子问题,递推求解
子问题关系独立(一次决策)相互独立重叠依赖
是否需要记录子问题解❌ 不需要❌ 不需要(递归)✅ 需要(DP表)
能否保证全局最优❌ 不一定✅ 保证✅ 保证
时间复杂度O(n log n) 常见O(n log n) 常见O(n²) 或 O(n³) 常见
空间复杂度O(1) 或 O(n)O(log n) 或 O(n)O(n) 或 O(n²)
典型应用活动选择、霍夫曼码、最小生成树归并排序、快排、二分查找背包、LCS、最短路径

五、如何选择?一张决策图

快速判断表

问题特征推荐算法
子问题相互独立 + 可以合并分治
子问题重叠 + 有最优子结构动态规划
每一步选最优 + 不影响后续贪心
子问题独立 + 规模递减明显分治(如二分查找)
需要所有解回溯/DFS
求最优解 + 可分解DP 或 贪心

六、实战练习题推荐

贪心算法

  • LeetCode 55. 跳跃游戏

  • LeetCode 45. 跳跃游戏 II

  • LeetCode 435. 无重叠区间

分治算法

  • LeetCode 53. 最大子数组和

  • LeetCode 169. 多数元素

  • LeetCode 241. 为运算表达式设计优先级

动态规划

  • LeetCode 70. 爬楼梯

  • LeetCode 300. 最长递增子序列

  • LeetCode 1143. 最长公共子序列

  • LeetCode 322. 零钱兑换


七、总结

算法一句话总结适用场景
贪心走一步看一步,选当前最好的有贪心选择性质的问题
分治大问题拆小,逐个击破再合并子问题相互独立
动态规划记住过去,用子问题推导当前子问题重叠 + 最优子结构

学习建议

  1. 先从分治入手,理解递归和问题分解

  2. 再学动态规划,掌握状态转移和递推思想

  3. 最后学贪心,理解其适用条件和局限性

记住:贪心是最快的,但不是总对的;分治是最自然的,但不一定最有效;DP是最通用的,但成本也最高。

http://www.jsqmd.com/news/830165/

相关文章:

  • Hermes Agent框架接入Taotoken自定义供应商的配置要点详解
  • 谷歌 AI 战略多维度推进:Gemini 更新、智能代理与创意 AI 齐头并进
  • 开源AI代码助手本地化部署:从Cursor10x看私有化编程助手实践
  • 专业的PLM系统生产厂家
  • 基于深度学习的苹果产量预测的系统设计与实现
  • 【WinForm UI控件系列】ComboTreeView下拉树选择控件
  • 知乎API开发指南:5分钟掌握Python数据采集的完整解决方案
  • Ragent AI:从 0 到 1 打造企业级 Agentic RAG 智能体
  • 通过curl快速调试stm32项目的大模型api请求与响应格式
  • 新手也能搞定!用Simulink搭建晶闸管直流调速系统(附完整模型文件)
  • Arduino开发环境搭建与LED控制实战:从零开始硬件编程
  • 基于Matlab元胞自动机模拟(CA)动态再结晶过程
  • QQ截图独立版:免费获取专业级屏幕工具集的完整指南
  • 声明式无侵入爬虫框架Clawless:零代码实现网页数据采集
  • 用Ray处理270万条NYC Taxi数据,我总结了这几个提升效率的Parquet读取技巧
  • JetBrains IDE试用期重置完整指南:快速恢复30天免费使用权限
  • CircuitPython物联网开发实战:从点灯到LoRa无线通信
  • java之集合
  • 关于ImToken智能合约交互
  • 如何用开源缠论量化工具实现几何交易可视化:从算法到实战的完整指南
  • 别再让强光干扰你的项目!OpenMV调低曝光度精准捕捉红色激光点(附完整代码)
  • 告别RDP!用PowerShell的Enter-PSSession远程管理Windows服务器,保姆级配置避坑指南
  • UI-TARS桌面版:5分钟打造你的终极AI智能助手完整指南
  • java作业集1-3总结性blog
  • 3招引爆阴阳师百鬼夜行自动化脚本:效率飙升实战秘籍
  • 抖音创作者开源工具箱:数据采集、内容处理与自动化工作流实战
  • RPG Maker游戏资源解密工具:快速提取加密文件的终极指南
  • LeetCode Hot 100 - 爬楼梯完全题解
  • 别再只会用next了!GDB调试实战:用until、finish和jump命令快速定位Linux C/C++程序中的内存泄漏
  • 基于红外对射传感器与Adafruit IO的智能邮箱检测系统实战