题解:P1763 埃及分数

· · 题解

题面

update 2025/8/9:优化、修改了代码使之能通过 hack 数据,同步修改了分析。去掉了非正解代码。

分析题干

对于分数 \dfrac{a}{b},要求把它拆分成 n 个分子为 1 的分数 \dfrac{1}{i_1} + \dfrac{1}{i_2} + \cdots + \dfrac{1}{i_n},满足:

  1. 满足第一条时,i_n 尽量小,即 \dfrac{1}{i_n} 尽量大

思路

显然不能采用贪心策略而在一开始尽量使用大分数,否则可能使剩下的分数极小而又不能简单分解,不得不分解成很多分数,使最后解不是最优解。考虑搜索算法来找出解。

考虑解答树的结构。

显然每层的数目极大,单纯的广搜空间和时间都不能接受(尤其空间)。如果使用深搜,那么可能会陷入深度极大但对答案完全没有贡献的枝杈,从而时间完全不能接受。因此,我们需要更优秀的搜索方法。

迭代加深搜索(IDDFS)

简要介绍一下迭代加深搜索迭代深化深度优先搜索):

在这个搜索策略中,一个具有深度限制的深度优先搜索算法会不断重复地运行,并且同时放宽对于搜索深度的限制,直到找到目标状态。——百度百科

也就是说,我们进行正常的深度优先搜索,但是限制搜索的深度,如果搜索时深度超过限制,无论如何立即回溯,继续搜索其它状态,从而遍历解答树上深度不超过限制的所有状态。如果找不到解,那就扩大限制,再次搜索;一旦找到结果,立即停止搜索、停止继续扩大限制。

