动态规划1:找零钱练问题

转自:https://blog.csdn.net/qq_27703417/article/details/70980229

动态规划

很多实际的问题往往需要使用动态规划来解决,动态规划的题目很难,如果按照自己的想法去实现会非常复杂,不仅逻辑复杂而且时间复杂度很高,解决动态规划的问题往往是有套路的,即动态规划的问题题型都差不多,解决思路比较相似,因此有时候可以套用已有的阶梯思路来解决问题。对于动态规划题目,首先需要理解动态规划的含义是什么,动态规划的本质究竟是什么,然后掌握动态规划问题解决的思路、策略和方法,最后多做几个题目来灵活应用动态规划的套路解决实际的问题。

题目:有数组penny,penny中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。

测试样例:[1,2,4],3,3  返回:2

思路:

方法一:暴力递归搜索

对于一个目标值aim,使用若干个给定的整数来拼凑出这个目标值aim,元素在数组penny[]中给定,每个元素可以使用0次,1次或者任意次,求出拼凑方案的数目有多少种。解决这个问题的思路显然是使用递归,不使用递归如果仅仅使用循环的话逻辑是非常复杂的,基本不可能解决,必须使用递归。即对数组的第一个元素的情况进行分情况枚举,假设目标值为10,零钱数组为[5,3,2],则考虑5元使用0次时的方案有多少种res1,5元使用1次的方案有多少种res2,5元使用2次的方案有多少种res3,将这3个结果加起来就得到了结果总的方案数目。这样通过一次枚举其实就将问题剥去了一层,使得问题有所简化,在计算res1,res2,res3时,面对的问题和初始时相同,只是使用的参数条件发生了变化,例如求res2时,此时可以使用的零钱数组是[3,2],目标值不再是10,而是aim-1*5=5,于是问题就是使用[3,2]来拼凑5,解决方法和之前是一样的,因此设计一个递归函数,输入一个目标值aim,和可以使用的零钱数组的范围index(表示小标为index及之后的元素),返回使用给定零钱可以拼凑出目标值的方案数目res。

递推关系:对penny[index]的使用的次数情况进行枚举,for(int i=0,penny[index]*i<=aim,i++),对于每一个i都对应一个枚举的情况,在该枚举情况下可以重新调整参数,此时可以使用的零钱数组是penny中index+1开始及之后的数组部分,新的目标值是aim=(aim-penny[index]*i),于是调用递归方法dp(penny,index+1,aim-penny[index]*i)即可返回当前枚举条件下的方案数目,对每一种情况下的方案数目进行累加即可得到总的方案数目。

边界条件:这里关键是对于边界条件或者叫作初始条件的确定,每次递归方法返回的都是使用数组元素arr[]拼凑得到aim的方案数目,那么到底怎么计算拼凑的方案数目呢?其实需要理解,使用递归时,问题不断展开,即相当于枚举出了使用penny[]数组元素各种组合情况,此时需要判断是否满足拼凑的要求,即怎样算是拼凑成功了,怎样算是拼凑没有成功,显然在某种枚举情况下,当aim值变成0时说明拼凑成功,否则拼凑不成功;并且由于前面的若干个元素的组成总是要保证和<=aim,因此最终的填坑的工作总是要交给最后一个元素来进行,即通过递归,使用前面的元素[5,3]可以组合成的各种价值已经都被列举完了,次时就要看最后一个元素2是否能在for(int i=0,penny[index]*i<=aim,i++)的枚举过程中使得总和达到aim值,如果恰好达到则算作一个方案,否则不能算作方案。因此边界条件是当index>penny.length-1时,判断aim是否等于0。理解:不管前面的元素如何组合(各种组合方案通过递归已经展现出来了),要想形成一个完整的方案(5使用x次,3使用y次,2使用z次),必须确定最后一个元素使用了多少次,因此必须对最后一个元素使用的次数进行枚举,每一次枚举的过程相同,枚举的同时判断剩余的目标值是否为0,如果为0表示当前的递归形成1个拼凑方案参与计数,否则不形成方案不参与计数。于是递归的边界条件就是当index==length时判断aim==0是否成立,若成立+1,否则+0;


 
 
  1. import java.util.*;
  2. //找零钱问题:方法一:使用暴力递归搜索,注意边界条件的确定
  3. public class Exchange {
  4. public int countWays(int[] penny, int n, int aim) {
  5. //特殊输入
  6. if(penny== null||penny.length<= 0||aim<= 0) return 0;
  7. //调用递归方法解决问题
  8. return this.process(penny, 0,aim);
  9. }
  10. //递归方法:输入一个零钱数组部分和目标值,返回拼凑的方案数目
  11. private int process(int[] penny,int index,int aim){
  12. int res= 0;
  13. //边界条件:初始条件,递归的边界条件满足后要立即返回,所谓边界条件是指能够使得方法return的条件
  14. if(index==penny.length){
  15. res=aim== 0? 1: 0;
  16. return res;
  17. }
  18. //对第一个元素的出现次数进行枚举
  19. for( int i= 0;i*penny[index]<=aim;i++){
  20. res+= this.process(penny,index+ 1,aim-penny[index]*i);
  21. }
  22. return res;
  23. }
  24. }

