Python 动态规划:巧妙解决“放苹果”问题及其代码实现184
在算法的世界里,有这样一类问题,它们表面上看起来像简单的计数或组合,但深入探究,却能引出强大而优雅的解决方案——动态规划(Dynamic Programming, DP)。“放苹果”问题便是其中一个经典的代表。它不仅是面试中常考的题目,更是理解DP思想,特别是整数划分(Integer Partitioning)概念的绝佳切入点。作为一名专业的程序员,熟练掌握这类问题的解法,能够帮助我们更好地解决现实世界中资源分配、组合优化等复杂难题。
本文将深入探讨“放苹果”问题,从其数学本质、递归关系推导到两种主要的Python实现方式:带备忘录的递归(Top-Down DP)和迭代的动态规划(Bottom-Up DP)。通过详尽的代码示例、复杂度分析以及实际应用场景的讨论,力求为读者呈现一篇全面而优质的指南。
一、问题描述:什么是“放苹果”?
“放苹果”问题通常可以这样描述:
假设有 `m` 个完全相同的苹果,和 `n` 个完全相同的盘子。请问,有多少种不同的方法可以将这些苹果放入盘子中?允许有些盘子是空的,但盘子的顺序不作区分。
关键点:
 苹果相同: 比如3个苹果A, B, C,分到盘子里是同一个结果。
 盘子相同: 比如盘子1放2个,盘子2放1个,与盘子1放1个,盘子2放2个是同一种方法。
 允许空盘: 有些盘子可以不放苹果。
举例说明:
 `m = 7` 个苹果,`n = 3` 个盘子。
 
可能的分配方式有:
7 = 7
7 = 6 + 1
7 = 5 + 2
7 = 5 + 1 + 1
7 = 4 + 3
7 = 4 + 2 + 1
7 = 3 + 3 + 1
7 = 3 + 2 + 2
(注意:`6+1` 和 `1+6` 视为同一种,因为盘子相同。同时,`7` 本身也算一种,即一个盘子放7个,其余盘子为空。) 
 `m = 1` 个苹果,`n = 3` 个盘子。
 
只有一种方法:`1` (一个盘子放1个,其余空)。 
 `m = 0` 个苹果,`n = 3` 个盘子。
 
