哈喽,各位,我是前端小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问题,指引向一个更简单、更本质的数学解法。
咱们下期见~