[算法]死磕递归和动态规划题

Posted 陈驰字新宇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[算法]死磕递归和动态规划题相关的知识,希望对你有一定的参考价值。

最近在忙着找实习,因而做了大量的笔试算法题,阿里,网易,腾讯,华为,发现各大厂商都喜欢出递归和动态规划题,而且出的特别多,这种题以前一直没有搞懂,总是半懂状态,现在感觉有必要好好整理一下。

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"都是最长公共子序列。

给定两个字符串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. 最长回文子字符串

回文字符串的子串也是回文,比如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;
}

 

以上是关于[算法]死磕递归和动态规划题的主要内容,如果未能解决你的问题,请参考以下文章

告别动态规划,连刷40道动规算法题,我总结了动规的套路

java——递归(动态规划,回溯)

动态规划法解通配符匹配算法题

动态规划和分治法,贪心算法以及递归的再一次深刻理解和体会

算法题LeetCode-硬币划分问题-(动态规划斜率优化空间压缩)

算法题LeetCode-硬币划分问题-(动态规划斜率优化空间压缩)