哈喽,各位,我是前端小L。
我们的DP之旅,已经探索了序列、矩阵、树上的各种问题,它们大多是"分析型"的------在一个给定的结构上寻找最优解。今天,我们将再次回到"构造型"DP的世界,我们的任务,是从一个A开始,通过"复制"和"粘贴",以最快的速度,"生产"出n个A。
这道题,是DP与初等数论结合的绝佳典范。它将向我们展示,动态规划的递推关系,有时会惊人地揭示出问题背后隐藏的数学本质。
力扣 650. 只有两个键的键盘
https://leetcode.cn/problems/2-keys-keyboard/

题目分析:
-
初始状态 : 屏幕上有1个
A。 -
操作:
-
Copy All: 复制屏幕上所有的A到剪贴板。 -
Paste: 将剪贴板的内容粘贴到屏幕上。
-
-
目标 : 得到恰好
n个A,所需要的最少操作次数。
核心洞察: Paste操作的次数,取决于你上一次Copy时屏幕上有多少个A。
-
如果你上次在有
j个A时按下了Copy,那么每次Paste,屏幕上的A就会增加j个。 -
要从
j个A变成i个A,你需要1次Copy ,和(i/j - 1)次Paste。总共需要1 + (i/j - 1) = i/j次操作。 -
这也意味着,
j必须是i的一个因数!
思路一:动态规划的"正统"之路 (O(n²))
这个"因数"关系,就是我们构建DP的基石。
1. DP状态定义: dp[i] 表示:得到 i 个A所需要的最少操作次数。
2. 状态转移方程推导: 为了计算 dp[i],我们思考,这 i 个A是怎么来的? 它必然是从某个状态 dp[j](j 是 i 的因数)通过Copy和Paste得到的。 从 j 个A到 i 个A,需要的操作数是 i/j 次。 所以,通过这条路径得到的总操作数是 dp[j] + i/j。
但是,i 可能有很多因数 j。我们应该从哪个 j 转移过来呢? 为了让 dp[i] 最小,我们必须遍历所有 i 的因数 j ,然后取所有 dp[j] + i/j 结果中的最小值。
于是,我们得到了状态转移方程: dp[i] = min_{j | i, j < i} { dp[j] + i/j }
3. Base Case: dp[1] = 0。我们一开始就有1个A,不需要任何操作。
代码实现 (DP)
class Solution {
public:
int minSteps(int n) {
if (n == 1) return 0;
vector<int> dp(n + 1);
dp[1] = 0;
for (int i = 2; i <= n; ++i) {
dp[i] = i; // 初始化为最坏情况:从1个A复制,粘贴i-1次,共i步
// 遍历 i 的所有因数 j (从大到小或从小到大都可以)
for (int j = i / 2; j >= 1; --j) {
if (i % j == 0) {
dp[i] = min(dp[i], dp[j] + i / j);
}
}
}
return dp[n];
}
};
这个解法是正确的,但效率不高。它引导我们走向了更深层次的思考。
思路二:"Aha!"时刻------DP关系揭示的数学本质
让我们仔细审视 dp[i] = dp[j] + i/j。 如果 i = a * b,那么 dp(a*b) = dp(a) + b (假设从a转移)。
现在,让我们考虑 n 的质因数分解 。 假设 n = p1 * p2 * p3 (p1, p2, p3 都是质数)。
-
dp(n) = dp(p1 * p2 * p3) -
为了得到
n,我们可以先得到p1 * p2,然后再乘以p3。 -
那么,
dp(n) = dp(p1 * p2) + p3 -
同理,
dp(p1 * p2) = dp(p1) + p2 -
而
dp(p1)是多少?因为p1是质数,它的唯一因数是1。所以dp(p1) = dp(1) + p1/1 = 0 + p1 = p1。
把它们串起来: dp(n) = dp(p1 * p2) + p3 = (dp(p1) + p2) + p3 = p1 + p2 + p3
一个惊人的结论诞生了:得到 n 个A的最少操作次数,恰好等于 n 的所有质因数之和!
为什么这个策略是最优的?因为Copy和Paste的操作,本质上是一种"乘法"增长。而质因数,是构成一个数的最基本的"乘法"单位。我们把 n 分解成最基本的乘法单元,然后一步步地"乘"上去,这自然就是最经济的路径。
最终解法:质因数分解 (O(sqrt(n)))
现在,问题被彻底"降维"成了一个纯粹的数学问题:求 n 的质因数之和。
算法流程:
-
初始化
ans = 0。 -
从
i = 2开始循环,i*i <= n: a. 如果n能被i整除,说明i是一个质因数。 b. 我们就把i加到ans中,然后把n除以i,继续检查i是否还是n的因数。 -
循环结束后,如果
n > 1,说明n本身就是一个大的质因数,把它也加到ans中。
代码实现 (数学解法)
class Solution {
public:
int minSteps(int n) {
int ans = 0;
for (int i = 2; i * i <= n; ++i) {
while (n % i == 0) {
ans += i;
n /= i;
}
}
if (n > 1) {
ans += n;
}
return ans;
}
};
总结:DP是发现规律的"显微镜"
今天这道题,是DP学习之旅中一次极其深刻和美妙的体验。它向我们展示了:
动态规划,不仅是一种求解工具,更是一种强大的分析工具。它能像一台"显微镜",帮助我们洞察问题内部的结构和规律,有时甚至能将一个复杂的DP问题,指引向一个更简单、更本质的数学解法。
咱们下期见~