代码原理分析:
这段程序用“类似 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]
。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
时仍可接受)。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
(这是起点)。进入轮次前: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
时被跳过)此时未定型里 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
已定型)未定型里最小的是 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; } |