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

python 贪心-dfs-dp

贪心


一、什么是贪心算法?

一句话:

每一步都做“当前看来最优”的选择,不考虑将来,不回头改。

特点:

  1. 局部最优 → 期望得到全局最优

    • 每一步选一个“看起来最划算”的选择;
    • 不像动态规划那样枚举所有子状态。
  2. 不回溯

    • 一旦做了选择就不改了,所以实现上经常就是:

      • 排序 + 一次线性扫描。
  3. 不是所有问题都适合贪心。
    想用贪心,通常要满足两个性质:

    • 贪心选择性质
      存在一种最优解,它的第一步就是我们贪心选的那个。
    • 最优子结构
      做了这步选择之后,剩下的子问题的最优解 + 当前选择 = 整体最优解。

二、例题 1:区间调度 / 活动选择(经典贪心)

题目描述

有 n 个活动,每个活动有开始时间 s[i] 和结束时间 e[i]
一个人同一时间只能进行一个活动,问:最多能参加多少个活动?

例如:

活动编号:  1   2   3   4   5
开始时间:  1   3   0   5   8
结束时间:  4   5   6   7   9

常见错误思路(反例)

  • 开始时间最早 排序选?不一定对。
  • 持续时间最短 排序选?也不一定对。

正确的贪心策略

结束时间从小到大排序,然后依次选择 能参加且结束最早 的活动。

理由直观一点说:

  • 越早结束,就给后面的活动留的时间越多;
  • 不管你前面选择了哪个能够参加的活动,只要是“在最早结束时间之前结束”的,
    换成“结束时间更早的那个”,一定不会让已经选的个数变少,后面只会更宽裕。

具体步骤

  1. 把所有活动按结束时间 e[i] 升序排序;

  2. 从头到尾扫一遍:

    • 维护一个变量 last_end = 上一个被选中活动的结束时间;
    • 如果当前活动的开始时间 s[i] >= last_end,就选它,并更新 last_end = e[i]

举例

活动:(start, end)

(1, 4), (3, 5), (0, 6), (5, 7), (8, 9)

按结束时间排序(其实已经是):

  1. 选 (1,4),last_end=4
  2. (3,5) 的 start=3 < last_end=4 → 冲突,跳过
  3. (0,6) 的 start=0 < 4 → 冲突,跳过
  4. (5,7) 的 start=5 ≥ 4 → 可以选,last_end=7
  5. (8,9) 的 start=8 ≥ 7 → 可以选

最后选了 3 个活动: (1,4), (5,7), (8,9)

Python 实现

from typing import List, Tupledef max_activities(intervals: List[Tuple[int, int]]) -> int:# intervals: [(start, end), ...]# 1. 按结束时间排序intervals.sort(key=lambda x: x[1])count = 0last_end = float('-inf')# 2. 贪心地选结束最早且不冲突的活动for s, e in intervals:if s >= last_end:count += 1last_end = ereturn count# 测试
intervals = [(1, 4), (3, 5), (0, 6), (5, 7), (8, 9)]
print(max_activities(intervals))  # 输出 3

这个问题是教科书级别的贪心成功案例:可以严格证明贪心的正确性。


三、例题 2:硬币找零(展示贪心何时不适用)

题目描述

给你几种硬币面值,比如:
中国常用的 [1, 2, 5, 10]
问:给定一个金额 amount,用最少枚数凑出这个金额(假设一定能凑出来)。

贪心策略

每次都优先用 面值最大的硬币,能用几枚就用几枚,然后再考虑次大的,直到凑完。

在很多实际货币系统中(比如常见的 1,2,5,10…),这招是对的。

例如金额 18:

  • 先用 10 → 剩 8
  • 再用 5 → 剩 3
  • 再用 2 → 剩 1
  • 再用 1 → 结束
    总共 4 枚硬币。

Python 贪心写法

def coin_change_greedy(coins, amount):coins.sort(reverse=True)  # 从大到小count = 0for coin in coins:if amount == 0:break# 该面值最多能用多少枚use = amount // coincount += useamount -= use * coinreturn count if amount == 0 else -1  # -1 表示无法凑出

但贪心不总是对的:反例

设计一种面额系统:coins = [1, 3, 4],amount = 6。

  • 贪心:

    • 先用 4 → 剩 2
    • 再用 1 → 剩 1
    • 再用 1 → 剩 0
      → 一共 3 枚(4 + 1 + 1)
  • 但最优解:

    • 用两枚 3:3 + 3 = 6
      → 只需要 2 枚!

这里贪心就错了,因为“每次拿最大面值”这个局部最优,并不能保证全局最优。

这道题如果面额任意给出,一般要用 动态规划 才能保证正确。


