基于 IDA*(迭代加深搜索)与分数不等式上下界剪枝求解最优单位分数拆分。
OJ: roj
题目 ID: 1444
难度:提高+/省选-
标签:搜索剪枝IDA*
创建: 2026-09-30 10:54
更新: 2026-10-07 11:01
形式化题目
给定正整数
使得满足以下评价标准的解最优:
- 项数
最小; - 在
最小的前提下,最大分母 最小(对应最小的分数 最大); - 若最大分母仍相同,分母序列的字典序最小。
输出按升序排列的分母序列
正解
思路
本题是经典的迭代加深搜索(IDA*)与数学剪枝问题。
1. 为什么采用 IDA*?
题目首先要求加数个数
- 只要在某一层深度
找到了可行解,该深度即为项数最少的全局最优项数; - 在同一层内,遍历所有可能解并更新最优解,即可保证选出最大分母最小、字典序最小的答案。
2. 搜索分支上下界推导与剪枝
假设当前正在搜索第
-
下界(
): - 题目要求分母严格递增,故
。 - 当剩余项数
时,若选择的单位分数 ,后续继续相加必将超出目标分数。因此 。 - 综合得到下界:
。
- 题目要求分母严格递增,故
-
上界(
): - 剩余未确定的分母必须严格递增,因此后续的每个分数都严格小于当前分数,即
。 - 即使剩下的
个数都取到最大可能值(极限近似 ),它们的和也不会超过 。 - 如果
,则即使剩下全部取当前最大可能值,其总和也无法凑齐剩余的 。 - 因此必须满足
。 - 综合得到上界:
。
- 剩余未确定的分母必须严格递增,因此后续的每个分数都严格小于当前分数,即
-
叶子节点常数优化(
): - 当只剩下最后一项(
)时,不再进行循环枚举,而是直接判断 是否成立。 - 此时
,必须满足 且 。若成立,则直接得到一组可行解。
- 当只剩下最后一项(
代码
Python 版:
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-09-30 10:54
# update_at: 2026-09-30 10:54
import sys
from math import gcd
def egypt_search(a: int, b: int, limit: int) -> tuple[int, ...] | None:
"""用恰好 limit 个互不相同的单位分数凑出 a/b,返回分母序列最优的一组,无解返回 None。
序列比较规则:先比最大分母(越小则最小的分数 1/x_k 越大),
再把整个分母序列从小到大做字典序比较,越小越优。
"""
best: tuple[int, ...] | None = None
def dfs(k: int, a: int, b: int, last: int, chosen: list[int]) -> None:
"""还剩 k 个加数要凑出 a/b,已选分母都小于等于 last,chosen 是当前路径。"""
nonlocal best
if k == 1:
# 只剩一个加数时必须恰好相等:1/x = a/b ⟺ x = b/a(要求 b 是 a 的倍数)
if b % a == 0:
x = b // a
if x > last:
seq = tuple(chosen + [x])
# (最大分母, 整个序列) 一起比:先保最大分母最小,再保字典序最小
if best is None or (seq[-1], seq) < (best[-1], best):
best = seq
return
lo = max(last + 1, b // a + 1) # 1/x ≥ a/b 会立刻超额或把余量取空,排除
hi = k * b // a # 连 k 个 1/x 都凑不满 a/b 的分母,排除
for x in range(lo, hi + 1):
na, nb = a * x - b, b * x # a/b - 1/x = (ax-b)/(bx)
g = gcd(na, nb)
chosen.append(x)
dfs(k - 1, na // g, nb // g, x, chosen)
chosen.pop()
dfs(limit, a, b, 0, [])
return best
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
a, b = next(data), next(data)
g = gcd(a, b)
a, b = a // g, b // g # 先约分,搜索状态里保持最简分数
for limit in range(1, 10): # IDA*:加数个数从 1 开始逐层加深
best = egypt_search(a, b, limit)
if best is not None:
print(*best)
return
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* 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:40
*/
#include <cstdio>
typedef long long ll;
// ===== IDA* 迭代加深搜索 埃及分数 =====
// 目标:把 a/b 拆成加数最少、其次最大分母最小、再字典序最小的单位分数和
ll ga, gb; // 当前还差多少(最简分数)需要凑出
ll best[16]; // 目前全局最优的分母序列
ll path[16]; // 当前搜索路径上已选的分母
int depLimit; // 本轮迭代加深允许的加数个数
bool hasBest = false; // 是否已经找到过可行解
// 计算最大公约数(辗转相除)
ll gcd_ll(ll a, ll b) {
if (b == 0) return a;
return gcd_ll(b, a % b);
}
// 比较两条分母序列谁更优:先比最大分母(小者优,即最小的分数更大),
// 再比整个序列的字典序(小者优)。返回 true 表示新序列比 best 更优
bool better() {
// depLimit 相同时两条序列长度一致,最大分母就是最后一个元素
if (path[depLimit] != best[depLimit]) return path[depLimit] < best[depLimit];
for (int i = 1; i <= depLimit; i++) {
if (path[i] != best[i]) return path[i] < best[i];
}
return false;
}
// 还剩 k 个加数要凑出 ga/gb,上一个已选分母是 last(初始为 0)
// dep 表示当前填到第几个位置
void dfs(int k, ll a, ll b, ll last, int dep) {
// 边界:只剩最后一个加数,必须恰好 1/x = a/b,即 x = b/a
if (k == 1) {
if (b % a != 0) return;
ll x = b / a;
if (x <= last) return; // 分母必须严格递增
path[dep] = x;
// 注意:最优性要等本层搜完才知道,同一层内始终保留目前最优的一条
if (!hasBest || better()) {
hasBest = true;
for (int i = 1; i <= dep; i++) best[i] = path[i];
}
return;
}
// 下界:分母严格递增;且 1/x < a/b 才不会立刻超额,即 x > b/a
ll lo = last + 1;
if (b / a + 1 > lo) lo = b / a + 1;
// 上界:剩下 k 项每个都 <= 1/x,所以必须有 k/x >= a/b,即 x <= k*b/a
ll hi = k * b / a;
for (ll x = lo; x <= hi; x++) {
// a/b - 1/x = (a*x - b) / (b*x),约分后进入下一层
ll na = a * x - b;
ll nb = b * x;
ll g = gcd_ll(na, nb);
path[dep] = x;
dfs(k - 1, na / g, nb / g, x, dep + 1);
}
}
int main() {
ll a, b;
scanf("%lld %lld", &a, &b);
ll g = gcd_ll(a, b); // 先约分,保证搜索状态里是最简分数
a /= g;
b /= g;
// IDA*:加数个数从 1 开始逐层加深,第一个搜出解的深度即最少项数
for (depLimit = 1; depLimit <= 10; depLimit++) {
// 重新初始化全局最优记录
hasBest = false;
dfs(depLimit, a, b, 0, 1);
if (hasBest) {
// 输出最优解:按升序输出各分母(与题解 main.py 一致,只输出分母序列一行)
for (int i = 1; i <= depLimit; i++) {
printf("%lld%c", best[i], i == depLimit ? '\n' : ' ');
}
break;
}
}
return 0;
}复杂度
- 时间复杂度:在深度上限为
(本题中 )的迭代加深搜索中,上下界剪枝将每一步分支严格夹在 内,极大缩减了搜索树节点数。在 的数据范围内,搜索耗时远小于 1000ms(实际运行耗时几十毫秒至数百毫秒)。 - 空间复杂度:
,递归深度与保存当前分母序列的空间均受深度上限 约束,空间开销微乎其微。
总结
埃及分数问题是 IDA* 与数学不等式剪枝的典范应用:
- 利用题目的“项数最少”目标,把 BFS 的逐层扩展优势与 DFS 的极低空间消耗通过 IDA* 结合起来;
- 利用分母递增的特性,通过
和 确定严格的取值区间 ,将看似无穷的搜索空间压缩到极小范围。
图示解析
graph TD
A["输入: a/b (已约分)"] --> B["IDA*: 限制项数 limit = 1, 2, 3..."]
B --> C["回溯 DFS(k, a, b, last)"]
C --> D{"k == 1 ?"}
D -- 是 --> E["检查 b % a == 0 且 b/a > last"]
E -- 满足 --> F["记录/更新本层最优解 (min 最大分母)"]
D -- 否 --> G["计算上下界: lo = max(last+1, b//a+1), hi = k*b//a"]
G --> H["枚举分母 x ∈ [lo, hi]"]
H --> I["剩余分数 a'/b' = a/b - 1/x,递归 k-1"]
I --> C
F --> J{"本层是否搜出解?"}
J -- 是 --> K["输出最优解,结束"]
J -- 否 --> B