方法二:记忆搜索

在实际面试的题目中,对于动态规划的问题,必然可以使用暴力搜索、记忆搜索的方法来解决,实际上所谓的动态规划就是在暴力搜索、记忆搜索的基础之上通过优化而得到的一种解题套路。

暴力搜索方法--记忆搜索方法—动态规划方法—状态继续化简后的动态规划方法

虽然动态规划的问题千奇百怪,但是它的解题思路和策略以及进行优化的轨迹是高度一致的,可以按照套路来进行。对于一个复杂的问题,使用暴力搜索方法通常是容易的,要将其改进为动态规划并进一步进行状态化简就比较麻烦(要向降低时间复杂度总是会使得编程难度增加),因此需要多练习多思考多积累经验多找感觉。

上面的递归过程其实枚举出了零钱的所有不越界的组合方案:

5元使用0次,3元使用0次,2元使用0次  失败

5元使用0次,3元使用0次,2元使用1次  失败

5元使用0次,3元使用0次,2元使用2次  失败

5元使用0次,3元使用0次,2元使用3次  失败

5元使用0次,3元使用0次,2元使用4次  失败

5元使用0次,3元使用0次,2元使用5次  成功

5元使用0次,3元使用1次,2元使用0次  失败

5元使用0次,3元使用1次,2元使用1次  失败

5元使用0次,3元使用1次,2元使用2次  失败

5元使用0次,3元使用1次,2元使用3次  失败

5元使用1次,3元使用0次,2元使用0次  失败

5元使用1次,3元使用0次,2元使用1次  失败

5元使用1次,3元使用0次,2元使用2次  失败

5元使用1次,3元使用1次,2元使用0次  失败

5元使用1次,3元使用1次,2元使用1次  成功

5元使用2次,3元使用0次,2元使用0次  成功

即这种递归其实是一种暴力搜索的方式,它将所有组合进行枚举判断,注意理解:使用递归本身仅仅是使得程序写法上更加简便,对于程序运行来说毫无作用,因此尽管使用了递归但是他是一种暴力搜索的方式效率低下,时间复杂度很高。

分析这种暴力搜索的方法的缺陷在哪里:

假设目标值为30;零钱数组是penny[8,4,2,1]

8元使用1次,4元使用3次,和为20,剩下用[2,1]拼凑10;

8元使用2次,4元使用1次,和为20,剩下用[2,1]拼凑10;

这2种情况下使用上面的递归过程枚举的过程是:

枚举1:

8元使用1次,4元使用3次,2元0次,1元10次;

8元使用1次,4元使用3次,2元1次,1元8次;

8元使用1次,4元使用3次,2元2次,1元6次;

8元使用1次,4元使用3次,2元3次,1元4次;

8元使用1次,4元使用3次,2元4次,1元2次;

8元使用1次,4元使用3次,2元5次,1元0次;

枚举2:

8元使用2次,4元使用1次,2元0次,1元10次;

8元使用2次,4元使用1次,2元1次,1元8次;

8元使用2次,4元使用1次,2元2次,1元6次;

8元使用2次,4元使用1次,2元3次,1元4次;

8元使用2次,4元使用1次,2元4次,1元2次;

