加法链经典题:每步只能用前面两个数之和向后延伸,要求链最短。用迭代加深 DFS + 双向界剪枝(末值翻倍够不到 n 剪掉、剩余步数翻倍够不到 n 的候选剪掉),从大到小枚举两两之和贪心逼近 n。
OJ: roj
题目 ID: 1443
难度:普及+/提高-
标签:搜索迭代加深剪枝加法链
创建: 2026-09-30 10:46
更新: 2026-10-07 11:01
形式化题目
给定正整数
, ; - 对每个
,存在 ( 可以相等),使 。
要求
样例:1 2 4 5 是一条最短链(
正解
思路
朴素想法:直接从
观察一(链长有天然上界):链严格递增且
观察二(迭代加深恰合口味):我们要的是最短链,DFS 找到的不一定最短;BFS 又要存整层状态。既然答案深度不超过
观察三(两个方向界剪枝):当前链末值为
- 若
,后面每步最多翻倍也够不到 ,剪掉; - 反过来,新的候选值
之后还剩 步,若 同样无望,即 。把候选从大到小枚举,小于该下界的整段尾巴直接 break,而不是逐个continue。
从大到小枚举还有个贪心作用:优先逼近
候选生成:下一个数只能是已有链中两数之和(下标可相等)。链长为
样例
| 步骤 | 链 | 新值来源 |
|---|---|---|
| 0 | 1 2 4 8 | 翻倍逼近(下界剪枝迫使前段尽量大) |
| 5 | +9 | |
| 6 | +17 | |
| 7 | +34 | |
| 8 | +68 | |
| 9 | +77 |
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-05 22:00
# update_at: 2026-07-05 22:00
import sys
from functools import lru_cache
def min_depth(n: int) -> int:
"""n=1..n*2 的最优链长预计算不了,这里只给出深度下界:每步最多翻倍。"""
d = 0
v = 1
while v < n:
v *= 2
d += 1
return d
def search(chain: list[int], used: set[int], depth: int, n: int) -> list[int] | None:
"""在剩余 depth 步内把链延伸到 n;找到即返回整条链,否则返回 None。"""
last = chain[-1]
if last == n:
return chain
# 剪枝一:每步最多翻倍,翻 depth 倍也够不到 n
if last << depth < n:
return None
# 候选新值 = 前面的数两两之和(i、j 可相等,含翻倍),从大到小尝试以贪心逼近 n
m = len(chain)
cands = sorted({chain[i] + chain[j] for i in range(m) for j in range(i + 1)}, reverse=True)
lower = max(-(-n >> (depth - 1)), last + 1) # 須大于 last,且剩 depth-1 步翻倍须够到 n
for v in cands:
if v < lower:
break # cands 从大到小,后面只会更小
if v > n or v in used:
continue
chain.append(v)
used.add(v)
res = search(chain, used, depth - 1, n)
if res is not None:
return res
chain.pop()
used.remove(v)
return None
def solve_one(n: int) -> list[int]:
"""迭代加深:链长从下界开始逐层加深,第一条找到的链就是最短链。"""
if n == 1:
return [1]
for depth in range(min_depth(n), 23):
res = search([1], {1}, depth, n)
if res is not None:
return res
raise AssertionError("n 太大,深度上限 22 不够")
def solve() -> None:
out: list[str] = []
for token in sys.stdin.buffer.read().split():
n = int(token)
if n == 0:
break
out.append(' '.join(map(str, solve_one(n))))
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 01:40
* update_at: 2026-10-05 01:41
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXD = 32; // 答案链的最大长度;n 在本题范围内远小于 2^30
const int MAXCAND = MAXD * MAXD;
ll n; // 当前询问的 n
ll chain[MAXD]; // 迭代加深时正在构造的链,chain[0] = 1,严格递增
ll best[MAXD]; // 搜到的最短链
ll best_len; // 最短链最后一项的下标,即最优步数 m
ll cand[MAXD][MAXCAND]; // cand[step] 保存该层链里两两相加得到的候选值,按层存放避免递归互相覆盖
int cand_cnt[MAXD]; // cand_cnt[step] 是 cand[step] 里的候选个数
// 返回 n 的步数下界:每步末值最多翻倍,所以至少要 log2(n) 步。
ll depth_lower_bound(ll x)
{
ll d = 0;
ll v = 1;
while (v < x) {
v *= 2;
d++;
}
return d;
}
// 上取整的 a / b。
ll ceil_div(ll a, ll b)
{
return (a + b - 1) / b;
}
// dfs(step, remain):当前链最后一项下标是 step,还剩 remain 步可用。
// 能在 remain 步内走到 n 就保存整条链并返回 true,否则返回 false。
bool dfs(ll step, ll remain)
{
ll last = chain[step];
if (last == n) {
best_len = step;
for (ll i = 0; i <= step; i++) {
best[i] = chain[i];
}
return true;
}
// 剪枝一:末值每步最多翻倍,remain 步后也够不到 n。
if (last << remain < n) {
return false;
}
// 候选新值只能由链中两数相加得到(下标可相同,即翻倍),收集后从大到小尝试。
cand_cnt[step] = 0;
for (ll i = 0; i <= step; i++) {
for (ll j = 0; j <= i; j++) {
cand[step][cand_cnt[step]] = chain[i] + chain[j];
cand_cnt[step]++;
}
}
sort(cand[step], cand[step] + cand_cnt[step], greater<ll>());
// 剪枝二:新值 v 之后还剩 remain-1 步,必须满足 v * 2^(remain-1) >= n;
// 同时链严格递增,v 至少要大于 last。
ll lower = ceil_div(n, 1LL << (remain - 1));
if (lower < last + 1) {
lower = last + 1;
}
for (int k = 0; k < cand_cnt[step]; k++) {
ll v = cand[step][k];
if (v < lower) {
break; // 候选从大到小排列,后面只会更小
}
if (v > n) {
continue;
}
if (k > 0 && v == cand[step][k - 1]) {
continue; // 同一候选值只试一次
}
chain[step + 1] = v;
if (dfs(step + 1, remain - 1)) {
return true;
}
}
return false;
}
int main()
{
while (scanf("%lld", &n) == 1) {
if (n == 0) {
break;
}
chain[0] = 1;
// 迭代加深:从步数下界开始逐层 +1,第一条搜到的链就是最短链。
for (ll depth = depth_lower_bound(n); depth <= 30; depth++) {
if (dfs(0, depth)) {
break;
}
}
for (ll i = 0; i <= best_len; i++) {
if (i > 0) {
printf(" ");
}
printf("%lld", best[i]);
}
printf("\n");
}
return 0;
}复杂度
设最优链长为
总结
- 「每步最多翻倍」给出链长下界
,把无界搜索变成深度极小的搜索。 - 求最短 + 深度小 ⇒ 迭代加深是标配:每层第一条找到的链即最短链。
- 双向界剪枝:末值翻倍够不到
剪掉;候选值太小(剩余步数翻倍够不到 )整段剪掉。 - 候选从大到小枚举,让「贪心逼近 + 下界剪掉尾巴」同时生效。