vlambda博客
学习文章列表

二分递归动态规划程序员

我们有一个正常人类交流群,里面偶尔会聊一些正经话题。


比如前两天有个人在群里发了一段话:手里只有俩玻璃球 怎么用最少的次数找出 在100层楼里 从几楼扔下去 这个球刚好不会碎 球不碎可以捡回来再扔 碎了就不能用了


有人马上就说李永乐老师讲过这个题,之前是谷歌的一道面试题;有人说不是扔鸡蛋吗,字节跳动的面试题也有这一道,在知乎上看过;有人说对,是扔鸡蛋,是一种变异二分查找。


我看到这里就一阵惋惜:好好的玻璃球,去打弹珠玩不好玩吗?好好的鸡蛋,炒了吃了它不香吗?干嘛扔来扔去爬上爬下的。


想到这里我就又想起我是不可能成为一个高阶的程序员了,我也就能写点能运行的代码去敷衍敷衍测试和用户了。不过值得欣慰的是,说扔鸡蛋那个兄弟,他还是个文科生,写代码完全是抓瞎,比写草书还龙飞凤舞,这货看逻辑都是当科普看的。而事实也证明,没有逻辑是不行的,比如有一次我们一起面杀狼人杀,这货一顿发金水播口条,然后跳预言家说自己晚上验了个猎人。。当然最难受的还是我们的那个猎人小姐姐居然深信不疑,场面一度非常尴尬。。


不过我们好歹算是对自己有点要求的程序员,还会挣扎讨论一下故作姿态。有个测试的小哥哥直接问:答案是什么?


我试着说一下,因为其实我也不太懂,我是去科普了一下李永乐老师的视频和CSDN上的牛人的解答。答案大概是这样的:


当你只有一个鸡蛋的时候,你就只能一层一层往上扔,这个很好理解;


当你有两个鸡蛋的时候,你就拿第一个蛋,从10层往下扔,没碎的话就去20层扔,还是没碎的话去30层扔。当你在40层扔的时候碎了,你就用第二个蛋,从31层开始往上扔,这样就可以知道那一层是碎的临界点了。


当你有N个蛋,也就是很多个的时候,就不用这么保守了。你直接从50层扔,碎了就从25层扔,没碎就去75层扔,以此类推,每次都从中间一层开始,反正你有的是鸡蛋。这样就能非常快的找到碎的临界楼层。


这种每次从中间开始的方法,叫二分法,是排序和查找算法的一种。其他的排序算法还有冒泡排序,选择排序,插入排序等十种经典排序算法。写到这里提醒大家注意,阅读的时候不管是什么时候看到“经典”二字,你需要做的就是“朗读并背诵”,这是标准流程,可以避免很多低级失误,比如觉得李商隐的《夜雨寄北》是抄袭的等等。


排序和查找算法,都要讲究两个东西,一个是空间复杂度,一个是时间复杂度。意思就是用了多大的存储空间算出来的,用了多长时间算出来的。我当年抱着对软件行业巨大的热情报考了软件工程专业,觉得软件可以改变世界。结果只学了半学期学到冒泡排序的时候就懵逼了,完全不知道这个东西到底是在干什么。我干嘛要去教计算机排序啊,而且我也不会,怎么去给它编程啊?


说远了,收回来。但凡接受过九年义务教育的人都知道,临界值其实一般都比较好算,就比如上面的1,2,N。但是当你有其他个数的蛋的时候,你试几次能把临界楼层试出来呢?


这就涉及到一个动态规划,和找递归式的问题。递归可以简单理解为循环调用自己,比如阶乘 n!=1×2×3×...×(n-1)×n,你不用写代码一直写到乘n,那先把人累死了。写一个n乘n-1就行了,然后不断递归调用,一直乘到1。实际代码写出来就是下面这样,第12行调用了第一行,也就是自己;第10行乘到1结束,就把阶乘算出来了


 1 public int doFactorial(int n){

2 if(n<0){

 3     //传入的数据不合法
4 return -1;
5 }
6 if(n==0){
7 return 1;

8 }else if(n==1){

 9     //递归结束的条件
10 return 1;
11 }else{
12 return n*doFactorial(n-1);
13 }
14}


从这里就能看出来计算机的好处和人的价值,人的价值就是总结一个算法,计算机的好处就是算得快。写到这大家应该对程序员为什么工资高有点概念了,因为程序员能让计算机干活,而计算机做重复工作比人又快又准,就给企业省了钱。省的这些钱就给了程序员了,所以程序员在网上非常低调,只有日渐消退的发际线和锃光瓦亮的脑门非常醒目。主要就是因为拿了别人的钱,只好自黑自嘲,有时候还自残,免得被别人提刀过来砍。