8元使用2次,4元使用1次,2元5次,1元0次;

可见在上述递归枚举过程中,尽管这2中枚举过程对于使用[2,1]来拼凑10的过程是完全一样的,但是第二次枚举时并不会直接拿[2,1]à10的结果来用,并且之前在每一种拼凑结束后也没有记录下拼凑的方案数目,因此每次遇到一个拼凑问题[x,y,z]aim总是要重新开始从头遍历枚举,这显然造成了重复计算。因此改进方案是对每次计算得到的部分结果片段进行保留以便重复利用。理解:不一定需要整条方案计算出来了再保留,对于任意当前计算出来的部分结果都要进行保留,因为递归展开是从前往后展开的,但是回归是从后往前回归的,即总是先计算出数组后面几个元素拼凑结果后再计算出前面的结果,例如总是先计算出[2,1]à10的结果,将它保留,于是之后在遇到8*1+4*3+[2,1]à10时可以直接使用这个结果,在8*2+4+[2,1]à10时也可以直接使用这个结果。即从后向前回归结果时对每一个结果都进行保留,在之后的每一次枚举求值时直接从已知的结果中取值即可:有的问题中,后面需要用的值完全来自于前面的计算结果,于是直接使用前面的计算结果即可;有的问题部分值来自于前面的计算结果,于是在计算时先去前面的计算结果中寻找,如果找到了就直接使用,如果没找到就进行独立的计算。可以使用map后者数组或者二维数组来保存已经计算的局部结果。

 

所谓的记忆搜索方法是指在递归或者循环计算的过程中,将已经计算出来的结果进行保存。通常是指在递归计算的过程中,在递归的回归阶段,低维或者简单的方法调用会计算出结果从而进行回归,此时,对于当前计算出来的结果,虽然不是最终的结果而仅仅是局部的结果,但是在大量递归回归的过程中,这个结果可能需要被其它的递归调用使用到,因此为了避免重复计算,需要将此次结果保留下来,使得下一次递归调用时可以直接使用而不用在重复递归计算,例如:

在递归计算8*1+4*3+[2,1]à10的时候,如果这是第一次计算[2,1]à10,那么会递归的展开[2,1]à10的计算过程,计算出所有使用2和1拼凑10的解决方案,得到方案的数目为6种,

在递归计算8*2+4*1+[2,1]à10的时候,如果之前没有保留[2,1]à10的结果,那么此时还是需要对[2,1]à10进行递归地展开计算次数,但是如果之前保留了[2,1]à10的结果,那么此时通过搜索结果哈希表(这里的结果哈希表是广义的,指通过键值对存放结果的结构,如果键只有一个要素可以使用hashmap或者一维数组,如果键有2个元素可以使用二维数组)就可以直接得到结果为6种,从而避免了递归过程。

记忆搜索方法:

1.每计算完一个p(index,aim)都要将结果放入到map中去,index和aim可以代表一个递归调用过程,因此用index和aim组成共同的key返回结果为value。

2.要进入一个递归过程p(index,aim),先以index和aim注册的key在map中查询是否已经存在value值了,如果存在则直接取值,如果不存在才进行递归计算。

