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

动态规划的“生成”之美:三路指针,优雅构建「丑数」序列

哈喽,各位,我是前端小L。

我们的DP之旅,已经探索了求最优解、求路径、求可能性的各种问题。那些问题,大多是在一个“给定”的输入上,进行分析和计算。今天,我们将迎来一种全新的DP范式——“生成式DP”。我们的目标,不再是分析一个现成的序列,而是要亲手生成、构建一个满足特定规则的序列。

这,就是“丑数II”。它将向我们展示,动态规划如何像一个精密的“数字工厂”,按照“2, 3, 5”这三条核心生产线,源源不断地、按顺序地制造出我们想要的数字。

力扣 264. 丑数 II

https://leetcode.cn/problems/ugly-number-ii/

题目分析: “丑数”是指只包含质因数 2, 3, 5 的正整数。1 通常被认为是第一个丑数。 我们的目标是,找到第 n 个丑数。

前几个丑数是 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, ... 7 不是丑数(因为它有质因数7)。11 也不是。

核心洞察: 每一个丑数(除了1),都必然是由另一个更小的丑数乘以 2, 3, 或 5 得到的。

  • 2 = 1 * 2

  • 3 = 1 * 3

  • 4 = 2 * 2

  • 5 = 1 * 5

  • 6 = 2 * 3 (或者 3 * 2)

这个“递推”的性质,是DP的沃土。

思路一:常规武器——最小堆 (Priority Queue)

既然每个丑数都能生成3个更大的“候选”丑数,而我们又总是想要当前“最小”的那个,最小堆这个数据结构就自然而然地浮现在我们脑海中。

算法流程:

  1. 初始化一个最小堆,并将第一个丑数 1 放入。

  2. 为了防止重复(比如 2*33*2 都会生成 6),我们还需要一个哈希集合 Set 来记录已经入过堆的数。

  3. 循环 n 次: a. 从堆中弹出最小的元素 current_ugly。这就是我们按顺序找到的第 i 个丑数。 b. 将 current_ugly 分别乘以 2, 3, 5,得到三个新的候选丑数。 c. 对于每个新的候选丑数,如果它没在 Set 里出现过,就把它加入堆和 Set

评价: 这个方法是正确的,思路也很清晰。时间复杂度是 O(n log n)(每次堆操作是log n),空间复杂度是 O(n)。这是一个非常不错的通用解法,但在追求极致的我们看来,还有提升空间!

思路二:“三路归并”的DP神之一手 (O(n))

让我们换个角度。丑数的序列 [1, 2, 3, 4, 5, 6, ...] 本身是一个有序序列。 这个有序序列,可以看作是由三个“子序列”归并而成的:

  • 序列A:所有丑数 * 2 -> [1*2, 2*2, 3*2, 4*2, ...] = [2, 4, 6, 8, ...]

  • 序列B:所有丑数 * 3 -> [1*3, 2*3, 3*3, 4*3, ...] = [3, 6, 9, 12, ...]

  • 序列C:所有丑数 * 5 -> [1*5, 2*5, 3*5, 4*5, ...] = [5, 10, 15, 20, ...]

我们的目标,就是从这三个有序的“候选”序列中,不断地挑出最小的那个,来构建我们的主序列。这不就是“合并k个有序链表”的经典思想吗!

1. DP状态定义: dp[i] 表示第 i 个(1-indexed)丑数。我们的目标是 dp[n]

2. 状态转移的“三指针”: 为了高效地从三个“候选”序列中取最小值,我们不需要真的把它们都生成出来。我们只需要用三个指针,分别指向这三个序列中,下一个将要被考虑的“父丑数”的位置。

  • p2: 指向序列A中,下一个该乘以2的丑数在 dp 数组中的索引。

  • p3: 指向序列B中,下一个该乘以3的丑数在 dp 数组中的索引。

  • p5: 指向序列C中,下一个该乘以5的丑数在 dp 数组中的索引。

状态转移方程: 下一个丑数 dp[i],必然是三个候选者中的最小值: dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5)

