代码解析:完善程序-1
代码原理分析:
这段程序用“类似 Dijkstra 的逐点扩展”思路,计算:用若干个数字 4,通过 +、差的绝对值、整除(仅在能整除时)这些运算,得到目标整数 n 所需的最少 4 的个数。
代码原理讲解
- 状态含义
F[v]表示构造整数v所需的最少“4”的个数。初始知道F[4]=1。 - 扩展规则(可用运算)
设已经最优确定的两个数为i与x(它们分别需要F[i]与F[x]个“4”),
允许的合并运算是:i + x|i - x|- 若能整除:
i / x、x / i
y的候选代价都是F[i] + F[x](把两个表达式拼起来),
用它去最小化F[y]。 - 为什么像 Dijkstra
每一轮都在“未定型”的数里选F值最小的x,并把它与所有已定型的i合并更新。
因为所有被拿来合并的i都已经是最优的,所以t = F[i] + F[x]一定是真实可行的消耗。
又因为x是当前所有未定型里F最小者,所以此时确定x的值不会被任何将来的组合再降低——与 Dijkstra 选取最短路顶点的逻辑完全一致。于是,当n被标记为已访问时,F[n]就是最少 4 的个数。 - 为何要用
Vis[i]作为 ④ 的条件
如果让“尚未最优”的i参与合并,t=F[i]+F[x]可能基于一个“非最优”的F[i],会产生错误的松弛顺序,破坏正确性。只允许用“已定型”的i可以保证每一步的正确性。 - 边界与安全
- 循环中选择
x的枚举从1开始,因此不会把x选成0,避免了取模/整除的除零风险。 M是可达的上界;如果你需要更大的目标数,把M调大即可。
- 循环中选择
- 复杂度
- 选最小
F的过程是一次线性扫(O(M)),外层最多做M轮;
每轮与所有已定型的i合并也是一次O(M)的扫,
因此总体最坏复杂度约O(M^2)(这里M=10000时仍可接受)。 - 若想提速,可用优先队列维护“未定型的最小 F”,把选择
x的过程降到O(log M)。
- 选最小
小例子
下面把算法前 3 轮(依次选 x=4, x=1, x=8x=4,\;x=1,\;x=8)的全过程按你现在这版代码(含 if (i != x) 才做差值更新)完整展开。记号说明:
F[v]= 用若干个“4”得到整数v的最少 4 的个数;Vis[v]=1表示v已“定型”(就像 Dijkstra 已确定最短路);- 合并时只允许 左操作数
i已定型(if (Vis[i])),而被更新的目标可以尚未定型; - 允许的运算:
i + x、|i - x|(只有i != x才做)、若能整除则i / x与x / i。
初始:
F[*]=+INF,Vis[*]=0;- 置
F[4]=1(这是起点)。
第 1 轮:选 x = 4
进入轮次前:Vis[4]=0,已知 F[4]=1 是所有未定型里最小,选之。
设 Vis[4]=1,然后用 已定型的 i 与 x=4 合并。此时已定型只有 i=4 自身。
i=4, x=4:t = F[4]+F[4] = 1+1 = 2- 加法:
4+4=8→F[8] ≤ 2 - 差值:
i!=x为假 → 不更新F[0](这点很关键) - 除法:
4%4==0→F[4/4]=F[1] ≤ 2;x%i==0同样给F[1] ≤ 2
- 加法:
轮末结果(定型:{4}):
- 新得到:
F[1]=2,F[8]=2 F[0]未更新(仍为 +INF,因为差值在i==x时被跳过)
第 2 轮:选 x = 1
此时未定型里 F 最小的是 1(2) 与 8(2),按索引先选 x=1。
设 Vis[1]=1。此时已定型集合为 {1,4},因此内层会用 i=4 和 i=1 各与 x=1 合并:
i=4, x=1:t = F[4]+F[1] = 1+2 = 3- 加法:
4+1=5→F[5] ≤ 3 - 差值:
|4-1|=3→F[3] ≤ 3 - 除法:
4%1==0→F[4/1]=F[4] ≤ 3(不改,因F[4]=1更优)1%4!=0→ 无
- 加法:
i=1, x=1:t = F[1]+F[1] = 2+2 = 4- 加法:
1+1=2→F[2] ≤ 4(首次给2一个上界) - 差值:
i!=x为假 → 不更新F[0] - 除法:
1%1==0、1%1==0→ 都是F[1] ≤ 4(不改)
- 加法:
轮末结果(定型:{4,1}):
- 已有:
F[1]=2,F[8]=2 - 新增/改进:
F[5]=3,F[3]=3,F[2]=4 - 仍未定型:
2,3,5,8,…(只有4,1已定型)
第 3 轮:选 x = 8
未定型里最小的是 8(2),选 x=8。设 Vis[8]=1。
此时已定型集合为 {1,4,8},依次与 x=8 合并:
i=4, x=8:t = F[4]+F[8] = 1+2 = 3- 加法:
4+8=12→F[12] ≤ 3 - 差值:
|4-8|=4→F[4] ≤ 3(不改,仍为 1) - 除法:
4%8!=0→ 无;8%4==0→F[8/4]=F[2] ≤ 3(把F[2]从 4 降到 3)
- 加法:
i=1, x=8:t = 2+2 = 4- 加法:
1+8=9→F[9] ≤ 4 - 差值:
|1-8|=7→F[7] ≤ 4 - 除法:
1%8!=0→ 无;8%1==0→F[8/1]=F[8] ≤ 4(不改)
- 加法:
i=8, x=8:t = 2+2 = 4- 加法:
8+8=16→F[16] ≤ 4 - 差值:
i!=x为假 → 不更新F[0] - 除法:
8%8==0、8%8==0→ 都给F[1] ≤ 4(不改)
- 加法:
轮末结果(定型:{4,1,8}):
- 关键改进:**
F[2]从 4 降到 3(由8 ÷ 4得来) - 现有较小值:
F[2]=3,F[3]=3,F[5]=3,F[12]=3,
以及F[7]=4,F[9]=4,F[16]=4等 - 下一轮通常会在
F=3的未定型里按索引先选x=2(然后继续扩展)
小备注:关于 F[0]
你最开始提到“x=4 时定型了 F[0]”。在这版代码里我们写了
if (i != x) update(F[abs(i - x)], t);
所以第 1 轮 i=x=4 不会更新 F[0]。如果你把这条限制去掉,第一轮就会得到 F[0]=2(由 4-4),不过这不会被选作 x,因为挑选 x 的枚举从 1 开始,不包含 0。
是否允许 i==x 的差值,要看题目是否希望把“同一个表达式两次使用”作为一种合法合成方式。
代码注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | #include <iostream> #include <cstdlib> #include <climits> using namespace std; const int M = 10000; // 计算的上界,可按需要调大 bool Vis[M + 1]; // Vis[x] = 1 表示 x 的最优值已确定(已“出队/定型”) int F[M + 1]; // F[x] = 得到 x 所需的最少 4 的个数(代价) // 将 x 更新为更小的 y(最小化) inline void update(int &x, int y) { if (y < x) x = y; } int main() { int n; cin >> n; // 1) 初始化所有状态为“不可达”(无穷大) for (int i = 0; i <= M; i++) F[i] = INT_MAX; // 2) 基础状态:用 1 个“4”就能得到 4 // —— 这是整套构造的“起点” F[4] = 1; // ← ① // r 不是必须的变量,只是原题里保留的计数器,这里不影响逻辑 int r = 0; // 3) 主循环:只要目标 n 还没“定型”,就持续扩展 while (!Vis[n]) { // ← ② r++; // 3.1 在所有“未定型”的数里,选 F 值(用 4 的个数)最小的那个,记为 x int x = 0; // 这里 x 不会取到 0,因为下面枚举 i 从 1 开始 for (int i = 1; i <= M; i++) { if (!Vis[i] && F[i] < F[x]) // ← ③ 选未访问且 F 最小的 i x = i; } Vis[x] = 1; // 把 x 标记为“已定型” // 3.2 用“已定型”的所有数 i 与新定型的 x 进行一次“合并运算”, // 产生新的候选数,并用最少 4 的个数去松弛它们 for (int i = 1; i <= M; i++) { if (Vis[i]) { // ← ④ 只用已经定型的 i(它们的 F[i] 已最优) int t = F[i] + F[x]; // 合并 i 与 x 共消耗的“4”的个数 // 加法:i + x if (i + x <= M) update(F[i + x], t); // 绝对值差:|i - x| if (i != x) // i==x 时 |i-x|=0,会反复更新 F[0],含义不大 update(F[abs(i - x)], t); // 整除:i / x 与 x / i(仅当能整除时,结果才是整数才更新) if (x != 0 && i % x == 0) update(F[i / x], t); if (i != 0 && x % i == 0) update(F[x / i], t); } } } cout << F[n] << endl; return 0; } |
