一道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) = 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为找到第一个解所需要的时间)。
 
但是我不想这么做,因为这样如果第一个解需要迭代的时间较长,或者根本本题根本无解,那么这样做会导致我们要算很久或者根本直接没有结果,我想直接把算子的常数直接推出来,而且还能判断是否有解。怎么做呢?我们可以利用同余方程来迅速求解。
 

用同余方程寻找多项式算子中的常数

同余方程(也称扩展欧几里得公式或者模逆运算,反正是一个意思)是数论里面最著名的东西了,同余方程描述如下:
  1. 设存在方程 ax + by = L
  2. 如果该方程的线性同余形式为 ax = L mod b
  3. 如果这个形式下该方程有解,那么L一定且仅满足gcd(a,b) == L时成立
 
同余方程有三个性质:
  1. 定理一:如果d = gcd(a, b),则必能找到正的或负的整数k和l,使d = ax+ by。
  2. 定理二:若gcd(a, b) = 1,则方程ax ≡ c (mod b)在[0, b-1]上有唯一解。
  3. 定理三:若gcd(a, b) = d,则方程ax ≡ c (mod b)在[0, b/d - 1]上有唯一解。
 
如果得到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: 
 
 
 

运用模逆运算(同余方程)来解决Matlab课上的一道思考题的更多相关文章

  1. 如何解决 Matlab 画图时中文显示乱码的问题?

    使用的是win10系统,从前几个月某一天,我的matlab的figure里的中文都变成了口口.很是郁闷,还以为是动到了什么配置引起的. 前几天更新了matlab 2018b,发现还有这个问题.就觉得不 ...

  2. 20165223《信息安全系统设计基础》第九周学习总结 & 第八周课上测试

    目录 [第九周学习总结] 教材内容总结 [第八周课上测试] (一)求命令行传入整数参数的和 (二)练习Y86-64模拟器汇编 (三)基于socket实现daytime(13)服务器和客户端 参考资料 ...

  3. 20175316盛茂淞 2018-2019-2 《Java程序设计》第2周课上测试总结

    20175316 2018-2019-2 <Java程序设计>第2周课上测试总结 上周考试题目总结 题目1 题目要求: 在Ubuntu中用自己的有位学号建一个文件,教材p29 Exampl ...

  4. 20165305 苏振龙《Java程序设计》第四周课上测试补做

    第一次测试 第二次测试 第三次测试 上传代码 第四次测试 总结 之前我一直在git bash进行程序设计,但是对于我来说操作起来有点困难,所以我改用了虚拟机,之后之前一直困扰我的问题在虚拟机下就没有了 ...

  5. # 2017-2018-1 20155302 课下实践IPC及课上补充

    课上实践补交 题目二要求: 学习使用stat(1),并用C语言实现 提交学习stat(1)的截图 man -k ,grep -r的使用 伪代码 产品代码 mystate.c,提交码云链接 测试代码,m ...

  6. 2017-2018-1 20155232 《信息安全系统设计基础》第四周学习总结以及课上myod练习补充博客

    2017-2018-1 20155232 <信息安全系统设计基础>第四周学习总结以及课上myod练习补充博客 课上myod练习 1 参考教材第十章内容 2 用Linux IO相关系统调用编 ...

  7. 2018-2019-1 20165330 《信息安全系统设计基础》第六周课上测试ch02&课下作业

    课上测试 测试-3-ch02 任务详情 编写一个程序 "week0203学号.c",运行下面代码: 1 short int v = -学号后四位 2 unsigned short ...

  8. Java实验--关于课上找“水王”问题分析

    问题的表述就是说有那么一个人,他在一个论坛上发帖,然后每贴必回,自己也发帖.那么这个人在发帖的数目上就超过了整个论坛的帖子数目的一半以上. 我对这个问题一开始的思路是,用SQL语句获取整个列表中的数据 ...

  9. 关于转入软件工程专业后第二次java课上作业的某些体会

    今天是第二周的java课. 自从转入了软件工程专业后,在我没有学习c++的基础上,直接开始了学习java的过程.不得不说过程很艰辛.今天下午老师让编写一个随机产生作业的软件.而我的基础差到都不知道如何 ...

随机推荐

  1. Lightoj1015【基础题】

    题意: 计算输入数>0的所有和: 思路: 直接干... #include<cstdio> #include<queue> #include<map> #inc ...

  2. beanutils包下载

  3. PostgreSQL - 怎么将时间转换成秒

    保留原来的毫秒值 select extract(epoch from '03:21:06.678'::time); 这个extract(epoch from )函数得到的是时间是秒单位,如果需要毫秒值 ...

  4. JAVA列出某文件夹下的所有文件

    import java.io.*; public class ListFiles { private static String s = ""; private static Bu ...

  5. C# 基础之索引器

    当一个类有数组成员时,索引器将大大简化对类中数组成员的访问 索引器类似于属性有get与set访问器 列如: 使用: 总结:从以上代码可以看出索引器也是对私有字段进行访问的方式,但此时的私有字段是数组类 ...

  6. 292. Nim游戏

    292. Nim游戏 class Solution(object): def canWinNim(self, n): """ :type n: int :rtype: b ...

  7. Qt 进程和线程之一:运行一个进程和进程间通信

    Qt提供了对进程和线程的支持.本节讲述了怎样在Qt应用程序中启动一个进程,以及几种常用的进程间通信方法.如果对进程和线程的概念不是很了解,可以看我的另一篇博客:[多进程和多线程的概念. 设计应用程序时 ...

  8. 2017浙江工业大学-校赛决赛 小M和天平

    Description 小M想知道某件物品的重量,但是摆在他面前的只有一个天平(没有游标)和一堆石子,石子可以放左边也可以放右边.他现在知道每个石子的重量.问能不能根据上述条件,能不能测出所问的重量. ...

  9. STM32的低功耗模式

    一 待机模式standby和STOP模式的区别: 进入低功耗模式:都一样,都是先关闭相应时钟,关闭相应外设,配置相应所有IO口(浮动输入),然后配置相应的唤醒中断源,中断影响的O口,然后调用相应函数进 ...

  10. The Weakest Sith

    http://codeforces.com/gym/101149/problem/F 题目要输出最丑陋的衣服.所以每件衣服都要和其他衣服比一次. 但是注意到,能赢一件衣服的衣服,就算是好衣服了. 那么 ...