在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
一道Matlab编程题 & 暴力解法
Matlab课上老师出了这样一道题:
虽然这是一道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) = 63*(4k + 2j + i) step4: 63*(4(5k + l) + 2j + i) = 63*(20k + 4l + 2j + i) step5: 63*(20(k + m) + 20l + 2j + i) = 63*(20k + 20m + 4l + 2j + i) step6: 63*(20(2k + n) + 20m + 4l + 2j + i) = 63*(40k + 20n + 20m + 4l + 2j + i) 其中i, j, l, m, n都是大于等于0的任意整数
我们可以很清楚地看到我们想要的多项式的系数就是所有除数的最小公倍数,至此我们已经找到了我们想要的答案的第一部分了。
接下来我们要确定我们在多项式中的未定系数i, j, l, m, n,不过其实做到这里我们已经可以看到答案了,因为我们只要按暴力解法找出我们想要的第一个解(比如本题的最小鸡蛋数是1449),我们就可以确定我们想要的常数就是23了,也就是答案的算子为63*(40n + 23) (n >= 0)。这样做当然是可以的,而且这样做肯定比暴力解法来的快,复杂度褪化为O(M*C + N)(C为找到第一个解所需要的时间)。
但是我不想这么做,因为这样如果第一个解需要迭代的时间较长,或者根本本题根本无解,那么这样做会导致我们要算很久或者根本直接没有结果,我想直接把算子的常数直接推出来,而且还能判断是否有解。怎么做呢?我们可以利用同余方程来迅速求解。
用同余方程寻找多项式算子中的常数
同余方程(也称扩展欧几里得公式或者模逆运算,反正是一个意思)是数论里面最著名的东西了,同余方程描述如下:
同余方程有三个性质:
如果得到ax ≡ c (mod b)的某一特解X,那么令r = b/gcd(a, b),可知x在[0, r-1]上有唯一解,所以用x = (X % r + r) % r就可以求出最小非负整数解x了!(X % r可能是负值,此时保持在[-(r-1), 0]内,正值则保持在[0, r-1]内。加上r就保持在[1, 2r - 1]内,所以再模一下r就在[0, r-1]内了)。
有了同余方程我们可以很简单地求出我们所需要的常数了,在同余方程的描述中,我们所想要的东西为逆元,我们只需要稍微拓展一下我们的辗转相除法就可以了。每一次求匹配除数都要算一次逆元。
%External Euclidean Algorithm %xPtr is a pointer, x is p(1), y is p(2) %warnning: the index is different from C\C++ function result = ExMyGcd(n,m, xPtr) if n == 0 find= m; xPtr.value(1) = 1; xPtr.value(2) = 0; else num=[0,0]; numPtr=libpointer('int32Ptr', num); find = ExMyGcd(rem(m,n), n, numPtr); xPtr.value(1) = numPtr.value(2); xPtr.value(2) = numPtr.value(1) - numPtr.value(2)*floor(m/n); end result = find; end %ExMyGcd
xptr是一个指针,最后我们的逆元就是在xPtr.value(1)中!
另外通过同余方程的描述我们可以很快地判断题目的答案是否存在,因为我们的L,也就是余数,必须等于gcd(a,b),如果不满足这个条件,也就表明我们无法找到一个逆元使我们的同余方程成立,自然也就不可能找到我们所想要的算子了。
完整的算法描述如下(我使用了OOP,用matlab里面的类做了一层封装,由于matlab里面的类的get和set太反人类了,我直接把属性做成public用了)
1. main.m
obj = SearchPolyFuntionHelper(inputDivisors, modResult, 1, 1, 0); obj = DoSearch(obj); n = 0; if obj.m_isValid == 0 disp('invalid condition') %不满足同余定理的条件,直接舍弃 else coefficient = obj.m_coefficient; innerCofficient = obj.m_innerCofficient; offset = obj.m_offset; resultNum = []; resultIndex = 0; while 1 r = coefficient *(innerCofficient * n + offset); if (r >=1000000000) break end resultNum(resultIndex + 1) = r; resultIndex = resultIndex + 1; n = n + 1; end disp(resultIndex) resultNum( resultIndex) end
2. SearchPolyFuntionHelper.m
classdef SearchPolyFuntionHelper methods (Access = 'public') function obj = SearchPolyFuntionHelper(divisors, modResult, coefficient, innerCofficient, offset) obj.m_divisors = divisors; obj.m_modResult = modResult; obj.m_coefficient = coefficient; obj.m_innerCofficient = innerCofficient; obj.m_offset = offset; obj.m_isValid = 1; end % SerchPolyFuntionHelper function obj = DoSearch(obj) sum = size(obj.m_divisors); for n = 1: sum(2) if obj.m_modResult(n) == 0 mCoefficient = obj.m_coefficient; mDivisors = obj.m_divisors(n); gcdDivisor = gcd(mCoefficient, mDivisors); if gcdDivisor ~= mDivisors obj.m_coefficient = mCoefficient*mDivisors / gcdDivisor; end end n = n + 1; end for n = 1: sum(2) if obj.m_modResult(n) ~= 0 obj = Expand(obj, n); if obj.m_isValid ==0 return end end n = n + 1; end end %DoSearch end methods(Access = 'private') %Expand the cofficent function dObj =Expand(dObj, index) %先确定最小公倍数 mDivisors = dObj.m_divisors(index); gcdDivisor = gcd(dObj.m_coefficient * dObj.m_innerCofficient, mDivisors); oldInnerCofficient = dObj.m_innerCofficient; dObj.m_innerCofficient = oldInnerCofficient * mDivisors / gcdDivisor; %确定偏移量 coefficient*oldInnerCofficient*i + offset ( i>=0) mod = dObj.m_modResult(index); offset = dObj.m_offset; offsetMod = rem(offset * dObj.m_coefficient, mDivisors); %得到偏移量的余数 lastMod = rem(mod - offsetMod + mDivisors, mDivisors); if lastMod ~=0 %如果当前offset已经可以满足余数,就不需要再加了 num=[0,0]; xPtr=libpointer('int32Ptr',num); mul = dObj.m_coefficient* oldInnerCofficient; gcdDivisor = ExMyGcd(mDivisors, mul, xPtr); if rem(lastMod, gcdDivisor) ~=0 dObj.m_isValid = 0; %如果不满足同余定理,说明无法找到这样的多项式,直接退出 return; end newCofficient= xPtr.value(1); newCofficient = newCofficient * lastMod/ gcdDivisor; dObj.m_offset = newCofficient* oldInnerCofficient + offset; end end%Expand end properties (Access = 'public') m_coefficient; m_innerCofficient; m_offset; m_divisors; m_modResult; m_isValid; ElementIndex; end properties (Dependent) Modulus end end 3. ExMyGcd.m
%External Euclidean Algorithm %xPtr is a pointer, x is p(1), y is p(2) %warnning: the index is different from C\C++ function result = ExMyGcd(n,m, xPtr) if n == 0 find= m; xPtr.value(1) = 1; xPtr.value(2) = 0; else num=[0,0]; numPtr=libpointer('int32Ptr', num); find = ExMyGcd(rem(m,n), n, numPtr); xPtr.value(1) = numPtr.value(2); xPtr.value(2) = numPtr.value(1) - numPtr.value(2)*floor(m/n); end result = find; end %ExMyGcd 新的算法的时间复杂度分析
新的算法已经可以找到多项式了,所以复杂度是简单的O(MK + N),现在的问题就是确定K的大小,也就是确定同余方程找逆元的大小,因为我写的同余方程是基于欧几里得算法的,所以我们只需要找到欧几里得算法的复杂度就可以了!
在不考虑模运算本身的时间复杂度下,我们只考虑这样的问题: 欧几里得算法在最坏情况下所需的模运算次数和输入的a和b的大小有怎样的关系?
我们不妨设a>b≥1, 构造数列{un}:
显然, 若算法需要n次模运算, 则有 我们比较数列{un}和菲波那契数列{Fn},
以此类推,由数学归纳法容易得到
也就是
于是得到,也就是说如果欧几里得算法需要做n次模运算, 则b必定不小于Fn−1. 根据斐波那契数列的性质, 有
即 ,所以模运算的次数为O(lgb)
由此我们得到了我们算法的时间复杂度为O(Mlog(D) + N),由于logD(D是最大的除数)的增长速率缓慢,所以时间复杂度可以看成是O(M + N)。
性能测试
暴力算法与用同余方程的算法在需要算出n以内鸡蛋数的条件下,进行时间复杂度的比较结果。 测试完整代码:(注意两个测试的结果数会相差1,因为我自己写的原因,暴力解法永远会多一个,不影响最后的结果)
clear clc mainFun() function mainFun() modResult = [1,0,1,4,3,0,1,0]; inputDivisors = [2:9]; disp('-----------------Mytest-----------------') tic; obj = SearchPolyFuntionHelper(inputDivisors, modResult, 1, 1, 0); obj = DoSearch(obj); n = 0; if obj.m_isValid == 0 disp('invalid condition') %不满足同余定理的条件,直接舍弃 else coefficient = obj.m_coefficient; innerCofficient = obj.m_innerCofficient; offset = obj.m_offset; resultNum = []; resultIndex = 0; while 1 r = coefficient *(innerCofficient * n + offset); if (r >=1000000000) break end resultNum(resultIndex + 1) = r; resultIndex = resultIndex + 1; n = n + 1; end disp(resultIndex) resultNum( resultIndex) end toc; vpa(tic - toc); disp('-----------------OridnaryTest-----------------') tic; 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) toc; vpa(tic - toc); end 结果: 同余方程解法比暴力解法在n很大的情况下要快好几百倍
reference: |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论