$A$:不会,因为解答树的每层状态数是成指数级增长的,前几层的状态数目相对于最后的一两层来说九牛一毛,即使反复搜索也成本不高。也就是说,即使假设我们提前知道解的深度,直接进行一次该深度的搜索,也比迭代加深搜索快不了多少。 显然,这种思路可以避免广搜的空间紧张,也可以避免深搜陷入无底洞。当然,搜索出的解一定层数最少。 迭代加深分数个数(即层数),每层不断枚举 $i$,从 $\dfrac{a}{b}$ 中减去 $\dfrac {1}{i}$,并向下传递: - 剩下需要搜索的分数 - 上一个分母 - 当前层数 - 最大层数 以此进行搜索。 此时很容易发现一个优化:搜索到最后一层时不必再枚举,而是直接判定当前剩下的分数是否符合条件:分子为 $1$,分母大于上个分母。 发现严重超时,连 $\dfrac{7}{10}$ 这种小数字都超时,需要剪枝。 ## 剪枝 #1 思考可行性剪枝:搜索中各个分数值严格递减,而每一层最多剩下几个分数是固定的。显然,如果剩下的分数全部顶满最大值,仍小于剩下的分数,可以直接剪枝。 在搜索时写一句剪枝: ```cpp if ((maxdep - step + 1) * (1 / double(i)) < double(a) / double(b))//判断剩下的层数能否容纳剩下的分数 break; ``` 好些了。 按这样思路写好提交,分数介于 $40$ 到 $100$ 之间(实际分数我自己两次写好提交都不一样),但是 hack 数据超时。(搜索的时间复杂度是 $O(玄学)$,两次可能差着一点常数) ## 剪枝 #2 事实上,最大分母的量级很大,使得每一层搜索工作量极大。在每次搜索时,由一次固定最大分母搜索,改为多次层数相同、但最大分母差异较大的搜索。如果最大分母每次 $+1$ 显然功效不大,可以由 $10^3$ 每次 $\times 10$ 到 $10^7$。 ```cpp ll maxdepth = 1; for (; maxdepth <= 1e5; ++maxdepth)//迭代加深最大深度 { ll maxb = 1000; while (maxb <= 10000000)//迭代加深最大分母,最多5次,不会浪费大量时间 { memset(vans, 0, sizeof(vans)); memset(vnow, 0, sizeof(vnow)); nowp = ansp = 0; dfs(1, a, b, 0, maxdepth, maxb); if (ansp)//找到答案立刻输出、退出 { for (ll i = 1; i <= ansp; ++i) { printf("%lld ", vans[i]); } return 0; } maxb *= 10; } } ``` 但是即使这样仍然不能通过 hack 数据。它层数极多,前面的剪枝不能通过,因此还需要剪枝。 ## 剪枝 #3(最妙的剪枝) 考察最后两层搜索,它们占了主要的时间。 我们想求 $\dfrac {a}{b} = \dfrac{1}{x} + \dfrac{1}{y}$ 的整数解,通分得 $\dfrac {a}{b} = \dfrac{x+y}{xy} $,由于 $\gcd(a,b)=1$,所以存在正整数 $k$,使得: $$ \begin {cases} ka=x+y\\ kb=xy \end {cases} $$ 根据韦达定理,$x,y$ 是下面这个方程的两个解: $$ q^2-kaq+kb=0 $$ 此时 $\Delta = k^2a^2-4kb$,为了有两个不等的实根,须有 $\Delta > 0$,化简得 $k>\dfrac{4b}{a^2}$。 简单枚举 $k$,计算这个方程的两个根 $x,y$,即可在一次循环中求得剩余两层的解而无需再搜索。 在枚举 $k$ 时应有范围限制:已经求得 $k>\dfrac{4b}{a^2}$,还需求得上界。 设最大分母为 $maxb$,有: $$ x<y\\ ka=x+y<2y<2\times maxb\\ k < \dfrac{2maxb}{a}\\ kb=xy<maxb(maxb-1)\\ k<\dfrac{maxb(maxb-1)}{b}\\ $$ 即 $$ k \in \left( \dfrac{4b}{a^2},\min \left( \dfrac{2maxb}{a}, \dfrac{maxb(maxb-1)}{b}\right) \right) $$ 还需验证 $\Delta$ 是完全平方数。 计算: $$ x=\dfrac{ka-\sqrt{\Delta}}{2}\\ y=\dfrac{ka+\sqrt{\Delta}}{2}\\ $$ 此处 $ka$ 和 $k^2a^2$ 同奇偶,$-4kb$ 不影响奇偶性,若 $\Delta$ 是完全平方数时 $ka$ 也和 $\sqrt{\Delta}$ 同奇同偶,从而分子是偶数,保证了解是整数。 此时 $x,y$ (当然不能大于 $maxb$ )即是解,直接装入。 ```cpp for (ll k = 4 * b / (a * a); k*a<=2*maxb; ++k){ ll delta = (k * k * a * a) - (4 * k * b), x, y;//计算判别式 if (delta <= 0) continue; ll s = sqrt(delta); if (s * s != delta || delta <= 0)//判别式必须是完全平方数,保证x,y均为整数 continue; y = ((k * a) + s) >> 1; x = ((k * a) - s) >> 1;//计算x,y if (x > maxb || y > maxb || x < 0 || y < 0) continue; if ((ansp == 0 || y < vans[maxdep])){ if (x == vnow[step - 1]) continue; for (ll i = 1; i < step; ++i) vans[i] = vnow[i]; vans[step] = x; vans[step+1] = y; ansp = maxdep; break; } } ``` 此时这段代码能搜能剪,性能十分优越,因而此时可以用 $0.2s$ 左右的时间通过 hack 数据。 完整代码: ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; ll gcd(ll a, ll b){ return b ? gcd(b, a % b) : a; } const double eps = 1e-9; ll vans[105], vnow[105]; ll nowp, ansp, maxdep = 1, maxb; void dfs(ll step, ll a, ll b, ll lastb){ if (!a)//直接跳过 return; ll g = gcd(a, b); a /= g, b /= g; if (step == maxdep - 1){ /* 解方程部分: 对于最后两层a / b = 1 / x + 1 / y: 显然a / b = (x+y) / xy;且a/b最简 那么x+y = ka, xy = kb 依据韦达定理构造二次方程:q^2 - kaq + kb = 0; 如果要方程有不等的两个根:delta = k*k*a*a - 4kb; 那么解得k>4b/a^2;为下界 令maxb为分母最大值 显然x + y < maxb k < 2 * maxb / a; x < y < maxb xy < maxb(maxb-1) k < maxb(maxb-1) / b; min(2*maxb/a, maxb*(maxb-1)/b)即为上界 */ //另:不开long long 见祖宗 for (ll k = 4 * b / (a * a); k*a<=2*maxb; ++k){ ll delta = (k * k * a * a) - (4 * k * b), x, y;//计算判别式 if (delta <= 0) continue; ll s = sqrt(delta); if (s * s != delta || delta <= 0)//判别式必须是完全平方数,保证x,y均为整数 continue; y = ((k * a) + s) >> 1; x = ((k * a) - s) >> 1;//计算x,y if (x > maxb || y > maxb || x < 0 || y < 0) continue; if ((ansp == 0 || y < vans[maxdep])){ if (x == vnow[step - 1]) continue; for (ll i = 1; i < step; ++i) vans[i] = vnow[i]; vans[step] = x; vans[step+1] = y; ansp = maxdep; break; } } return; } for (ll i = lastb + 1; i < maxb; i++){ if (a * i < b)//a/b小于1/i可直接跳过 continue; //若剩下层数全部填满与当前分数相同仍不足当前分数,return true; if ((maxdep - step + 1) * (1 / double(i)) < double(a) / double(b))//判断剩下的层数能否容纳剩下的分数 break; ll newa = a * i - b; ll newb = i * b; vnow[step] = i;//装入答案 dfs(step + 1, newa, newb, i); vnow[step] = 0; } } int main(){ ll a, b; cin >> a >> b; //a = 73, b = 313; for (maxdep = 1; maxdep <= 15; ++maxdep){//迭代加深最大深度 maxb = 1000; while (maxb <= 10000000){//迭代加深最大分母,最多5次,不会浪费大量时间 memset(vans, 0, sizeof(vans)); memset(vnow, 0, sizeof(vnow)); nowp = ansp = 0; dfs(1, a, b, 0); if (ansp){//找到答案立刻输出、退出 for (ll i = 1; i <= ansp; ++i) printf("%lld ", vans[i]); return 0; } maxb *= 10; } } return 0; } ```