一道Matlab编程题 & 暴力解法
Matlab课上老师出了这样一道题:
一个篮子有K个鸡蛋: 2个2个拿剩1个; 3个3个全部拿完; 4个4个拿剩1; 5个5个拿剩4个; 6个6个拿剩3个; 7个7个拿全部拿完; 8个8个拿剩1个; 9个9个拿全部拿完; 求篮子里鸡蛋的个数K
虽然这是一道matlab拿来玩的题目,可是我觉得完全可以拿来做笔试题或者面试题。仔细想还是有点考算法能力的。
这道题直观地想是非常简单的,简单一想就可以发现鸡蛋个数一定是7,9,3的最小公倍数63的N倍,然后我们就可以把63的倍数枚举出来,然后分别与除以2~9的除数,判断余数是否是对应值就可以了。当然你也可以用点观察法,当然你可以通过一点简单的观察来得到结果的规律来减少枚举次数。
matlab的算法代码如下:
resultNum = []; resultIndex = 0; i = 1; while 63*i < 1000000000 while 1 i = i + 2; if max(abs(rem(63*i, inputDivisors) - modResult)) == 0 break end end resultNum(resultIndex + 1) = 63*i; resultIndex = resultIndex +1; end disp(resultIndex) resultNum( resultIndex)
如果把这道题目一般化,比如有N个鸡蛋,i次i次拿,拿M次,然后求鸡蛋个数在N以内的取值。那枚举肯定是最慢的方法了,算法复杂度一眼看出来就是O(MN) 。显然,当M和N都很大时,这个时间复杂度是很大的,我们要想个办法把它简化。
从另一个角度看问题
我们先来看下枚举本身为什么会慢的问题,我们可以看到,枚举每一次都要尝试和M个除数进行相除,然后得到余数,除了本身求余这种对CPU的ALU指令非常不友好导致速度有瓶颈以外,还有最致命的就是枚举要通过不断地 “试验” 来得到判断的目的,当然这个过程可以裁枝(就像上面所说的观察法),但是这个问题的裁枝也只是减少了复杂度MN前面的常数系数。
既然裁枝的方法是制约着整个整个算法的瓶颈,那么我们要想能不能不裁枝,而是通过直接用一个一定成立的方程(也就是算子)来推出所有的结果呢?
我们可以联想到算勾股定理(找出A^2 + B^2 == C^2)所有成立的公式的时候,这是某些Java书里面最喜欢考的弱鸡算法题了,第一眼反应的时候肯定是用个三重循环来枚举(复杂度O(N^3)),仔细想一下,可以用Hash表裁枝(复杂度O(N^2)),但是我们如果使用的是完全平方公式4M^2N^2 == (M^2 + N^2)^2 - (M^2 - N^2)^2来思考这道题,我们可以发现我们只用枚举O(sqrt(M^2 + N^2)*sqrt(M^2 - N^2))次(也就是O(N))就可以找到所有的结果了,因为我们已经构建了一个一定成立的算子来填充结果了,而不需要再用枚举去匹配。
同样的,我们这道算法题也可以用同样的思路,我们观察一下可以得到,如果一个式子要满足结果对M个除数的余数都是成立的,那么结果必然是某些余数为0的除数的倍数。而根据余数定理可知,一个式子的对某个数的余式一定是一个多项式,所以我们立马就可以得到我们的结果一定是满足某个多项式的。如果能得到结果的算子,那么我们只要往算子里面填数据就可以得到所有的答案了。
寻找结果的多项式算子
首先第一步我们要确定多项式的系数,根据我们上面说的那样,这一题的答案一定是63*[polynomial],然后我们继续观察,根据拿两个剩一个这个条件,答案必须是mod 2 == 1,那么由于63中不含有2,那么[polynomial]必须形如2k+?(?是任意数),所以我们可以得到我们的答案一定是63*(2k + ?);同理,根据拿四个剩一个这个条件,答案必须是mod 4== 1,在前面的基础上,我们的答案必须形如63*(2(2k + j) + i) = 63*(4k + 2j +i)....
经过一系列分析,我们可以得到如下过程:
step1: 63k step2: 63*(2k + i) step3: 63*(2(2k + j) + i)