指针的移动 (关键细节!): 在确定了 dp[i] 之后,我们需要检查这个最小值是由哪个(或哪些)候选者产生的,然后把对应的指针向前移动一步。

  • 如果 dp[i] == dp[p2] * 2,说明序列A的当前候选者被选中了,p2++

  • 如果 dp[i] == dp[p3] * 3,说明序列B的当前候选者被选中了,p3++

  • 如果 dp[i] == dp[p5] * 5,说明序列C的当前候选者被选中了,p5++

注意: 这里的 if 不能写成 else if!因为可能会有相等的情况,比如 6 = 2 * 3 = 3 * 2。此时,p2p3 都需要向前移动,以避免将来产生重复的丑数。

代码实现 (三指针DP)

class Solution {
public:int nthUglyNumber(int n) {// dp[i] 表示第 i+1 个丑数vector<int> dp(n);dp[0] = 1;// 三个指针,指向下一个要被乘的丑数在dp数组中的索引int p2 = 0, p3 = 0, p5 = 0;for (int i = 1; i < n; ++i) {int next2 = dp[p2] * 2;int next3 = dp[p3] * 3;int next5 = dp[p5] * 5;// 找到三个候选者中的最小值dp[i] = min({next2, next3, next5});// 移动指针if (dp[i] == next2) {p2++;}if (dp[i] == next3) {p3++;}if (dp[i] == next5) {p5++;}}return dp[n - 1];}
};

总结:DP的“生成式”思维

今天这道题,为我们展示了动态规划的一种全新应用范式——生成式DP。 它不再是分析一个已有的输入,而是从一个初始状态(dp[0]=1)开始,按照一套固定的生成规则,逐步构建出整个问题的解空间

“三指针”技巧,是这种模型下的一个极其优雅的实现。它本质上是对“多路归并排序”思想的巧妙运用,将一个 O(n log n) 的问题,优化到了线性的 O(n)。

当你未来遇到一个需要“从小到大生成一个满足特定规则的序列”的问题时,希望你的脑海中,能够浮现出今天这个“三路指针”的优美身影。

咱们下期见~

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

相关文章:

  • 高并发系统中的限流与异步优化实战指南
  • agent设计模式:第一章节—提示链
  • 【STM32】RTC实时时钟
  • 【数据结构与算法基础】04. 线性表与链表详解(C++ 实战)
  • C程序中的预处理器
  • 长沙黄页全域seo
  • 负载均衡技术:Nginx/HAProxy/F5 等负载均衡配置与优化
  • 外国人做的关于中国的视频网站吗高师院校语言类课程体系改革与建设 教学成果奖申报网站
  • Linux 进阶指令实操指南:文件查看、时间管理、搜索压缩全场景覆盖(附高频案例)
  • K8S(十六)—— K8S集群apiserver证书有效期修改指南(适配v1.20.11版本)
  • Altium Designer(AD24)Reports报告功能总结
  • 第一章 绪论——课后习题解练【数据结构(c语言版 第2版)】
  • Ubuntu 系统 RabbitMQ 安装指南与使用(含 C++ 客户端与 SSL 错误解决)
  • 网站开发外包 价格阿里巴巴国际站入驻费用及条件
  • MVVM架构模式详解:从原理到Android实战
  • 【Pico企业版】Pico企业版的多种Wifi快速连接方式(Pico 4UE的快捷Wifi连接技巧)
  • Kafka服务端处理producer请求原理解析
  • 以电商系统为例,理解用户体验五层模型
  • 兰州网站开发企业学校门户网站建设
  • CreArt2.5.7 | 无限AI图片生成,将文字描述转化为艺术作品
  • Linux企业级解决方案架构:字节跳动短视频推荐系统全链路实践
  • Python编程之常用模块
  • ios面试底层题目
  • h5游戏免费下载:《高达战争》
  • 百度网盘不限速2025年最新方法
  • 网站维护大概要多久学校英文网站建设申请
  • 深入比较 Rust 与 Go:并发时代的两把利剑
  • 容器安全:gVisor系统调用过滤,攻击面缩小?
  • 前端基础二、CSS(二)、CSS基础选择器
  • 学做电影网站asp.net门户网站项目怎么做