结果表map可以使用hashmap或者一维数组或者二维数组map[i][j]的结果代表p(i,j)的返回结果。


 
 
  1. import java.util.*;
  2. //使用搜索记忆方法计算零钱凑整数的方案数目
  3. public class Exchange {
  4. public int countWays(int[] penny, int n, int aim) {
  5. //特殊输入
  6. if(penny== null||penny.length<= 0||aim<= 0) return 0;
  7. //创建一个全局的数组dp[][]用来存放计算的中间结果,元素从0~n-1,共n个,aim从0到aim共aim+1个
  8. int[][]dp= new int[n][aim+ 1];
  9. //调用递归方法解决问题
  10. return this.process(penny, 0,aim,dp);
  11. }
  12. //递归方法:给定零钱数组和目标值返回可以凑整的方案数
  13. private int process(int[] arr,int index,int aim,int[][]dp){
  14. int res= 0;
  15. //边界条件
  16. if(index==arr.length){
  17. res=aim== 0? 1: 0;
  18. return res;
  19. }
  20. //先取结果集合中找是否存在这个结果
  21. if(dp[index][aim]!= 0){
  22. //dp[][]默认初始值为0,如果不为0表示已经计算过了,已经有了结果
  23. //对于dp[index][aim]不一定都有凑整方案,如果无法凑整,应该令其为-1而不是0
  24. if(dp[index][aim]==- 1){
  25. //表示该情况下无法凑整,于是直接返回0
  26. return 0;
  27. } else{
  28. //表示该结果存在且不为0,可以直接使用
  29. return dp[index][aim];
  30. }
  31. } else{
  32. //表示dp[index][aim]==0,即为初始值,说明没有计算过,于是递归计算(所谓递归总是通过借助下一步的结果来计算当前步骤)
  33. for( int i= 0;i*arr[index]<=aim;i++){
  34. res+= this.process(arr,index+ 1,aim-arr[index]*i,dp);
  35. }
  36. }
  37. //把当前计算得到的局部结果放入到结果集合中去,判断res是否存在,如果res有值表示当前情况下有可行方案,于是给dp[index][aim]赋值,如果res为0表示没有可行方案于是赋值为-1专门表示不可行
  38. dp[index][aim]=res== 0?- 1:res;
  39. return res;
  40. }
  41. }

方法三:动态规划法

经典的动态规划方法,动态规划的操作方法具有很强的可通用性要掌握套路并会举一反三

对于找零钱这类问题,思路是将其零散化,通过转化成为多个零散的子问题,使得这些零散的子问题可以通过逐步递进地利用已经有的结果来计算当期的结果。即考虑将当前这个复杂的问题分解,分解成为细小的问题,这些细小问题的最低级情况下的解要求是可以直接得到的,于是根据这些可以直接得到的初始条件逐步向后面计算,每次后面的计算总是可以使用前面已经计算得到的结果来进行计算,这样通过循环计算前面的结果直到计算出最后需要求解的复杂问题。

目标值从aim=0开始

如何分解一个复杂问题?动态规划的方法是使用数组来进行分解,例如已知当前的复杂问题是:使用数组arr[0~n]来求出拼凑结果值为aim的方案数目,那么将这个问题分解成为N行,aim+1列的矩阵dp[n][aim+1],矩阵中每个值dp[i][j]表示使用元素arr[0~i]来拼凑出目标值j的方案数目,于是将复杂问题转化成为了N*(aim+1)个子问题,并且可知这个矩阵中有一些最低级的问题的结果是可以直接得到的,例如对于第一列dp[i][0],含义是使用arr[0~i]这几个元素来拼凑出目标值aim=0,那么显然方案数目都是1种。于是dp[i][0]=1,对于第一行dp[0][j],含义是使用第一个元素arr[0]来拼凑出目标值j的方案数目,显然需要判断j/arr[0]==0是否为0,如果为0表示可以拼凑,于是拼凑方案dp[0][j]是1,否则表示不可拼凑,于是拼凑方案dp[0][j]是0。这是该动态规划问题的初始条件,只有初始条件建立好了之后才可以进行递推的计算。对于任意的一个值dp[i][j]表示使用arr[0~i]的元素来拼凑出目标值为j的方案数目,总结规律可以发现:dp[i][j]=dp[i-1][j]+dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]+……即动态规划方法中要找出:当前问题如何转换为使用之前已经解决的问题,即之前已经拥有的结果如何利用。

于是可以当前问题dp[i][j]即i行j列的问题可以转换为前面i-1行j列,i-1行j-arr[i]列,i-1行j-arr[i]*2列……的结果的相加即可,显然都是对已经有的结果的处理而已,很简单。注意:如何保证在计算dp[i][j]时前面一行的前面几列已经计算出来了呢?这就要对动态规划矩阵中的计算顺序路径作出规定,只有按行从上到下,每行从左到右进行计算,才能保证在计算dp[i][j]时dp[i-1][j]+dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]+……都已经计算出来,否则就无法解决问题。因此在本问题中,在计算出了第一行第一列的结果dp[i][j]之后,对之后的dp[i][j]不能随意计算,必须按照从第1行第2列开始逐行逐列地进行计算,可以使用一个双层遍历来进行计算所有值。当所有dp[i][j]计算完成后,矩阵右下角的元素值dp[n-1][aim]就是所求的结果值。

