死磕遞歸和動態規劃算法

 

正文算法

最近在忙着找實習,於是作了大量的筆試算法題,阿里,網易,騰訊,華爲,發現各大廠商都喜歡出遞歸和動態規劃題,並且出的特別多,這種題之前一直沒有搞懂,老是半懂狀態,如今感受有必要好好整理一下。數組

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),以下:a,b=b,a+bui

複製代碼
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)足夠了。咱們經過這道題總結規律,遞歸問題,進入一個方法,先寫出一個終止條件,而後根據題目,找出遞推關係,進行遞歸。spa

同類型的題目有臺階問題和生兔子問題。.net

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):a,b=b,a+b

複製代碼
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年後兔子的數量。

時間複雜度爲O(2^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);
}
複製代碼

時間複雜度爲O(N):a,b,c=b,c,a+c

複製代碼
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. 找零錢問題

有數組arr,arr中全部的值都爲正數且不重複。每一個值表明一種面值的貨幣,每種面值的貨幣可使用任意張,再給定一個整數aim(小於等於1000)表明要找的錢數,求換錢有多少種方法。

給定數組arr及它的大小(小於等於50),同時給定一個整數aim,請返回有多少種方法能夠湊成aim。

測試樣例:
[1,2,4],3
返回:2

全部的動態規劃題本質都是優化後的暴力求解,通常動態規劃題是構造一個dp矩陣,第一行和第一列賦初值,而後根據遞推關係,由一個個子問題求出整個問題,即把剩餘位置的值填滿,說白了就是空間換時間。由於暴力求解會有大量的重複計算,動態規劃能夠有效地避免重複計算。

好比找零錢問題,咱們能夠當作0個arr[0],讓剩餘的組成aim,1個arr[0],讓剩餘的組成aim - 1 * arr[0],2個arr[0],讓剩餘的組成aim - 2 * arr[0],以此類推。爲何會產生重複計算,是由於比方我用了1個10元,0個5元,而後讓剩下的組成aim - 10和我用0個10元,2個5元,讓剩下的組成aim - 10本質是同樣的。

暴力求解法:

複製代碼
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;
}
複製代碼

動態規劃法:

首先思考如何設計dp矩陣,這裏咱們把行設置成arr下標,表明的就是利用[0...i]區間內組成aim的值的方法數,列表明的是aim值,從0取到aim。

咱們先給第一列賦值,由於aim是0,因此只有一種組合方式,就是每一個價值的紙幣都取0個,因此第一列全取1。

接下來看第一行,就是求arr[0]可以湊成的錢的方案,只要是其倍數的都能湊成,因此相應位置應該填寫1。

最後咱們肯定其餘位置,徹底不用arr[i]貨幣,只用剩下的,則方法數dp[i - 1][j].

用1個arr[i],方法數是dp[i - 1][j - 1 * arr[i]]。

用2個arr[i],方法數是dp[i - 1][j - 2 * arr[i]]。

以此類推,是上面那一行,通過化簡,能夠簡化成dp[i][j] = dp[i - 1][j] + dp[i][j - arr[i]]。這就是狀態轉移方程。

