原题

在古埃及,人们使用单位分数的和(如 

Python 陷入局部最优解 python求最优解_python

 ,a是自然数)表示一切分数,这种表现方法称为埃及数。例如:

Python 陷入局部最优解 python求最优解_python_02

 ,但不允许加数中有相同的,例如

Python 陷入局部最优解 python求最优解_开发语言_03

 。对于一个分数

Python 陷入局部最优解 python求最优解_开发语言_04

 ,表示方法有很多种,但是哪种最好呢?首先,加数少的比加数多的好;其次,加数个数相同的,最小的分数越大越好。如:

Python 陷入局部最优解 python求最优解_贪心算法_05

Python 陷入局部最优解 python求最优解_开发语言_06

Python 陷入局部最优解 python求最优解_最优解_07

Python 陷入局部最优解 python求最优解_开发语言_08

Python 陷入局部最优解 python求最优解_python_09

符合条件的最优解是最后一个。

编程求出分数 

Python 陷入局部最优解 python求最优解_开发语言_04

 (0<a<b)的最优表示 

Python 陷入局部最优解 python求最优解_最优解_11

 。

输入

第一行:一个自然数 a

第二行:一个自然数 b

输出

从小到大输出找到的单位分数的分母,每行一个整数 

Python 陷入局部最优解 python求最优解_最优解_12

输入样例

19
45

输出样例

5
6
18

思路

看到题目的第一反应是可以使用贪心算法,以找寻局部最优解为目标,每次都用原始分数减去最大的埃及数,比如19/45减去1/3,得到4/45,然后比4/45小的最大埃及数是1/12,于是4/45再减去1/12,这样每次都减去最大的埃及数,直到最后剩下某个1/a,把这些被减数合在一起,就是题目的解。

这样固然一定可以找出解(虽然我做不了数学证明,但要相信法老的智慧:D),但得到的却未必是最优解。按照题目的要求,最优解有两个条件:在所有可能的解中,1)所含的加数的数量最少,2)最后一个加数(最小的分数)的分母最小。比如,虽然通过贪心算法可以得到19/45的解是1/3+1/12+1/180,但是在所有可能的解中,最优的解却是1/5+1/6+1/18。两者长度相同,但最优解的最后一个分数的分母比较小(18小于180),也就是分数更大。而这种解是使用贪心算法无法得出来的。

要得到最优解,直觉上就必须求出所有可能的解,按照被减数的分母递增,一个个试下去。但这种算法的计算量十分恐怖,而且最重要的是,要试到何时才是终点呢?理论上每个被减数都可以得到一个解,但往往这样的分数的分母是一个天文数字。于是,再仔细思考一下,不难想到,一旦我们找到一个解,我们就可以放弃那些明显比我们找到的解还要差的解(长度更长,或最后一个分数更小),从而进行剪枝,不再继续向下查找。按照算法的术语来说,这叫限制深度的深度优先搜索,也就是迭代加深。

Python 陷入局部最优解 python求最优解_贪心算法_13

举个例子(图中的蓝色实线代表首先通过贪心算法找到的解,虚线代表可能的搜索路径,绿色的实现代表最优解),为了求出19/45的埃及数表现形式,我们首先使用贪心算法得到一个解,先减去1/3,再用余数减去1/12(所能减去的最大的分数),一直到得到1/180,满足条件,就停止往下搜索。这就是我们通过贪心算法得到的第一个解。而这个解包含有三个加数,1/3、1/12和1/180,所以我们可以说这个解的深度是3(从上到下第三层就得到解),所以当我们从第三层返回到第二层,试着用下一个加数(1/15)进行计算的时候,我们只要再向下试一个数,如果找不到就返回,而不需要再去第四层、第五层甚至更深去搜索了。同样的道理,如果从第二层返回第一层的时候,在第一层分别去试1/4和1/5,如果能在第二层得到解,则马上把深度更新为2,后面的计算都不用再向第三层计算了。

基本思路有了,但还有个重要的问题待解决。我们怎样决定在每一层搜索的广度?比如当得到第三层的一个解后,返回到第二层,继续去试其他的分数,一直试到 

Python 陷入局部最优解 python求最优解_贪心算法_14

,但这个 

Python 陷入局部最优解 python求最优解_贪心算法_14

 应该是多少呢?如果是在得到过解的第三层,我们可以令 

Python 陷入局部最优解 python求最优解_贪心算法_14

 小于等于之前解的最后一个分母的大小。比如上面的例子里,当得到第一个解1/3+1/12+1/180后,我们在第三层的用来尝试的分数的分母必然不能大于180。但是如果返回到第二层,第二层的 

