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

一个‘放苹果’问题,我搞懂了动态规划的入门钥匙 | C++实战

从放苹果问题解锁动态规划的核心思维

记得第一次接触动态规划时,我被那些晦涩的状态转移方程搞得晕头转向。直到遇到这个看似简单的"放苹果"问题,一切才豁然开朗。把M个苹果放进N个盘子有多少种分法?这个幼儿园级别的题目,竟然藏着理解DP的黄金钥匙。

1. 问题本质与暴力递归解法

放苹果问题要求计算将M个无差别苹果放入N个无差别盘子的分法数,顺序不同的分法视为相同(如5,1,1和1,5,1算一种)。这实际上是在求正整数M的至多N个部分的划分

1.1 递归思维分解

最直观的方法是暴力递归——将问题分解为更小的子问题:

  1. 基本情况

    • 没有苹果(M=0):只有一种分法——所有盘子为空
    • 只有一个盘子(N=1):只有一种分法——全部苹果放入该盘子
  2. 递归情况

    • 当盘子比苹果多(N>M):至少有N-M个空盘子,去掉它们不影响结果
    • 一般情况:分法数=至少一个盘子空着的情况+所有盘子都有苹果的情况
int countWays(int m, int n) { if (m == 0 || n == 1) return 1; if (n > m) return countWays(m, m); return countWays(m, n-1) + countWays(m-n, n); }

1.2 递归树分析

以M=4,N=3为例,递归调用过程如下:

countWays(4,3) ├── countWays(4,2) // 至少一个盘子空着 │ ├── countWays(4,1) = 1 │ └── countWays(2,2) // 所有盘子都有苹果 │ ├── countWays(2,1) = 1 │ └── countWays(0,2) = 1 └── countWays(1,3) // 所有盘子都有苹果 └── countWays(1,1) = 1

提示:观察递归树会发现大量重复计算,如countWays(2,2)被多次计算,这正是动态规划要优化的关键。

2. 从递归到记忆化搜索

2.1 重叠子问题识别

上述递归解法的时间复杂度是指数级的O(2^min(M,N))。通过打印递归调用,可以发现:

计算countWays(3,2) 计算countWays(3,1) 计算countWays(1,2) 计算countWays(1,1) 计算countWays(1,2) // 重复计算!

2.2 记忆化实现

添加一个二维数组存储已计算结果:

int memo[11][11]; // 根据题目M,N≤10 int countWaysMemo(int m, int n) { if (memo[m][n] != 0) return memo[m][n]; if (m == 0 || n == 1) return 1; if (n > m) { memo[m][n] = countWaysMemo(m, m); } else { memo[m][n] = countWaysMemo(m, n-1) + countWaysMemo(m-n, n); } return memo[m][n]; }

记忆化将时间复杂度降为O(M*N),因为每个子问题只计算一次。

3. 动态规划表格法

3.1 DP表构建

我们可以自底向上填充DP表,其中dp[m][n]表示m个苹果n个盘子的分法数:

M\N1234
01111
11111
21222
31233
41345

填充规则:

  1. dp[0][n] = 1
  2. dp[m][1] = 1
  3. 当n > m时,dp[m][n] = dp[m][m]
  4. 否则,dp[m][n] = dp[m][n-1] + dp[m-n][n]

3.2 C++实现