複製代碼
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
        List<Integer> list = new ArrayList<>();
        
        int[] dp = new int[A.length];
        dp[0] = 1;
        
        for (int i = 1; i < dp.length; i++) {
            dp[i] = 1;
            for(int j = 0; j < i; j++){
                if(A[j] < A[i]){
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        
        int maxIndex = dp.length - 1;
        for (int i = dp.length - 2; i >= 0; i--) {
            if(dp[i] > dp[maxIndex]){
                maxIndex = i;    
            }
        }
        
        list.add(A[maxIndex]);
        for (int i = maxIndex - 1; i >= 0; i--) {
            if(A[maxIndex] > A[i] && dp[maxIndex] == dp[i] + 1){
                list.add(A[i]);
                maxIndex = i;
            }
        }
        
        int[] nums = new int[list.size()];
        for(int i = 0; i < nums.length; i++){
            nums[nums.length - 1 - i] = list.get(i);
        }
        return nums;
    }
複製代碼

 

7. 最長公共子序列

給定兩個字符串A和B,返回兩個字符串的最長公共子序列的長度。例如,A="1A2C3D4B56」,B="B1D23CA45B6A」,」123456"或者"12C4B6"都是最長公共子序列。

給定兩個字符串AB,同時給定兩個串的長度nm,請返回最長公共子序列的長度。保證兩串長度均小於等於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. 最長公共子串

注意和上一道題進行區分,公共子串必須連續。

dp[i][j]表示以兩個字符串分別以第i和第j個字符結尾所能達到的公共子串的長度,

狀態轉移方程爲

if(str[i-1]=str[j-1])

dp[i][j]=dp[i-1][j-1]+1;

if(str[i-1]!=str[j-1])

dp[i][j]=0;

複製代碼
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 < A.length(); i++) {
            if(A.charAt(i) == B.charAt(0)){
                dp[i][0] = 1;
            }
        }
        
        for (int i = 1; i < B.length(); i++) {
            if(B.charAt(i) == A.charAt(0)){
                dp[0][i] = 1;
            }
        }
        
        for (int i = 1; i < A.length(); i++) {
            for (int j = 1; j < B.length(); j++) {
                if(A.charAt(i) == B.charAt(j)){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
            }
        }
        
        
        //找出最大值,即爲最長公共子串
        int max = 0;
        int index = 0;//記錄A字符串最長公共子字符串最後一個位置
        for (int i = 0; i < A.length(); i++) {
            for (int j = 0; j < B.length(); j++) {
                if(dp[i][j] > max){
                    max = dp[i][j];
                    index = i;
                }
            }
        }
        
        return A.substring(index - max + 1, index + 1);
}
複製代碼

 

9. 最長迴文子字符串

迴文字符串的子串也是迴文,好比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);
}
複製代碼

10. 0-1揹包問題(徹底揹包、多重揹包)

一個揹包有必定的承重cap,有N件物品,每件都有本身的價值,記錄在數組v中,也都有本身的重量,記錄在數組w中,每件物品只能選擇要裝入揹包仍是不裝入揹包,要求在不超過揹包承重的前提下,選出物品的總價值最大。

給定物品的重量w價值v及物品數n和承重cap。請返回最大總價值。

測試樣例:
[1,2,3],[1,2,3],3,6
返回:6

第一,包的容量比該商品體積小,裝不下,此時的價值與前i-1個的價值是同樣的,即V(i,j)=V(i-1,j);

第二,還有足夠的容量能夠裝該商品,但裝了也不必定達到當前最優價值,因此在裝與不裝之間選擇最優的一個,即V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }

其中V(i-1,j)表示不裝,V(i-1,j-w(i))+v(i) 表示裝了第i個商品,揹包容量減小w(i)但價值增長了v(i);

由此能夠得出遞推關係式:

1) j<w(i)      V(i,j)=V(i-1,j)

2) j>=w(i)     V(i,j)=max{ V(i-1,j)V(i-1,j-w(i))+v(i) 

填表,首先初始化邊界條件,V(0,j)=V(i,0)=0;

 

 而後一行一行的填表,示例:

 

複製代碼
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;
}
複製代碼

 這個其實能夠優化的,優化成:

http://www.javashuo.com/article/p-euienipf-nn.html

http://www.javashuo.com/article/p-ddpwpraw-s.html

 

01揹包問題空間壓縮版:

複製代碼
package com.darrenchan.dp;

import java.util.Arrays;

/**
 * 空間壓縮版01揹包問題
 * 
 * @author Think
 *
 */
public class Backpack01 {

    public static void main(String[] args) {
        System.out.println(maxValue(new int[] { 15, 10, 12, 8 }, new int[] { 12, 8, 9, 5 }, 30));
    }

    public static int maxValue(int[] w, int[] v, int cap) {
        int[] dp = new int[cap + 1];

        for (int i = 0; i < w.length; i++) {
            for (int j = cap; j >= w[i]; j--) {// 倒序遍歷
                dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
            }
        }

        int maxValue = dp[cap];// 獲取的最大價值
        System.out.println(Arrays.toString(dp));

        return maxValue;
    }
}
複製代碼

 

徹底揹包問題空間壓縮版:

複製代碼
package com.darrenchan.dp;

import java.util.Arrays;

/**
 * 空間壓縮版徹底揹包問題
 * 
 * @author Think
 *
 */
public class BackpackComplete {

    public static void main(String[] args) {
        System.out.println(maxValue(new int[] { 15, 10, 12, 8 }, new int[] { 12, 8, 9, 5 }, 30));
    }

