算法练习day15——190403(简介、求n!、汉诺塔、打印字符串的子序列、打印字符串的全排列、母牛生小牛、最小路径和、累加和是否达到给定值)
1. 簡介
動態規劃是為了優化暴力嘗試的。
2. 求n!
2.1 一般思路
public static long getFactorial2(int n) {long result = 1L;for (int i = 1; i <= n; i++) {result *= i;}return result; }2.2 遞歸
- 要求n!,求出(n-1)!,再乘以n即可;
- 求(n-1)!,先求出(n-2)!,再乘以(n-1)即可;
- ...
- 需要有個Base case,即n=1時,不再遞歸,直接返回1。
即:n!的結果依賴于n-1;n-1依賴于n-2;...;2依賴于1。
計算的時候,將順序逆過來,由1得到2,再得到3,...,再得到n-1,得到n。
一般方法的思路:把不依賴于別的的狀態給出,然后計算依賴于它的的狀態,在往后計算
public static long getFactorial1(int n) {if (n == 1) {return 1L;}return (long) n * getFactorial1(n - 1); }3.漢諾塔問題
打印n層漢諾塔從最左邊移動到最右邊的全部過程
3.1 簡介
如圖:
將左桿上的盤全部移到右桿上,中桿可作為輔助,要求大盤不能壓小盤。
3.1.1 具體步驟
3.2 遞歸思路分析
有from,to,help三個桿,1~N個盤在from桿上,需把它們移到to桿上??梢岳胔elp桿。
運行結果:
3.3 將上述遞歸過程拆為6個子過程
L->R(M作為輔助)具體包括:
L->M(R作為輔助)具體包括:
后面四個步驟類似。
3.4 代碼實現
package Recursive;public class Hanoi {public static void main(String[] args) {moveLeftToRight(3);}public static void moveLeftToRight(int N) {if(N==1)System.out.println("Move "+N+" from 左 to 右");else{moveLeftToMiddle(N-1);System.out.println("Move "+N+" from 左 to 右");moveMiddleToRight(N-1);}}public static void moveLeftToMiddle(int N) {if(N==1)System.out.println("Move "+N+" from 左 to 中");else{moveLeftToRight(N-1);System.out.println("Move "+N+" from 左 to 中");moveRightToMiddle(N-1);}}public static void moveMiddleToRight(int N) {if(N==1)System.out.println("Move "+N+" from 中 to 右");else{moveMiddleToLeft(N-1);System.out.println("Move "+N+" from 中 to 右");moveLeftToRight(N-1);}}public static void moveRightToMiddle(int N) {if(N==1)System.out.println("Move "+N+" from 右 to 中");else{moveRightToLeft(N-1);System.out.println("Move "+N+" from 右 to 中");moveLeftToMiddle(N-1);}}public static void moveRightToLeft(int N) {if(N==1)System.out.println("Move "+N+" from 右 to 左");else{moveRightToMiddle(N-1);System.out.println("Move "+N+" from 右 to 左");moveMiddleToLeft(N-1);}}public static void moveMiddleToLeft(int N) {if(N==1)System.out.println("Move "+N+" from 中 to 左");else{moveMiddleToRight(N-1);System.out.println("Move "+N+" from 中 to 左");moveRightToLeft(N-1);}} }4.打印字符串的子序列(不是子串),包括空序列
4.1 分析
每走一步,都有兩個決策:
- 加當前位置上的字符
- 不加這個字符
4.2 代碼實現
package Recursive;public class PrintAllSub {public static void main(String[] args) {String str="abc";printAllSub(str.toCharArray(),0,"");}public static void printAllSub(char[] str,int i,String res) {if(i==str.length) {System.out.println(res);return;}printAllSub(str,i+1,res);printAllSub(str,i+1,res+str[i]);} }運行結果:
5.打印一個字符串的全排列
5.1 分析
第一步:確認第一個位置的字符,即第一個位置的字符一次和后面位置的字符進行交換
第二步:確認第二個位置的字符,...
...
最后,知道表示位置的index=字符串長度時,打印。
5.2 代碼實現
package Recursive;public class PrintAllPermutations {public static void main(String[] args) {String str="abc";printAllPermutations(str);}public static void printAllPermutations(String str) {char[] chs=str.toCharArray();process(chs,0);}public static void process(char[] chs,int i) {if(i==chs.length) {System.out.println(chs);return;}else {for(int j=i;j<chs.length;j++) {swap(chs,i,j);process(chs,i+1);swap(chs,i,j);//回歸原始位置,繼續下一輪交換}}}public static void swap(char[] chs, int i,int j) {char temp=chs[i];chs[i]=chs[j];chs[j]=temp;} }運行結果:
6.母牛生小牛
母牛每年生一只母牛,新出生的母牛成長三年后也能每年生一只母牛,假設牛不會死。
求N年后,母牛的數量。
6.1 分析
規律:
原因:第N年的牛個數=第N-1年牛的個數(牛不會死,去年的牛會保留到今年來)+向前數三年的牛的個數(新生的牛=可以生牛的母牛個數)
做法:寫出前幾項,得出規律,看規律是否合理。
時間復雜度:
有個更優的,時間復雜度為
- 利用線性代數中矩陣乘法來解。
- 斐波那契數列也存在的解
7.暴力遞歸試出動態規劃——最小路徑和
給一個二維數組,其中的每個數都是正數。要求從左上角走到右下角,每一步只能向右或者向下。沿途經過的數字要累加起來,返回最小的路徑和。
7.1 遞歸版本
package Recursive;public class MinRoadSum {public static void main(String[] args) {int[][] arr= {{3,2,1,0},{7,5,0,1},{3,7,6,2}};System.out.println(minRoadSum(arr));}public static int minRoadSum(int[][] arr) {int res=process(arr,0,0);//當前數組,當前的行號,當前列號,當前路徑和return res;}//從(i,j)出發,到達右下角的位子public static int process(int[][] arr,int i,int j) {int hlen=arr.length-1;int llen=arr[0].length-1;if(i==hlen&&j==llen) return arr[i][j];if(i==hlen)//只能往右走return arr[i][j]+process(arr,i,j+1);if(j==llen)//只能往下右走return arr[i][j]+process(arr,i+1,j);int down=process(arr,i+1,j);//下邊位置到右下角的最短路徑和int right=process(arr,i,j+1);return arr[i][j]+Math.min(down, right);} }這是暴力:
有大量重復解產生,如上圖中的f(1,1)。
整個過程中,重復狀態很多——暴力遞歸不行的原因。
7.2 改進
在求解子過程時,將結果保存,下次用到的時候直接拿,可減少計算量。
——把1-1作為一個key,將f(1,1)作為value,存入哈希表中
7.3 遞歸可以改為動態規劃的要求
——在展開遞歸的過程中,有重復的計算,而且這個重復的狀態與到達它的路徑無關。
比如點(1,1),它到右下角點的最短路徑與(0,0)是通過哪條路徑到達它的無關。
——這種問題叫做無后效性問題?!灰獏刀?#xff08;如,1,1),返回值就確定了(f(1,1)是一定的)。
有后效性問題:漢諾塔問題、N皇后問題?!白鞒龅倪x擇會影響后面的決策。
上題中,i和j一旦確定,返回值就是確定的。那么可以將所有的返回值存在和路徑數組大小一樣的數組dp中。
- dp數組中位置(i,j)表示:路徑數組中的位置(i,j)到右下角點的最短路徑和。
我們要的最終結果是(0,0)位置的值,標為※,
然后看可以確定的位置的值——base case,即右下角位置的值,就是matrix中右下角位置的值0;
if(i==hlen&&j==llen) return arr[i][j];接著看設置不被依賴的值:最后一行和最后一列
如果在最后一行,則當前位置的值=當前matrix位置中對應的值+右邊位置的最短路徑和(即dp中此位置右邊的值)
if(i==hlen)//只能往右走return arr[i][j]+process(arr,i,j+1);if(j==llen)//只能往下走return arr[i][j]+process(arr,i+1,j);最后一列也一樣:
接著分析一個普遍位置是怎么依賴的:
int down=process(arr,i+1,j);//下邊位置到右下角的最短路徑和 int right=process(arr,i,j+1);return arr[i][j]+Math.min(down,right);在當前(i,j),它需要:
- 右邊的位置:(i,j+1)
- 左邊的位置:(i+1,j)
- 選兩者中較小的
由于最后一行和最后一列已確定,則右下角位置的左上方的值就可以確定。它左邊的位置也可以求出來:
中間的位置從右到左,再從下到上,依次推倒頂部,即可得到答案。
以上就是暴力遞歸改為動態規劃的統一套路。
- 寫出可變版本
- 分析可變參數
- 哪些可變參數可以代表返回值的狀態
- 參數時幾維的,dp就是一張幾維表
- 在dp中標出最終需要的點
- 回到base case中,將完全不依賴的位置的值設置好
- 分析一個普遍位置需要哪些位置,逆著回去,就會說填表的順序。
8.累加和是否可達到給定值
給定一個數組arr,其中的數全為正數。如果可以任意選擇arr中的數字。能不能累加得到正數aim。返回true或false。
8.1 分析
和子序列一樣
比如給定:
3,2,7,13
aim=9。是否能加到9?
f(0,0):目前在0位置,形成的和是0;
可按照子序列的方法分析,在每個位置選擇要不要前一個位置的數。
到最后一層的時候,如果發現有一個累加和為9,返回true,然后一直往上,每一層只要有一個true,則最后結果肯定為true。
8.2 代碼實現
package Recursive;public class IsHaveSum {public static void main(String[] args) {int[] arr= {1,4,8};int aim=12;System.out.println(isHaveSum(arr,0,0,aim));}public static boolean isHaveSum(int[] arr,int i,int sum,int aim) {if(i==arr.length)return sum==aim;return isHaveSum(arr,i+1,sum,aim)||isHaveSum(arr,i+1,sum+arr[i],aim);} }8.3 分析是否有后效性
一個序列為3,2,5,...:
- 選3,2,沒選5,則是f(3,5)。
- 沒選3,2,選了5,還是f(3,5)
所以是無后效性的。
3和5一旦確定,則返回值肯定確定。
(arr,i,sum,aim):四個參數中,arr和aim是確定的,i和sum是變化的——二維表
- LEN:數組中元素的個數,i的最大值;
- SUM:數組中所有元素的和,sum的最大值。
f(i,sum)肯定可以裝在上面兩個參數形成的二維表中。
8.4 轉換
8.4.1 找出最終需要的狀態
8.4.2 找出基本狀態
if(i==arr.length)//最后一行return sum==aim;aim>SUM直接返回false;
aim<SUM,則在最后一列SUM對應的位置為T(True)
0~SUM之間是以1為單位遞增的。
8.4.3 分析普遍狀態
return isHaveSum(arr,i+1,sum,aim)||isHaveSum(arr,i+1,sum+arr[i],aim);狀態(i,sum),依賴的狀態有:
- (i+1,sum):下一行正下面的狀態;
- (i+1,sum+arr[i]):假設arr[i]=a,則指的是下一行正下方往右推a個單位的狀態。
8.5 若給定數組中有負值
SUM的范圍:負值之和~正值之和
注意下標的對應。
比如:3,-2,5
則SUM的范圍:-2~8
下標的范圍:0~10
總結
以上是生活随笔為你收集整理的算法练习day15——190403(简介、求n!、汉诺塔、打印字符串的子序列、打印字符串的全排列、母牛生小牛、最小路径和、累加和是否达到给定值)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法练习day14——190402(贪心
- 下一篇: 算法练习day16——190404(KM