首发于CSer
怎样的币值可以用贪心算法进行找零

怎样的币值可以用贪心算法进行找零

贪心算法

想必大家对贪心算法都比较熟悉,不熟悉就去看看《算法导论》之类的书,讲的非常清楚而又啰嗦,啰嗦的书最适合我们这些普通人自学。

当然,贪心算法比动态规划效率更高,在能用贪心的情况下,就不要搬起石头砸自己的脚。贪心里面最关键的部分就是最优子结构性质和贪心选择性质,满足这两个要求才能使用贪心算法。

最优子结构

那么,什么是最优子结构性质呢?最优子结构性质说通俗一点,就举个例子吧,就用最短路径来说。假设A到B找一条最短路径,假设找到的路径为A->C->D->B最短。那么最优子结构性质说的是,如果对于路径上的任意一点,到B都存在一条最短路径是包含在上述A到B的最短路径里。可能表述不太严谨,比如这里如果满足最优子结构性质应该有C->D->B是C到B的一条最短路径。《算法导论》第三版讲过拟阵的问题,说的就是贪心算法里的理论部分,有空看看一定会有收获。

最优子结构性质其实很好证明。还是拿例子说吧,就是上面的例子,假设C->D->B不是C到B的最短路径,那么假设是C->E->B最短,那么此时A->C->E->B就比A->C->D->B更加短,这显然和一开始的最优解假设是矛盾的,不是吗?其实就是一个替换的办法。

贪心选择性质

下面说说更重要的贪心选择性质,重要是因为其实最优子结构性质在动态规划里面也必须要用到。而只有满足了贪心选择性质,才能简化解法。贪心选择性质说的是,不考虑后面的步骤,当前选择选最优,现在的选择不会影响后面的步骤。很简单地例子就是本文主要要谈的找零问题。假设我们去买东西,用现金不用支付宝和微信。这时候如果买了1元的东西,你给了100元,那营业员会找你99个硬币或者一元纸币吗?我想应该不会,正常的操作是先找一个50元,在依次找两个20元(当然是在商店纸币充足的情况下)……这里,其实为了找最少数量的零钱,就是用到了贪心选择性质,认为先找最大的,每次都是这样直到凑够99元。

其实证明的时候还是用的替换的方法,假设我们不找最大的,我们从20元开始,那么我们就会找20,20,20,20,10……或者可以找更多的10元,这里我们注意到一点,这个两张20和一张10元可以替换为一个50,这里张数就变少了,因此假设的不选最大的也就不可能是最优解,类似的,更多地10元自然也可以替换为20元,也不是最优解。以上不是严格的证明,理解就可以,思想基本就是这样。

找零的问题

各路解释

这里我主要谈谈对找零这个问题的理解,贪心的问题大家看不懂的话就多看看书吧。这里要说的是,众所周知,其实不是所有的币值都能用贪心算法来解决的。但是从感觉上讲,RMB是能满足的,这就引起了我的兴趣,极大地兴趣。这种兴趣来自实验室同学的面试题,虽然我不是很清楚具体的币值,所以才想去找一个通用的判断方法,这里仅仅是判断而已,没有严格的证明,不严谨请见谅。

思考有卡顿的时候就想求助互联网,应该是我搜索能力不够,搜到一堆被黑的答案。比如,这里我们讨论方便,将币值数存储在数组里,假设从小到大依次存储在 a[0], a[1], a[2], …, a[n-1] 中,对于RMB来说就是, a[0]=1, a[1]=5, a[2]=10, a[3]=20, a[4]=50, a[5]=100​ 也许有人想为什么没有2元,因为太(bo)少(zhu)见(tai)了(lan)。其实你想,1,2,5元的状况这里和10,20,50的情况类似,所以无所谓。

下面说网上的解释,关于为什么RMB可以。有人说,要满足 2*a[i] \le a[i+1] 。我瞬间想到一个例子, a[0]=1, a[2]=5, a[3]=11 ,币值有点奇怪但是不妨做个反例。此时满足不等式,然后要找15元的零钱,怎么找?是不是发现了 3个5元一个11元+4个1元 更优。网上还有人说,要满足 a[i] * n = a[i+1] ,这样的情况肯定能满足贪心算法,然后问题是这是个充分条件,就是满足这个要求一定可以用贪心,但是贪心情况不仅仅如此,相比,你们想到了重要的RMB,就是我刚刚强调的20和50元的情况,不是倍数的关系,但是却可以贪心,其他的奇葩解释就更不用说了。

我的简单解释

其实,得益于各路大侠的解释,我也有了自己的思考。我认为是存在这样的情况,要么​ a[i]a[i+1] 存在倍数关系,毋庸置疑。要么前后之间不存在倍数关系。对于这种不存在倍数关系的情况,应该要满足这样一个条件:由于 a[i]<a[i+1] 所以我们一定能找到一个整数 k_i>1​ 满足 (k_i-1)*a[i]<a[i+1] 并且 k_i*a[i] > a[i+1] ,说通俗一点,比如对于20和50有, 20*2 < 50​20*3>50​ 。此时对于 k_i*a[i] 这样的数量的找零,我们要能用 a[i+1]+k_{i-1}*a[i-1]+…+k_0*a[0]​ 来代替,且张数不能更多。也即:

k_ia[i]=a[i+1]+k_{i-1}a[i-1]+…+k_0*a[0] (1)

有k_i\ge 1+k_{i-1}+...+k_0 (2)

我知道只有公式容易看花眼,所以我要解释一下。意思就是,对于一个币值,它的 k_i​ 倍刚刚超过比它后面一个币值时,我们可以将这个 k_i​ 倍的数值代替为一张它后面的这个币值加上若干比它小的币值,总张数不能比 k_i​ 多。意思就是这么个意思。但要注意的是,这是对 a[0]​a[n-2] 的限制条件,具有递推向后的关系,要么是倍数关系,要么就是我在上述扯了一大堆的第二种情况。

这里要注意的是,我们不应该去考虑 (k_i-1)a[i] < a[i+1], k_ia[i] > a[i+1], k_ia[i] \ge a[i+2] 的情况,这是为什么呢?比如我们增加一个币值为60元,我们会发现, 20*2 <50, 20*3>50, 20*3\ge 60 ,对于这种情况,其实要想考虑的话条件就发生了变化,这里20以上的币值不是考虑的50,而是考虑的60。什么意思呢?就是上面(1)、(2)式中的 a[i+1] 要改为 a[i+m]m \ge 1 ,这里的 k_i*a[i] 的值应该落在 a[i+m]a[i+m+1] (如果存在的话)之间。懒得画图了,大家就看看吧。关于证明就不写了,不言自明,用代替法做一下就可以了,话说这一段看不太明白怪我,太难解释了,不懂无所谓,因为正常情况你想啊,要是有50的币值到100之间又多了一个60的币值那意义就不是特别大。这个问题应该要设置一个平衡点。如果你要讲设置个60不是更方便吗?其实不然,设置1~100的所有币值。因为100以内的整数每个都有一张,多简单,还符合贪心算法。这又方便了吗?怕不是要用更大的收银台来将不同币值铺开。所以,并不是币值越多越好,私以为两倍两倍的涨显得更加有意义。当然三倍啥的也不是不行。1,3,9,27…也可以吧(我瞎说的,我是业余段子手emmmm)。

讲得不好,多多指教。

发布于 2019-03-21 17:35