动态规划呢,是运筹学的一个分支,可以理解为把复杂场景分成很多单个阶段,各个阶段求解。比如扔鸡蛋,我们可以分成1,2,N,3~N-1等,把每个阶段都求解,最后的答案就出来了。拦截导弹啊,贪吃蛇啊,都可以用到动态规划。


说了这么多,程序员怎么处理双蛋问题呢?


首先,需要把问题抽象出来,像我一样想着弹玻璃球,炒鸡蛋吃是不行的:


有t层楼,n个鸡蛋,鸡蛋是相同的,临界楼层是指从某个楼层之上抛下来,都会碎,但从这个楼层之下抛下来,都不会碎。没有碎的鸡蛋可以重复使用。试假设能找到这个临界楼层需要抛投的最少次数M。


然后,需要把解题思路和方程抽象出来:


设M(t,n)为在从t层楼,n个蛋的情况下需要抛投的最少次数,情况有多少种呢。当然是t种,从每一层都抛出一个鸡蛋试一下。


现在就要得到这t个抛投实验中的最小抛投次数。设Mk(t,n)为从k(1<=k<=t)层楼抛下1个鸡蛋进行测试而得到的抛投数。这t种抛投有通用的递推模式:


假设在k层楼进行抛投,会产生两种情况:


碎了:问题规模变为:M(k-1, n-1)


没碎:问题规模变为:M(t-k, n)


则Mk(t,n) = max(M(k-1, n-1), M(t-k, n)) + 1,也就是求出这种情况下最大的抛投次数。


而M(t,n)= min(M1(t,n), M2(t,n), M3(t,n) ...... Mk(t,n))


上面这一堆看不明白没关系,这是程序员要做的事情。这是分析过程和算法,最后还要写代码,代码就不贴了,直接放算出来的结果:



这样双蛋问题就解决了。


在这个过程中,程序员的另外一种很重要的技能:抽象的能力,也被带出来了。


我们可以接触到的企业应用程序员大概有两种,一种是实现业务需求的;一种是搞算法AI教机器学习的。黑客和白帽子不能算企业应用。


把问题抽象化是非常重要的能力。比如你把双蛋问题抽象成算法,玻璃球/鸡蛋,楼层这些玩意对你就没影响了;比如AlphaGo下象棋,你不用告诉阿法狗围棋是啥,你把落子吃子和胜负的规则抽象成算法告诉它,它就能把柯洁干趴;再比如语音识别,之前人类很长时间都在想办法告诉机器听懂人类的音节,就跟教小朋友“你”“Hello”之类的,但是机器听不懂,就算听懂一个人的,另一个人说话有口音它又听不懂了。后来人们不让机器去处理模拟信号(人的声音),而是把人的声音(频率,相位)转换成数字信号(0,1)让机器去处理,计算机看到1001就知道你说的是“你”,看到“101101”就知道你在说“Hello”,机器就能“听得懂”人在说什么了。


程序员把业务需求转换成计算机能执行的代码,代码可以输出正确的结果,就叫技术和业务结合。很多产品经理吐槽程序员写出来的程序和自己设计的不一样,有可能是产品经理和程序员说的,和程序员告诉计算机的,不一样。


而如果一个程序员不光能写可以正确运行的代码,还能写的性能很高(空间复杂度和时间复杂度都比较低),易读(逻辑清晰,别人看的明白),可维护可扩展(改个逻辑啊,加个分支流程啊,很容易且不会影响之前逻辑),这个在技术上就是高阶了。


当然了,写出来的代码和设计的不一样,也有可能是产品经理和程序员的交流这一步就出现了偏差。工作时间一长,专业能力达到一定水平,很多人就会发现沟通变成了瓶颈。


阅读和沟通是更高阶的技能,看懂别人写的东西,听懂别人说的话不是一件简单的事情。让计算机懂你的话,对于一个合格的程序员来讲不难,但是能让别的人听懂自己说的话,对程序员来讲是个比技术能力更高阶的技能。


写了这么多,本来想写的另外一个故事又没写完。。放到下一篇吧二分递归动态规划程序员


试着总结一下,怎样判断自己是不是一个好的企业程序员:


张口就是二分法,递归,那很可能是个入门级的;

想着怎么能写更容易让机器理解的代码,更好的满足业务,这就是进阶了

而一个程序员,跟你聊得都是业务,聊得风生水起,回头就用动态规划一顿操作解决问题,这就是高阶程序员。



看山是山,看山不是山,看山还是山。