    public static int maxValue(int[] w, int[] v, int cap) {
        int[] dp = new int[cap + 1];

        for (int i = 0; i < w.length; i++) {
            for (int j = w[i]; j <= cap; j++) {// 正序遍歷
                dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
            }
        }

        int maxValue = dp[cap];// 獲取的最大價值
        System.out.println(Arrays.toString(dp));

        return maxValue;
    }
}
複製代碼

 

多重揹包問題空間壓縮版:

複製代碼
package com.darrenchan.dp;

import java.util.Arrays;

/**
 * 空間壓縮版多重揹包問題
 * 
 * n是每個物品的個數
 * @author Think
 *
 */
public class BackpackMultiple {

    public static void main(String[] args) {
        System.out.println(maxValue(new int[] { 15, 10, 12, 8 }, new int[] { 12, 8, 9, 5 },new int[]{1,1,1,1}, 30));
    }

    public static int maxValue(int[] w, int[] v,int[] n, int cap) {
        int[] dp = new int[cap + 1];

        for (int i = 0; i < w.length; i++) {
            for (int k = 0; k <= n[i]; k++) {
                for (int j = cap; j >= k * w[i]; j--) {// 正序遍歷
                    dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]);
                }
            }
        }

        int maxValue = dp[cap];// 獲取的最大價值
        System.out.println(Arrays.toString(dp));

        return maxValue;
    }
}
複製代碼

11. 最長整除子序列

給出一個由無重複的正整數組成的集合, 找出其中最大的整除子集, 子集中任意一對 (Si, Sj) 都要知足: Si % Sj = 0 或 Sj % Si = 0。

若是有多個目標子集,返回其中任何一個都可。(LeetCode 368)類比最長遞增子序列。

示例 1:

集合: [1,2,3]

結果: [1,2] (固然, [1,3] 也正確)

 示例 2:

集合: [1,2,4,8]

結果: [1,2,4,8]
複製代碼
public List<Integer> largestDivisibleSubset(int[] nums) {
         // write your code here
        List<Integer> list = new ArrayList<Integer>();
        if(nums == null || nums.length == 0){
            return list;
        }
        Arrays.sort(nums);
        int[] dp = new int[nums.length];
        dp[0] = 1;

        for (int i = 1; i < nums.length; i++) {
            for (int j = i - 1; j >= 0; j--) {
                if (nums[i] % nums[j] == 0) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }

        int maxIndex = nums.length - 1;
        for (int i = nums.length - 1; i > 0; i--) {
            maxIndex = dp[i] > dp[maxIndex] ? i : maxIndex;
        }

        list.add(nums[maxIndex]);//最大的那個值
        for (int i = maxIndex - 1; i >= 0; i--) {
            if (nums[maxIndex] % nums[i] == 0 && dp[maxIndex] == dp[i] + 1) {
                list.add(nums[i]);
                maxIndex = i;
            }
        }

        return list;
    }
複製代碼

12. 尋找和爲定值的多個數

題目:輸入兩個整數n和sum,從數列1,2,3.......n 中隨意取幾個數,使其和等於sum,要求將其中全部的可能組合列出來。

思路:

咱們設置flag揹包,用來標註對應的n+1是否被選中,1表示被選中,0則表示未選中,每當知足m==n時,則輸出一組解。

複製代碼
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class SearchSomeSureValue {
    static int length;
    static void findCombination(int n,int m,int flagI[]){
        if (n<1||m<1) {
            return;
        }
        if (n>m) {
            n=m;
        }
        if (n==m) {
            flagI[n-1]=1;
            for (int i = 0; i < length; i++) {
                if (flagI[i]==1) {
                    System.out.print(i+1+" ");
                }
                
            }
            System.out.println();
            flagI[n-1]=0;
        }
        
        flagI[n-1]=1;
        findCombination(n-1, m-n, flagI);
        
        flagI[n-1]=0;        
        findCombination(n-1, m, flagI);
    }
    public static void main(String[] args) {
        int n,m;
        Scanner s=new Scanner(System.in);
        n=s.nextInt();
        m=s.nextInt();
        length=n;
        int[] flag=new int[n];
        findCombination(n, m, flag);
    }
}
複製代碼
相關文章
相關標籤/搜索