只有一种方法:`0` (所有盘子都为空)。 
这个问题的本质就是整数划分问题的一个变种:将一个整数 `m` 划分成最多 `n` 个(非负)整数之和,且不考虑顺序。
二、递归关系与动态规划思想
解决这类问题的关键在于找到其递归关系。我们定义一个函数 `dp[m][n]`,表示将 `m` 个苹果放入 `n` 个盘子中的方法数。
1. 边界条件(Base Cases):
`dp[0][n] = 1`: 当有0个苹果时,无论有多少个盘子,都只有一种方法(所有盘子都空着)。
`dp[m][0] = 0`: 当有非0个苹果,但盘子数为0时,无法放置,所以方法数为0。
`dp[m][1] = 1`: 当只有1个盘子时,无论有多少苹果,都只有一种方法(把所有苹果都放到这一个盘子里)。
2. 递归关系(Recurrence Relation):
考虑 `m` 个苹果和 `n` 个盘子的情况 `dp[m][n]`。我们可以将其分为两种情况讨论:
情况 A:盘子的数量大于苹果的数量 (`n > m`)
如果盘子的数量 `n` 超过了苹果的数量 `m`,那么至少有 `n - m` 个盘子是空的。由于盘子是相同的,这些多余的空盘子并不会增加新的分配方式。因此,将 `m` 个苹果放入 `n` 个盘子中的方法数,等同于将 `m` 个苹果放入 `m` 个盘子中的方法数。
即:`dp[m][n] = dp[m][m]` (当 `n > m` 时)
情况 B:盘子的数量小于或等于苹果的数量 (`n m` 时)
 `dp[m][n] = dp[m][n-1] + dp[m-n][n]` (当 `n int:
 """
 计算将m个苹果放入n个盘子中的方法数(带备忘录的递归)。
 
 Args:
 m (int): 苹果的数量。
 n (int): 盘子的数量。
 
 Returns:
 int: 放置方法总数。
 """
 # 边界条件
 if m < 0 or n < 0:
 return 0 # 非法输入
 if m == 0:
 return 1 # 0个苹果,1种方法 (所有盘子为空)
 if n == 0:
 return 0 # 有苹果但没有盘子,0种方法
 if n == 1:
 return 1 # 只有1个盘子,1种方法 (所有苹果都放进去)
 
 # 递归关系
 if n > m:
 # 盘子比苹果多,多余的盘子为空,等同于盘子数量和苹果数量一样多
 return count_apple_ways_memo(m, m)
 else: # n <= m
 # 两种情况之和:
 # 1. 至少有一个盘子是空的:相当于m个苹果放入n-1个盘子
 # 2. 所有n个盘子都非空:每个盘子先放一个,剩余m-n个苹果放入n个盘子
 return count_apple_ways_memo(m, n - 1) + count_apple_ways_memo(m - n, n)
# 示例测试
print("--- 带备忘录的递归 ---")
print(f"7个苹果,3个盘子:{count_apple_ways_memo(7, 3)} 种方法") # 预期输出: 8
print(f"1个苹果,1个盘子:{count_apple_ways_memo(1, 1)} 种方法") # 预期输出: 1
print(f"0个苹果,5个盘子:{count_apple_ways_memo(0, 5)} 种方法") # 预期输出: 1
print(f"5个苹果,0个盘子:{count_apple_ways_memo(5, 0)} 种方法") # 预期输出: 0
print(f"5个苹果,5个盘子:{count_apple_ways_memo(5, 5)} 种方法") # 预期输出: 7
print(f"5个苹果,7个盘子:{count_apple_ways_memo(5, 7)} 种方法") # 预期输出: 7 (等同于 5个苹果,5个盘子)
代码解释:
 `@functools.lru_cache(None)`:这是Python标准库提供的一个装饰器,它会自动缓存函数的调用结果。当函数以相同的参数再次被调用时,会直接返回缓存的结果,而不会重新执行函数体。`None`表示没有大小限制。
 函数内部首先处理各种边界条件,确保递归能够正确终止。
 然后,根据 `n` 和 `m` 的关系,应用之前推导出的递归关系。
四、Python 代码实现:迭代的动态规划(Bottom-Up DP)
迭代的动态规划(或称为表格法)是自底向上(Bottom-Up)的实现方式。它通过填充一个二维数组(或DP表)来存储子问题的解,从小规模问题逐步推导出大规模问题的解。这种方法通常更易于理解循环逻辑,并且避免了递归深度限制和函数调用栈的开销。def count_apple_ways_dp(m: int, n: int) -> int:
 """
 计算将m个苹果放入n个盘子中的方法数(迭代的动态规划)。
 
 Args:
 m (int): 苹果的数量。
 n (int): 盘子的数量。
 
 Returns:
 int: 放置方法总数。
 """
 if m < 0 or n < 0:
 return 0 # 非法输入
 
 # dp[i][j] 表示将i个苹果放入j个盘子中的方法数
 # 初始化一个 (m+1) x (n+1) 的二维数组,并全部初始化为0
 dp = [[0] * (n + 1) for _ in range(m + 1)]
 # 填充dp表的边界条件
 # 当有0个苹果时,无论有多少个盘子,都只有1种方法 (所有盘子都为空)
 for j in range(n + 1):
 dp[0][j] = 1
 
 # 当只有1个盘子时 (dp[i][1]),无论有多少苹果,都只有1种方法
 # (注意:dp[0][1] 已经通过 dp[0][j] = 1 初始化了,这里可以省略,
 # 或者从 i=1 开始填充,因为 dp[0][1] = 1 已经满足了)
 for i in range(1, m + 1):
 dp[i][1] = 1
 # 迭代填充dp表
 # i 代表苹果数 (从1到m)
 # j 代表盘子数 (从2到n,因为dp[i][0]和dp[i][1]已初始化)
 for i in range(1, m + 1):
 for j in range(2, n + 1):
 if j > i:
 # 盘子比苹果多,多余的盘子为空,等同于盘子数量和苹果数量一样多
 dp[i][j] = dp[i][i]
 else: # j <= i
 # 两种情况之和:
 # 1. 至少有一个盘子是空的:dp[i][j-1]
 # 2. 所有j个盘子都非空:dp[i-j][j]
 dp[i][j] = dp[i][j - 1] + dp[i - j][j]
 
 return dp[m][n]
# 示例测试
print("--- 迭代的动态规划 ---")
print(f"7个苹果,3个盘子:{count_apple_ways_dp(7, 3)} 种方法") # 预期输出: 8
print(f"1个苹果,1个盘子:{count_apple_ways_dp(1, 1)} 种方法") # 预期输出: 1
print(f"0个苹果,5个盘子:{count_apple_ways_dp(0, 5)} 种方法") # 预期输出: 1
print(f"5个苹果,0个盘子:{count_apple_ways_dp(5, 0)} 种方法") # 预期输出: 0
print(f"5个苹果,5个盘子:{count_apple_ways_dp(5, 5)} 种方法") # 预期输出: 7
print(f"5个苹果,7个盘子:{count_apple_ways_dp(5, 7)} 种方法") # 预期输出: 7 (等同于 5个苹果,5个盘子)
print(f"10个苹果,4个盘子:{count_apple_ways_dp(10, 4)} 种方法") # 预期输出: 41
代码解释:
 `dp = [[0] * (n + 1) for _ in range(m + 1)]`:创建一个 `(m+1) x (n+1)` 大小的二维列表 `dp`。`dp[i][j]` 将存储 `i` 个苹果放入 `j` 个盘子的方法数。
 初始化:
 
 `dp[0][j] = 1`:0个苹果放入任意数量盘子,都只有1种方法(所有盘子空)。
 `dp[i][1] = 1`:任意数量苹果放入1个盘子,都只有1种方法。
 
 
 嵌套循环: 外层循环 `i` 遍历苹果数量从1到 `m`,内层循环 `j` 遍历盘子数量从2到 `n`。
 填充逻辑: 根据前面推导的递归关系 `dp[i][j] = dp[i][i]` (当 `j > i` 时) 或 `dp[i][j] = dp[i][j-1] + dp[i-j][j]` (当 `j 
2025-11-04
PHP数据库连接故障:从根源解决常见难题
https://www.shuihudhg.cn/132183.html
Python数字代码雨:从终端到GUI的沉浸式视觉盛宴
https://www.shuihudhg.cn/132182.html
Java远程数据传输:核心技术、协议与最佳实践深度解析
https://www.shuihudhg.cn/132181.html
Python字符串数字提取指南:高效保留纯数字字符的多种策略与实践
https://www.shuihudhg.cn/132180.html
深入理解Java数组数据读取机制:从基础到高级实践
https://www.shuihudhg.cn/132179.html
热门文章
Python 格式化字符串
https://www.shuihudhg.cn/1272.html
Python 函数库:强大的工具箱,提升编程效率
https://www.shuihudhg.cn/3366.html
Python向CSV文件写入数据
https://www.shuihudhg.cn/372.html
Python 静态代码分析:提升代码质量的利器
https://www.shuihudhg.cn/4753.html
Python 文件名命名规范:最佳实践
https://www.shuihudhg.cn/5836.html