总结:动态规划问题在求解时的思路是使用一个二维数组来分解问题,将复杂问题分解成为一系列局部的问题,并且按照规定的路径先后计算出一系列的结果值,这条计算路径需要保证当前正在计算的结果可以使用之前已经计算出的结果来直接计算得到结果,当按照这条路径计算出所有的值后,最终问题对应的结果也就计算出来了。

对于任意一个dp[i][j]在求解的过程中,需要对i-1行中的若干个结点值进行求和运算,即要通过一个循环函数按照for(int k=0,k*arr[i]<j,k++)对该列进行遍历和枚举,于是对于每个结点的枚举的时间复杂度是O(aim),由于要对n*(aim+1)个结点进行枚举,因此总的时间复杂度是O(n*aim^2);

 

动态规划算法与记忆搜索算法:

记忆搜索其实与动态规划的本质非常类似,都是讲已经计算过的局部结果值进行保留以便在之后需要求的时候可以直接重复利用,因此记忆搜索算法的时间复杂度也是O(n*aim^2).

动态规划算法与记忆搜索算法的不同之处在于:对于记忆搜索算法,在计算时是没有计算路径的,它就是按照遍历递归的顺序进行计算,遇到出现过的值就直接拿来用,遇到没有计算过的值就进行计算并将其保留,其实这样也并不会造成比动态规划更多的计算,对重复结果的利用率和动态规划是相同的,只是说它的计算过程是没有路径的。对于动态规划,它的计算时有固定路径的,例如上题中,先计算出第1行和第1列,然后逐行逐列地进行计算,直到计算出最后的结果。

动态规划的这种固定的认为设置的计算顺序并没与直接带来时间上的效益,但是由于这种计算顺序,使得可能出现一些有用的特性,从而使得进一步的优化调整成为可能,例如对于上面dp[i][j]=dp[i-1][j]+dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]+……的计算过程可以进行进一步的简化,分析发现,例如图中所示,dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]的计算结果就是dp[i][j-arr[i]]的计算结果,于是dp[i][j]= dp[i-1][j]+ dp[i][j-arr[i]],同时由于这2项都出现在dp[i][j]的计算顺序之前,因此可以直接拿来使用,因此对于每一项dp[i][j]在计算时省去了遍历枚举的过程,于是每一个dp[i][j]的计算时间复杂度降低为O(1),因此总的时间复杂度降低为O(n*aim)。这个规律是因为计算时严格按照逐行逐列路径进行计算时才有的,因此使用动态规划时按照固定路径进行计算可以为进一步的优化带来可能。

 

记忆搜索方法与动态规划方法的联系:

1.记忆化搜索方法就是某种形态的动态规划方法

2.记忆化搜索不关心到达某一个递归过程的路径,只是单纯地对计算过的递归过程进行记录,避免重复的递归过程。

3.动态规划的方法则是规定好每一个递归过程的计算顺序,依次进行计算,后面的计算过程严格依赖前面的计算过程。

4.两者都是空间换时间的方法,也是枚举的过程,区别就在于动态规划规定计算顺序而记忆搜索不用规定。

什么是动态规划方法:

1.其本质是利用申请的空间来记录每一个暴力搜索的计算结果,下次要用结果的时候直接使用而不再进行重复的递归过程。

2.动态规划规定每一种递归状态的计算顺序,依次进行计算。

 

面试找你哥遇到的暴力递归题目可以优化为动态规划方法的大体过程:

1.实现暴力递归方法

2.在暴力搜索方法的函数中看看哪些参数可以代表一个递归过程(递归函数调用时传入的参数)。

3.找到代表递归过程的参数之后,记忆化搜索的方法非常容易实现。

4.通过分析记忆化搜索的依赖路径,进而实现动态规划。

5.根据记忆化搜索方法该出动态规划方法,进而看看是否能够化简,如果能够化简,进一步实现时间复杂度更低的动态规划方法。

 