int countWaysDP(int m, int n) { int dp[11][11] = {0}; // 初始化边界条件 for (int j = 1; j <= n; ++j) dp[0][j] = 1; for (int i = 1; i <= m; ++i) dp[i][1] = 1; for (int i = 1; i <= m; ++i) { for (int j = 2; j <= n; ++j) { if (j > i) { dp[i][j] = dp[i][i]; } else { dp[i][j] = dp[i][j-1] + dp[i-j][j]; } } } return dp[m][n]; }

4. 空间优化与进阶思考

4.1 滚动数组优化

观察状态转移方程,当前行只依赖上一行和当前行前面的值,可将空间复杂度从O(M*N)优化到O(N):

int countWaysOpt(int m, int n) { int dp[11] = {1}; // dp[0] = 1 for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (j > i) continue; dp[j] += dp[j-i]; } } return dp[n]; }

4.2 与其他DP问题的关联

放苹果问题实际上是完全背包问题的特例——背包容量为M,物品重量为1到M,每种物品无限个,求装满背包的组合数。其状态转移方程与以下经典问题同源:

  1. 整数划分:将正整数M表示为若干个正整数之和的方案数
  2. 硬币找零:用无限硬币凑出金额M的方法数
  3. 爬楼梯:每次爬1或2阶,到第M阶的方法数

4.3 变种问题扩展

  1. 盘子不同:如果盘子有区别,分法数为组合数C(M+N-1, N-1)
  2. 不允许空盘:先在每个盘放一个苹果,转化为M-N个苹果放N个盘子
  3. 苹果不同:每个苹果唯一,分法数为N^M(每个苹果有N种选择)
// 不允许空盘的解法 int countWaysNoEmpty(int m, int n) { if (m < n) return 0; return countWaysDP(m - n, n); }

在解决实际项目中的资源分配问题时,我经常发现这些变种比原问题更常见。比如服务器集群的任务分配(相当于"不允许空盘"的变种),或者有优先级的任务调度(相当于"苹果不同"的情况)。

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

相关文章:

  • Google 把 AI 搜索搬进 Windows Google app for desktop 完整上手
  • TBOX安全测试核心要点解析:如何验证通信加密、敏感信息与协议握手?
  • 别再为ESP8266连不上阿里云发愁了!手把手教你用安信可MQTT固件和‘神器’配置工具搞定
  • 别再只用串口助手了!用LabVIEW给STM32F103C8T6做个专属上位机(附完整源码)
  • 从零到一:Stegsolve在CTF图像隐写中的核心功能实战解析
  • AIM 澳亿美热泵烘干机使用寿命长吗? - 中媒介
  • 深入理解STM32F407的USART:异步通信原理与配置细节全解析
  • ccmusic-database应用场景:AI音乐版权监测——识别未授权曲目所属流派特征库
  • VXLAN集中式网关实战:为什么你的eNSP模拟器跑不通跨子网?可能是这些原因
  • Windows平台5款免费RPA工具横向评测:从TinyTask到来也科技
  • 幻境·流金科研辅助:论文插图生成、数据可视化美学增强、期刊格式适配
  • 青少年编程学习对未来职业发展的具体帮助
  • 真石漆耐久性测评? - 中媒介
  • Python 3.12 Special Attribute - 25 - __cached__
  • OpenClaw 微信通道搭建方法 三种部署模式详细讲解
  • WorkshopDL终极指南:3步搞定Steam创意工坊下载难题
  • 从‘奥卡姆剃刀’到‘结构风险’:聊聊机器学习模型设计中的‘简单’哲学与TensorFlow/Keras实战调参
  • Java 流程控制语句详解(第3-4课时)
  • 抖音视频批量下载与智能管理终极指南:为什么90%的内容创作者都在使用这个免费工具?
  • 从Kaggle到公司项目:高手们都在用的Baseline思维,到底比你强在哪?
  • 掌握nvme-cli:高性能NVMe存储设备管理终极指南
  • 用LayaAir IDE和TypeScript打造你的三国杀动态皮肤本地播放器(附完整代码)
  • 3步掌握AI抠图神器:ComfyUI-BiRefNet-ZHO让图片视频背景去除更简单
  • 跨越数字孤岛:Go语言赋能壹信即时通讯源码,解锁开源im系统与即时通讯app定制的私域增长密码 - 壹软科技
  • Premiere抠像合成避坑指南:为什么你的绿幕边缘总有杂色?从Alpha通道解释到输出设置的完整流程
  • 保姆级教程:用FPGA/树莓派实测MIPI CSI-2摄像头数据流(附波形分析)
  • linux处理工具(json)
  • 从油气勘探到城市安全:地震波技术如何跨界守护地下空间?
  • Hermes Agent 本地部署从安装到 Telegram 控制,再到环境踩坑排障
  • 如何高效处理通达信数据:完整解析与实用指南