四、小结:怎么判断一个问题能不能用贪心?

一个比较实用的经验:

  1. 首先大胆想一个“看起来很自然”的贪心策略:

    • 比如:最早结束、最小权重、最大价值密度、最短时间、最小消耗……
  2. 然后尝试:

    • 造反例:看是否能构造一个数据,使得贪心得到的结果明显不是最优;

    • 如果很难造反例,再尝试用“交换论证”证明:

      • 假设存在一个最优解不是从贪心选择开始;
      • 然后一步一步把它“交换”成以贪心选择开头的另一组解,且不变差;
      • 得出:存在最优解以贪心选择开头 → 贪心策略是安全的。

深度优先


一、DFS 是什么?

核心一句话:

沿着一条路一直走到走不动,再回头换下一条路。

特点:

  • 先走深,再回头(和 BFS 的“按层走”相反)

  • 常用实现方式:

    • 递归(函数自己调用自己)
    • 显式栈(stack.append() / stack.pop()

典型应用场景:

  • 图 / 树的遍历(搜索路径、连通块)
  • 迷宫寻路
  • 回溯类题(组合、排列、n 皇后,其实本质也是 DFS)

二、例题 1:图的遍历(DFS 输出访问顺序)

题目描述

给你一个无向图,用邻接表表示。
从某个起点 start 出发,用 DFS 输出访问节点的顺序。

例如图:

1 -- 2 -- 3
|    |
4 -- 5

邻接表可能写成:

graph = {1: [2, 4],2: [1, 3, 5],3: [2],4: [1, 5],5: [2, 4]
}

递归版 DFS

def dfs_recursive(graph, start, visited=None):if visited is None:visited = set()visited.add(start)print(start, end=' ')   # 这里就是“访问”这个点,你也可以做别的事for nei in graph[start]:if nei not in visited:dfs_recursive(graph, nei, visited)# 测试
graph = {1: [2, 4],2: [1, 3, 5],3: [2],4: [1, 5],5: [2, 4]
}
dfs_recursive(graph, 1)   # 一种可能输出:1 2 3 5 4

执行过程(直观理解):

  • 从 1 出发 → 走 2 → 再走 3,3 没路了回退 → 回到 2,再走 5 → 再走 4……
  • 每次“走到没路为止,再退回上一个点”,非常符合“深度优先”。

非递归(显式栈)版 DFS

def dfs_stack(graph, start):visited = set()stack = [start]   # 用列表模拟栈while stack:node = stack.pop()   # 取栈顶if node in visited:continuevisited.add(node)print(node, end=' ')# 把邻居压栈(可以反向压栈以控制访问顺序)for nei in reversed(graph[node]):if nei not in visited:stack.append(nei)# 测试
dfs_stack(graph, 1)

三、例题 2:网格 DFS —— 岛屿数量 (Number of Islands)

这是力扣非常经典的一题,DFS 的标准应用。

题目描述(简化版)

给一个由 '1''0' 组成的二维网格,

  • '1' 表示陆地
  • '0' 表示海水
    上、下、左、右相邻的 '1' 视为同一块岛屿。

求:岛屿的数量

示例:

grid =
1 1 0 0
1 0 0 1
0 0 1 1这里有 3 个岛:
- 左上角 2×2 那块连在一起算 1 个岛
- 中间 (2,2) 单独一个岛
- 右下角 2 个 1 连在一起算 1 个岛

解题思路(用 DFS “淹没岛屿”)

  • 扫描整个网格,每发现一个没有访问过的 '1'

    • 岛屿计数 +1
    • 从这个点出发做 DFS,把这个岛上所有相连的 '1' 都标记为已访问(或者直接改成 '0',表示“淹掉”)
  • 这样,后面再遇到这个岛上的格子就不会重复计数了。

递归 DFS 版本(Python)

from typing import Listdef num_islands(grid: List[List[str]]) -> int:if not grid:return 0rows, cols = len(grid), len(grid[0])def dfs(r: int, c: int):# 越界或遇到水就停止if r < 0 or r >= rows or c < 0 or c >= cols:returnif grid[r][c] == '0':return# 将当前陆地标记为“水”,避免重复访问grid[r][c] = '0'# 向四个方向继续 DFSdfs(r - 1, c)  # 上dfs(r + 1, c)  # 下dfs(r, c - 1)  # 左dfs(r, c + 1)  # 右count = 0for r in range(rows):for c in range(cols):if grid[r][c] == '1':  # 找到新岛屿count += 1dfs(r, c)          # 把整个岛都淹掉return count# 测试
g = [list("1100"),list("1001"),list("0011")
]
print(num_islands(g))  # 输出 3

这里 DFS 的角色:

  • 把“从某个点连出去的整块连通区域”全部走一遍(并标记);
  • 这就是 DFS 在“连通块计数”里的经典作用。

四、DFS 与回溯的关系(顺便一嘴)

你前面问过 n 皇后、回溯,其实:

回溯搜索 = 一种 带状态记录和恢复 的 DFS。

  • DFS 是“遍历图/树”的一般套路;
  • 回溯是在 DFS 上加了“试探 + 撤销”(比如试着放一个皇后、不行再拿掉)的逻辑。

五、小结

  • DFS 的核心:沿着一个方向走到头 → 回退 → 换方向

  • 通常用递归实现最自然,也可以手写栈。

  • 典型题型:

    • 图遍历、强连通分量、拓扑排序辅助
    • 树的先序/后序遍历
    • 网格搜索(岛屿数量、迷宫、连通块)
    • 各种回溯题(组合、排列、数独、n 皇后…)

动态规划

我直接用**“怎么想 + 两个经典例题 + Python 代码”**来讲动态规划(DP),看完你就能开始自己写简单 DP 题了。


一、动态规划是啥?

一句话版:

把大问题拆成小问题,小问题答案存起来,避免重复算,从而高效地得到最优解(或计数、最大值等)。

关键有两点:

  1. 重叠子问题

    • 同一个子问题会被反复计算,比如递归求 Fibonacci 时 f(30) 里会多次算 f(10)f(11) ……
    • DP 就是:算一次记下来,下次直接用。
  2. 最优子结构

    • 大问题的最优解可以由小问题的最优解组合出来。
    • 比如最长上升子序列长度,dp[i] 可以由前面 dp[j] 推出来。

写 DP 的套路一般是 5 步:

  1. 确定状态:dp[i] 表示什么?
  2. 写转移方程:dp[i] 怎么由前面的状态转移来?
  3. 初始化:起点的 dp 值是什么?
  4. 遍历顺序:i 是从小到大还是反着,内外层循环谁在前?
  5. 答案位置:最终答案是 dp[n] 还是 max(dp) 之类?

二、例题 1:爬楼梯(入门 DP)

题目

有一个楼梯一共 n 级台阶,每次你可以爬 1 级或者 2 级,问一共有多少种不同的爬法?

比如:

  • n = 1:只有 1 种(1)

  • n = 2:2 种(1+1,2)

  • n = 3:

    • 1+1+1
    • 1+2
    • 2+1
      → 共 3 种

1. 状态定义

dp[i] 表示:爬到第 i 级台阶的不同走法数量

2. 状态转移

走到第 i 级,有两种可能的“最后一步”:

  • 倒数第二步在 i-1 级,这一步跨 1 级 → 从 dp[i-1] 转移来
  • 倒数第二步在 i-2 级,这一步跨 2 级 → 从 dp[i-2] 转移来

所以:

[
dp[i] = dp[i-1] + dp[i-2]
]

这其实和 Fibonacci 一样。

3. 初始化

  • dp[0] = 1(可以理解为在地面啥也不做只有 1 种)
  • dp[1] = 1(只有迈一步)

4. 遍历顺序

i 从 2 往上算到 n:

for i in range(2, n+1):dp[i] = dp[i-1] + dp[i-2]

5. 答案

dp[n] 就是答案。

Python 实现

def climb_stairs(n: int) -> int:if n <= 1:return 1dp = [0] * (n + 1)dp[0] = 1dp[1] = 1for i in range(2, n + 1):dp[i] = dp[i - 1] + dp[i - 2]return dp[n]# 测试
print(climb_stairs(3))  # 3
print(climb_stairs(4))  # 5

你可以把 dp 数组打印出来看看每一项代表啥。


三、例题 2:0-1 背包问题(经典“最优值”DP)

题目(0-1 背包)

n 个物品,每个物品有:

  • 体积 w[i]
  • 价值 v[i]

有一个容量为 W 的背包,每个物品最多选一次(要么不选,要么选一次),
问:在不超过容量 W 的前提下,能得到的最大价值是多少?

例子:

物品: i   1   2   3
体积w:    2   3   4
价值v:    4   5   6
背包容量 W = 5
  • 可以选 1 + 2 号:体积 2+3=5,价值 4+5=9
  • 也可以选 1 + 3 号:体积 2+4=6 超了,不行
  • 也可以只选 3 号:体积 4,价值 6

所以最优是 9。


1. 状态定义

这里用比较标准的二维写法(更好理解):

  • dp[i][j] 表示:在只考虑前 i 个物品容量为 j 的情况下,所能得到的最大价值。

注意:i 表示物品数量,j 表示容量


2. 状态转移方程

对于第 i 个物品(下标从 1 开始记),有两种选择:

  1. 不选物品 i
    那最大价值就是:不考虑 i 时的最大价值,
    dp[i-1][j]
  2. 选物品 i(前提:装得下,即 j >= w[i]
    那你就要占用 w[i] 的容量,得到 v[i] 的价值 + 背包剩余容量 j - w[i] 时前面 i-1 个物品的最大价值:
    dp[i-1][j - w[i]] + v[i]

所以:

[
dp[i][j] = \max\Big( dp[i-1][j],\ \ dp[i-1][j - w[i]] + v[i] \Big)
]

j < w[i] 时,装不下,只能不选:

[
dp[i][j] = dp[i-1][j]
]


3. 初始化

  • dp[0][j] = 0:0 个物品时,无论容量多大,价值都是 0。
  • dp[i][0] = 0:容量为 0,装不了东西。

4. 遍历顺序

  • 外层 i: 从 1 到 n
  • 内层 j: 从 0 到 W
for i in range(1, n+1):for j in range(0, W+1):...

5. 答案

dp[n][W] 就是答案:考虑所有 n 个物品,容量为 W 时的最大价值。


Python 实现(二维写法)

def knapsack_01(weights, values, W):"""weights: 物品体积列表,比如 [2,3,4]values:  物品价值列表,比如 [4,5,6]W:       背包容量"""n = len(weights)# dp[i][j]: 前 i 个物品、容量 j 时的最大价值dp = [[0] * (W + 1) for _ in range(n + 1)]for i in range(1, n + 1):w = weights[i - 1]v = values[i - 1]for j in range(0, W + 1):# 不选第 i 个物品dp[i][j] = dp[i - 1][j]# 选第 i 个物品(前提是容量够)if j >= w:dp[i][j] = max(dp[i][j], dp[i - 1][j - w] + v)return dp[n][W]# 测试
weights = [2, 3, 4]
values = [4, 5, 6]
W = 5
print(knapsack_01(weights, values, W))  # 输出 9

你也可以打印 dp 表,看“从左上往右下填表”的过程,更直观。


四、动态规划的一般套路总结(背下来真的有用)

解题时按这个顺序想:

  1. 状态定义:dp[i] / dp[i][j] 表示什么?

    • 计数?最小值?最大值?是否可行?
  2. 转移方程:dp[i] 怎么由前面的状态推出来?

    • 画图、列举小规模情况,找规律。
  3. 初始条件:dp[0] / dp[1] / dp[0][] / dp[][0] 等是多少?

  4. 遍历顺序:i/j 从小到大还是从大到小?

    • 有时如 0-1 背包用一维数组就要倒序;二维则按顺序。
  5. 答案:在 dp 的哪个位置?

    • 可能是 dp[n],dp[n][W],也可能是 max(dp) 等。

http://www.dtcms.com/a/618681.html

相关文章:

  • Android Studio - 使用 BuildConfig
  • 在ec2上部署Qwen2.5omini和Qwen3omini模型
  • 设备通信的艺术:从协议选型、性能调优到自定义实现的全维度技术实践
  • 过滤器模式、责任链模式
  • 做货源的网站郑州企业免费建站
  • HCIP笔记5--OSPF域间路由、虚链路、认证
  • Java 黑马程序员学习笔记(进阶篇27)
  • 海南网站推广建设温州市城市基础设施建设网站
  • CentOS 7 安装 unzip-6.0-21.el7.x86_64.rpm 步骤详解(附安装包)
  • 审计局网站建设管理创意设计广告
  • Goer-Docker系列-1-使用kubectl命令部署Web应用
  • php网站超市响应式网站建设方案
  • 家用路由器挑选指南:考量无线协议与Wi-Fi 7新技术参数
  • 站长工具网站提交沈阳网站优化怎么做
  • SAP FICO成本分解功能
  • 前端实现扫描大屏幕二维码上传图片并同步显示方案
  • 免费域名网站wordpress 修改子主题
  • 二叉树的前序遍历
  • 网站规划建设与管理维护课后答案四平市城乡建设局网站
  • 『大模型部署』NVIDIA Orin + bnb量化 + Qwen3-VL | 4bit、8bit量化
  • 网站建设状况vue快速搭建网站
  • 深入数据库性能优化:从参数调优到RAC高可用架构构建
  • MaxTex下载及LaTex环境配置
  • HttpServletResponse 详细指南
  • 网站建设3a模型是什么意思即墨网站建设哪家好
  • 为什么网站设计很少全屏网络维护难吗
  • 北京做网站的公司商集客电话专业app开发设计的公司
  • SpringCache缓存
  • kubernetes基于sealos工具快速安装指导
  • Linux 信号机制