[算法]死磕遞歸和動態規劃題
最近在忙著找實習,因而做了大量的筆試算法題,阿裏,網易,騰訊,華為,發現各大廠商都喜歡出遞歸和動態規劃題,而且出的特別多,這種題以前一直沒有搞懂,總是半懂狀態,現在感覺有必要好好整理一下。
1. 斐波那契數列
談到遞歸問題,我們不妨先從斐波那契數列開始,這個大家應該都不陌生吧,1,1,2,3,5,8......除了第一項和第二項為1外,對於第N項,有F(N) = F(N - 1) + F(N - 2)。
我們先看一下暴力求解,其時間復雜度為O(2^N):
public static int f1(int n) { if(n < 1){return 0; } if(n == 1 || n == 2){ return 1; } return f1(n - 1) + f1(n - 2); }
當然我們可以優化成時間復雜度為O(N),如下:
public static int f2(int n){ if(n < 1){ return 0; } if(n == 1 || n == 2){ return 1; }int pre = 1;//第一個 int res = 1;//第二個 int temp = 0; for (int i = 3; i <= n; i++) { temp = res; res += pre; pre = temp; } return res; }
當然這道題還可以進一步優化成時間復雜度O(logN),采用矩陣乘法,這裏就不說了,一般O(N)足夠了。我們通過這道題總結規律,遞歸問題,進入一個方法,先寫出一個終止條件(狀態方程),然後根據題目,找出轉移方程,進行遞歸。
同類型的題目列舉:
2. 臺階問題
有n級臺階,一個人每次上一級或者兩級,問有多少種走完N級臺階的方法。為了防止溢出,請將結果Mod 1000000007。
給定一個正整數int N,請返回一個數,代表上樓的方式數。保證N小於等於100000。
這道題類似於斐波那契數列,跳上N級臺階的情況,要麽是從N-2級臺階直接跨2級臺階,要麽是從N-1級臺階跨1級臺階,即轉移方程是f(N) = f(N - 1) + f(N - 2),狀態方程為f(1) = 1,f(2) = 2。
類比上一道題,得到兩種求解方法如下:
時間復雜度為O(2^N):
public static int f1(int n) { if(n < 1){ return 0; } if(n == 1 || n == 2){ return n; } return f1(n - 1) + f1(n - 2); }
時間復雜度為O(N):
public static int f2(int n){ if(n < 1){ return 0; } if(n == 1 || n == 2){ return n; } int pre = 1;//第一個數 int res = 2;//第二個數 int temp = 0; for (int i = 3; i <= n; i++) { temp = res; res += pre; pre = temp; } return res; }
3. 生兔子問題
假設成熟的兔子每年生1只兔子,並且永遠不會死,第一年有1只成熟的兔子,從第二年開始,開始生兔子,每只小兔子3年之後成熟又可以繼續生。給出整數N,求出N年後兔子的數量。
public static int f1(int n) { if(n < 1){ return 0; } if(n == 1 || n == 2 || n == 3){ return n; } return f1(n - 1) + f1(n - 3); }
111
public static int f2(int n){ if(n < 1){ return 0; } if(n == 1 || n == 2 || n == 3){ return n; } int prepre = 1;//第一個數 int pre = 2;//第二個數 int res = 3;//第三個數 int temp1 = 0; int temp2 = 0; for (int i = 4; i <= n; i++) { temp1 = pre; temp2 = res; res += prepre; prepre = temp1; pre = temp2; } return res; }
4. 找零錢問題
有數組penny,penny中所有的值都為正數且不重復。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim(小於等於1000)代表要找的錢數,求換錢有多少種方法。
給定數組penny及它的大小(小於等於50),同時給定一個整數aim,請返回有多少種方法可以湊成aim。
測試樣例:[1,2,4],3,3
返回:2
暴力求解法:
public static int process1(int[] arr, int index, int aim){ int res = 0; if(index == arr.length){ res = aim == 0 ? 1 : 0; }else{ for (int i = 0; i * arr[index] <= aim; i++) { res += process1(arr, index + 1, aim - i * arr[index]); } } return res; }
動態規劃法:
public static int process2(int[] arr, int aim){ int[][] dp = new int[arr.length][aim + 1]; //先賦值第一列,全是1 for (int i = 0; i < dp.length; i++) { dp[i][0] = 1; } //再賦值第一行 for (int i = 1; i * arr[0] <= aim; i++) { dp[0][ i * arr[0]] = 1; } //給所有元素賦值 for (int i = 1; i < dp.length; i++) { for (int j = 1; j < dp[i].length; j++) { dp[i][j] = dp[i - 1][j]; dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0; } } return dp[arr.length - 1][aim]; }
5. 矩陣最小路徑
有一個矩陣map,它每個格子有一個權值。從左上角的格子開始每次只能向右或者向下走,最後到達右下角的位置,路徑上所有的數字累加起來就是路徑和,返回所有的路徑中最小的路徑和。
給定一個矩陣map及它的行數n和列數m,請返回最小路徑和。保證行列數均小於等於100.
測試樣例:[[1,2,3],[1,1,1]],2,3
返回:4
public int minPathSum(int[][] m){ int row = m.length; int col = m[0].length; int[][] dp = new int[row][col]; dp[0][0] = m[0][0]; //給行初始化 for (int i = 1; i < row; i++) { dp[i][0] = dp[i - 1][0] + m[i][0]; } //給列初始化 for (int i = 1; i < col; i++) { dp[0][i] = dp[0][i - 1] + m[0][i]; } //給剩余元素初始化 for (int i = 1; i < row; i++) { for (int j = 1; j < col; j++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j]; } } return dp[row - 1][col - 1]; }
6. 最長遞增子序列
這是一個經典的LIS(即最長上升子序列)問題,請設計一個盡量優的解法求出序列的最長上升子序列的長度。
給定一個序列A及它的長度n(長度小於等於500),請返回LIS的長度。
測試樣例:[1,4,2,5,3],5
返回:3
public static int[] getLIS(int[] A) { // write code here // 先求出dp數組 int[] dp = new int[A.length]; for (int i = 0; i < A.length; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { if(A[i] > A[j]){ dp[i] = Math.max(dp[i], dp[j] + 1); } } } //然後根據條件求出來遞增子序列是什麽 //dp[i]的上一個比它小1,並且A的值要小 //先求出dp中的最大值 int index = 0;//最大值的下標 int max = 0;//最大值,最長子序列的長度 for (int i = 0; i < dp.length; i++) { if(dp[i] > max){ max = dp[i]; index = i; } } int[] lis = new int[max]; lis[--max] = A[index]; int now = index;//當前比較的元素 for (int i = index - 1; i >= 0; i--) { if(A[i] < A[now] && dp[i] + 1 == dp[now]){ lis[--max] = A[i]; now = i; } } return lis; }
7. 最長公共子序列
給定兩個字符串A和B,返回兩個字符串的最長公共子序列的長度。例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最長公共子序列。
給定兩個字符串A和B,同時給定兩個串的長度n和m,請返回最長公共子序列的長度。保證兩串長度均小於等於300。
測試樣例:"1A2C3D4B56",10,"B1D23CA45B6A",12
返回:6
public static String getLCS(String A, String B) { int dp[][] = new int[A.length()][B.length()]; dp[0][0] = A.charAt(0) == B.charAt(0) ? 1 : 0; for (int i = 1; i < B.length(); i++) { dp[0][i] = Math.max(dp[0][i - 1], A.charAt(0) == B.charAt(i) ? 1 : 0); } for (int i = 1; i < A.length(); i++) { dp[i][0] = Math.max(dp[i - 1][0], A.charAt(i) == B.charAt(0) ? 1 : 0); } for (int i = 1; i < A.length(); i++) { for (int j = 1; j < B.length(); j++) { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); if(A.charAt(i) == B.charAt(j)){ dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1); } } } int num = dp[A.length() - 1][B.length() - 1];//最長公共子序列的長度 System.out.println(num); StringBuilder sb = new StringBuilder(); int m = A.length() - 1; int n = B.length() - 1; while(num > 0){ if(m > 0 && dp[m - 1][n] == dp[m][n]){ m--; }else if(n > 0 && dp[m][n - 1] == dp[m][n]){ n--; }else{ sb.insert(0, A.charAt(m));//因為此時A.charAt(m) == B.charAt(n),所以選哪一個均可 m--; n--; num--; } } return sb.toString(); }
8. 最長回文子字符串
回文字符串的子串也是回文,比如P[i,j](表示以i開始以j結束的子串)是回文字符串,
那麽P[i+1,j-1]也是回文字符串。這樣最長回文子串就能分解成一系列子問題了。
這樣需要額外的空間O(N^2),算法復雜度也是O(N^2)。 首先定義狀態方程和轉移方程:
P[i,j]=0表示子串[i,j]不是回文串。P[i,j]=1表示子串[i,j]是回文串。
P[i,i]=1
P[i,j]{=P[i+1,j-1],if(s[i]==s[j])
=0 ,if(s[i]!=s[j])}
public static String longestPalindrome(String s){ if(s == null || s.length() == 1){ return s; } int len = s.length(); //dp[i][j]=1 表示子串i-j為回文字符串 int[][] dp = new int[len][len]; int start = 0; int maxlen = 0; for (int i = 0; i < len; i++) { dp[i][i] = 1; if(i < len - 1 && s.charAt(i) == s.charAt(i + 1)){ dp[i][i + 1] = 1; start = i; maxlen = 2; } } //m代表最長子串長度 for (int m = 3; m <= len; m++) { for (int i = 0; i < len - m + 1; i++) { int j = i + m - 1; if(dp[i + 1][j - 1] == 1 && s.charAt(i) == s.charAt(j)){ dp[i][j] = 1; start = i; maxlen = m; } } } return s.substring(start, start + maxlen); }
9. 0-1背包問題
一個背包有一定的承重cap,有N件物品,每件都有自己的價值,記錄在數組v中,也都有自己的重量,記錄在數組w中,每件物品只能選擇要裝入背包還是不裝入背包,要求在不超過背包承重的前提下,選出物品的總價值最大。
給定物品的重量w價值v及物品數n和承重cap。請返回最大總價值。
測試樣例:[1,2,3],[1,2,3],3,6
返回:6
public static int[] maxValue(int[] w, int[] v, int cap) { // write code here int[][] dp = new int[w.length + 1][cap + 1]; // 第一行和第一列不用賦初值,因為都是0 for (int i = 1; i <= w.length; i++) { for (int j = 1; j <= cap; j++) { dp[i][j] = dp[i - 1][j]; if (j >= w[i - 1]) { dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]); } } } int maxValue = dp[w.length][cap];// 獲取的最大價值 /** * 到這一步,可以確定的是可能獲得的最大價值,但是我們並不清楚具體選擇哪幾樣物品能獲得最大價值。 * * 另起一個 x[] 數組,x[i]=0表示不拿,x[i]=1表示拿。 * * dp[n][c]為最優值,如果dp[n][c]=dp[n-1][c] ,說明有沒有第n件物品都一樣,則x[n]=0 ; 否則 * x[n]=1。當x[n]=0時,由dp[n-1][c]繼續構造最優解;當x[n]=1時,則由dp[n-1][c-w[i]]繼續構造最優解。以此類推,可構造出所有的最優解。 */ int[] x = new int[w.length + 1];//不看0位,為了和矩陣對應,x[0]不用看 for (int i = w.length; i > 1; i--) { if(dp[i][cap] == dp[i - 1][cap]){ x[i] = 0; }else{ x[i] = 1; cap -= w[i - 1]; } } x[1] = dp[1][cap] > 0 ? 1 : 0; return x; }
[算法]死磕遞歸和動態規劃題