动态规划的难点关键还是在于对问题本身的理解,即还是要先会得到暴力搜索的解决方案,再次基础上才能进一步优化出动态规划方法。动态规划本身只是一种套路,一种对规律的处理方式,先人们在处理复杂问题时,是通过暴力搜索的方法来解决的,但是发现在暴力搜索的过程中,可以重复使用一些计算结果,于是总结出了一套可以利用到重复计算结果的方法套路,这个方法套路就是动态规划方法,因此动态规划方法更多的是一种死板的套路,处理方式都是雷同的,因此学好这个套路再理解感悟,要有信息解决复杂的动态规划问题。

 

在解决复杂问题时,可以通过先提出暴力搜索方法à改进为记忆搜索算法à改进为动态规划方法à对动态规划问题进行进一步优化这种思路来解决。但是对于一些经典的教科书级别的动态规划问题,面试官需要直接写出动态规划的算法,因此对于一些经典的动态规划问题,要熟练掌握它的动态规划解决方法。

 

动态规划方法的关键点:

1.最优化原理,也就是最优子结构性质。这指的是一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简单来说就是一个最优化策略的自策略总是最优的,如果一个问题满足最优化原理,就成其具有最优化结构性质。

2.无后效性。指的是某状态下决策的收益,只与状态和决策相关,与到达该状态的方式无关。

3.子问题的重叠性,动态规划将原来具有指数级时间复杂度的暴力搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。


 
 
  1. import java.util.*;
  2. //零钱凑整问题:动态规划算法
  3. public class Exchange {
  4. public int countWays(int[] penny, int n, int aim) {
  5. //特殊输入
  6. if(penny== null||penny.length<= 0||aim<= 0) return 0;
  7. //动态规划需要创建一个二维数组来存储计算结果
  8. int[][] dp= new int[n][aim+ 1];
  9. //对第1列的值进行计算
  10. for( int i= 0;i<n;i++){
  11. dp[i][ 0]= 1;
  12. }
  13. //对第1行的值进行计算
  14. for( int j= 0;j<=aim;j++){
  15. dp[ 0][j]=j%penny[ 0]== 0? 1: 0;
  16. }
  17. //从上到下从左到右计算dp[i][j]
  18. for( int i= 1;i<n;i++){
  19. for( int j= 1;j<=aim;j++){
  20. if(j-penny[i]>= 0){
  21. dp[i][j]=dp[i- 1][j]+dp[i][j-penny[i]];
  22. } else{
  23. dp[i][j]=dp[i- 1][j];
  24. }
  25. }
  26. }
  27. //返回结果:矩阵右下角的值就是结果
  28. return dp[n- 1][aim];
  29. }
  30. }
  31. 方法二:只需要使用 1个数组
  32. import java.util.*;
  33. public class Exchange {
  34. public int countWays(int[] penny, int n, int aim) {
  35. int[] dp = new int[aim+ 1];
  36. for( int i = 0;i <= aim;i++)
  37. if(i % penny[ 0] == 0)
  38. dp[i] = 1;
  39. for( int i = 1;i<n; i++)
  40. for( int j = 1;j<=aim;j++)
  41. if(j>=penny[i])
  42. dp[j] += dp[j-penny[i]];
  43. return dp[aim];
  44. }
  45. }

原理:每一次对i的大循环表示可以使用的零钱种类,i=0时表示只能使用元素penny[0]来拼凑出各种目标值j,当i=1时表示可以使用penny[0~1]这2个元素来拼凑出目标值j,那么它的拼凑有2中方案:

1:依然不使用penny[1]来进行拼凑,于是方法数目是dp[j];

2:使用penny[2]和之前的元素来拼凑,于是是使用一个penny[2],剩余目标值由原来的元素来拼凑;是dp[j-penny[2]];

于是使用penny[0~1]的方案数目就是:penny[2]+ dp[j-penny[2]]

这种解法高度精细,只需要使用1个数组代码很简单,但是需要对过程高度理解和掌握,高度依赖于逻辑失去了使用动态规划的套路性,因此还是使用二维数组的方法来解决动态规划问题。其实这种方法与二维数组原理是一样的,只是二维数组中将各个计算子模块写在了二维数组中,这里是通过迭代重合在了同一个数组中,进行了叠加而已,还是使用二维数组来解决问题。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值