《剑指offer》算法题第三天
今日题目:
- 斐波那契数列
- 青蛙跳台阶问题(及其变种:变态跳台阶)
- 矩形覆盖
- 旋转数组的最小数字
- 矩阵中的路径
- 机器人的运动范围
细心的同学会发现,第1,2,3题其实对应的是《剑指》书上的同一道题目,即第10题斐波那契数列,这类问题属于递归问题,虽然思路比较简单,但却是属于那种不看答案想不出来,看了答案恍然大悟的题目,因此在平时同学们和博主都应该多练练这一类型的题目,培养这种递归的思维。有趣的是,博主在做题的时候发现这三道题目是可以用动态规划的思路来解决的,而且往往动态规划的所用的时间是要低于递归的。在后文中会以矩形覆盖为例子来说明。
第5,6两题是回溯法的题目,在leetcode上有不少这一类型的题,只要多做练习,这一类型的题目还算是比较容易上手的。本文只说明第6题。
3.矩行覆盖
题目描述:
我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
思路:
以n=8为例,我们先把2*8矩形的覆盖方法记为f(8)。当用一个2*1的小矩形取覆盖大矩形的最左边时有两种选择:竖着放或横着放。如果选择竖着放,那么右边还剩下2*7的区域,也就是f(7);如果横着放在左上角,左下角必须也放置一个矩形,那么右边还剩下2*6的区域,也就是f(6)。所以我们有f(8)=f(7)+f(6)。从这边我们可以看出来,其实这就是一个斐波那契数列。
理清思路后,接下来我们分别用递归以及动态规划的方法来实现。
递归代码如下:
1 public class Solution { 2 public int RectCover(int target) { 3 if(target == 0) return 0; 4 if(target == 1) return 1; 5 if(target == 2) return 2; 6 return RectCover(target-1)+RectCover(target-2); 7 } 8 }
代码比较简单、清晰,但是效率非常的低,他的时间复杂度不难推出来是成指数的。在牛客网上的运行时间为447ms。
接下来我们看一下动态规划是如何解决的,代码如下:
1 public class Solution { 2 public int RectCover(int target) { 3 if(target == 0) return 0; 4 int[] dp = new int[target+1]; 5 dp[0] = 1; 6 dp[1] = 1; 7 for(int i = 2; i <= target; i++){ 8 dp[i] = dp[i-1] + dp[i-2]; 9 } 10 return dp[target]; 11 } 12 }
很明显,这段代码的时间复杂度仅为O(n),当然空间复杂度也为O(n),所以,你也可以这么做:
1 public class Solution { 2 public int RectCover(int target) { 3 if(target == 0) return 0; 4 int num1 = 1; 5 int num2 = 1; 6 int res = 1; 7 for(int i = 2; i <= target; i++){ 8 res = num1 + num2; 9 num1 = num2; 10 num2 = res; 11 } 12 return res; 13 } 14 }
这样空间复杂度也就仅为常数啦。动态规划在同样的条件下运行时间仅为12ms。
4.旋转数组的最小数字
题目描述:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
这题在leetcode上面是有原题的,当时博主在做的时候并没有太多的想法,直接从后往前遍历数组,如果某个数比它前面的数字要小的话,那么这个数就是最小的,时间复杂度为O(n)。代码如下:
1 public class Solution { 2 public int minNumberInRotateArray(int [] array) { 3 if(array.length ==0) return 0; 4 for(int i = array.length-1; i >0; i--){ 5 if(array[i-1] > array[i]) 6 return array[i]; 7 } 8 return array[0]; 9 } 10 }
但是在《剑指》上,这题有一个时间复杂度为O(logn)的思路:利用二分查找的思路来寻找最小数字,具体的过程阐述起来过于复杂,感兴趣的同学可以阅读书上第83页,这边只贴出代码:
1 public class Solution { 2 public int minNumberInRotateArray(int [] array) { 3 if(array.length ==0) return 0; 4 int s = 0; 5 int e = array.length - 1; 6 if(array[s] < array[e])//表示旋转了0个数字到数组后面 7 return array[s]; 8 while(s != e){ 9 if(e - s == 1) 10 return array[e]; 11 int mid = (s+e)/2; 12 if(array[mid] == array[s] && array[mid] == array[e])//处理特殊情况 13 return orderFind(array,s,e); 14 if(array[mid] >= array[s]) 15 s = mid; 16 else if(array[mid] <= array[e]) 17 e = mid; 18 } 19 return array[s]; 20 } 21 public int orderFind(int[] array,int start,int end){ 22 int res = array[start]; 23 for(int n:array){ 24 res = (n < res)?n:res; 25 } 26 return res; 27 } 28 }
6. 机器人的运动范围
题目描述: 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
思路:
这是比较直接的回溯法的题目,首先使用一个数组来标记机器人已经走过的放歌,防止重复,然后使用回溯法让机器人运动,每次运动到一个方格都判断一下是否合法,如果合法,计数器加一接着往下走;否则返回0;
代码如下:
1 public class Solution { 2 public int movingCount(int threshold, int rows, int cols) 3 { 4 boolean[][] visited = new boolean[rows][cols]; 5 return movingCount(threshold,visited,rows,cols,0,0); 6 } 7 8 public int movingCount(int k,boolean[][] visited,int rows,int cols, 9 int row,int col) 10 { 11 if(row < 0 || row >= rows|| 12 col < 0 || col >= cols || visited[row][col]) 13 return 0; 14 visited[row][col] = true; 15 int sum = 0; 16 int row_tmp = row; 17 int col_tmp = col; 18 do{ 19 sum += (row%10); 20 row /= 10; 21 }while(row > 0); 22 23 do{ 24 sum += (col%10); 25 col /= 10; 26 }while(col > 0); 27 row = row_tmp; 28 col = col_tmp; 29 if(sum > k) 30 return 0; 31 else{ 32 return movingCount(k,visited,rows,cols,row+1,col)+ 33 movingCount(k,visited,rows,cols,row-1,col)+ 34 movingCount(k,visited,rows,cols,row,col+1)+ 35 movingCount(k,visited,rows,cols,row,col-1)+1; 36 } 37 } 38 }