Python 陷入局部最优解 python求最优解_贪心算法_14

 又该是多少呢?如果返回第一层,第一层的 

Python 陷入局部最优解 python求最优解_贪心算法_14

 呢?

这里就不得不手动计算一下。

如果已知解的最大深度,用maxdepth表示,上一层的深度用depth表示,而当前分数的余数用a/b表示,则必有以下等式成立:

Python 陷入局部最优解 python求最优解_开发语言_19


其中 

Python 陷入局部最优解 python求最优解_开发语言_20


x+1以及x+2并不表示比x只大了1或2,在这里只是表示分数的分母依次是增大的。

于是,不难看出,

\frac{1}{x} > \frac{1}{x+1} > \frac{1}{x+2} > \frac{1}{x_{n}}

 ,所以上面的等式可以转化为下面这个不等式:

Python 陷入局部最优解 python求最优解_最优解_22


使用小于等于而不是小于号的原因是,如果maxdepth-depth等于1,那么 a/b=1/x 即是解,所以不能排除相等的可能性。

因为a、b、x都是大于0的自然数,两边乘以bx再除以a,可得

Python 陷入局部最优解 python求最优解_python_23

因此,不论在哪一层,我们可以依次递增分数的分母,但是只要试到这个等式不成立为止即可(while循环)。

还有一点要注意的是,虽然题目告诉我们每下一层的分数的分母都是比当前这层的分母大,但我们却不应该、也不需要简单的把分母递增1去计算,比如在计算19/45的时候,减去1/3后,比余数小的最大的分数是1/12,我们就没有必要从1/4、1/5、1/6这样递增去计算了,这样也可以省去大量无意义的计算。


代码实现

为了记录第一个解的深度,用来限制后面搜索的深度和广度,我们需要定义一个全局变量,而且这个变量还要随着找到的解来更新。最省事的办法就是使用global关键字声明一个全局变量 maxd,这样就不用在函数体中定义了。同样地,我们也需要一个全局列表,用来保存找到的所有符合条件的解,所以也可以把列表 allres 声明成全局变量(并不一定需要这样做,因为python的列表保存的是存放列表中数据的内存地址,所以可以直接在函数体内调用append方法)。

另外,在找到第一个解之前,这个表示深度的变量应该是无限大(根据计算机的计算能力,在处理一些分数的时候使用无限大可能会导致计算时间无限增大,这时使用限定的深度会更好),所以我们可以导入python自带的math模块,使用其中的math.inf常量,得到一个浮点数,用来表示无限大(infinity).

而在每一层开始的时候,我们应该从比分数a/b小的最大分数的分母开始计算,所以也可以使用 math.ceil(b/a) 向上取整,来得到这个起始数 start 

当然,最基本的,因为题目是关于分数的计算,所以在整个过程中,我们都必须使用分数形式。我们当然可以引入python的fractions模块来计算分数,但是,我们需要用的分数计算只有减法而已,而得到的结果也只有两种可能,要么为0(找到解),要么依然是一个分数,用来向下继续计算。所以我们可以直接定义一个分数相减的函数 substract(a, b) ,用来返回分数a减去分数b的结果。

全部代码如下:

def substract(a, b):
    res = a[0]*b[1]-b[0]*a[1]
    return (res, a[1]*b[1]) if res else 0

def frac(n, res, x):
    global maxd, allres
    while x <= n[1]*(maxd-len(res))/n[0]:
        m = substract(n, (1,x))
        if m == 0: 
            res.append(x)
            allres.append(res)
            maxd = min(maxd, len(res))
            return
        else:
            k = max(x+1, math.ceil(m[1]/m[0]))
            frac(m, res+[x], k)
        x += 1

import math
a = int(input())
b = int(input())
n = (a, b)
allres = []
maxd = math.inf
start = math.ceil(b/a)
frac(n, list(), start)
allres = sorted(allres, key=lambda x:x[-1])
allres = sorted(allres, key=len)
for i in allres[0]:
    print(i)

因为题目要求最后只要输出最优解,所以我们在得到所有符合条件的解之后,可以按照给定的条件进行排序,再列出第一个元素的内容即可。


思考

这种解法是把所有符合条件的解先找到,然后再通过排序,打印出最优解。但是我们还可以在递归的函数里完成这种比较,然后只返回一个最优解。大家想想可以如何实现呢?