动态规划面试题(最大字段和/三角数组最大和/LIS最长非降子序列/LCS最长(连续/非连续)公共子序列)

1.最大字段和

描述:给定n个整数(可能为负数)组成的序列 a[1],a[2],a[3]... ,a[n],求该序列如a[i]+a[i+1]+... +a[j]的子段和的最大值。                             当所给的整数均为负数时定义子段和为0,如果序列中全部是负数则最大子断和为0,依此定义,所求的最优值为Max{0,a[i]+a[i+ 1]      +....+a[1},1≤i≤j≤n。输入:-2,11,-4,13,-5,-2 ;输出:20

分析:状态dp[i]表示以第i个元素结尾的最大字段和

dp[0] : 0
dp[1] : 11 + dp[0] = 11
dp[2] : -4 + dp[1] = 7
dp[3] : 13 + dp[2] = 20
dp[4] : -5 + dp[3] = 15
dp[5] : -2 + dp[4] = 13
             dp[0] = 0
状态转移方程:dp[i] = ar[i] + dp[i-1]

代码: 

import java.util.Arrays;

public class sum {
    public static void main(String[] args) {
        int[] arr = new int[]{-2,11,-4,13,-5,-2};
        int[] dp = new int[arr.length];
        int sum = maxSegmentSum(arr,dp);
        System.out.println (Arrays.toString (dp));
        System.out.println (sum);
    }

    private static int maxSegmentSum(int[] arr, int[] dp) {
        dp[0] = arr[0];
        //负数的最大字段和为0
        if (dp[0] < 0){
            dp[0] = 0;
        }
        int max = dp[0];
        for (int i = 1; i < arr.length; i++) {
            dp[i] = arr[i] + dp[i-1];
            if(dp[i] < 0){
                dp[i] = 0;
            }
            //找出最大的字段和赋值给max
            if(max < dp[i]){
                max = dp[i];
            }
        }
        return max;
    }
}

2.三角数组求和的最大值

描述:

/**
 * 描述:
 * 有一个三角数组,形如:
 * 5
 * 12  6
 * 7   13  18
 * 12  14  10  9
 * 求从最上面元素开始,每一层选择一个元素,每一个元素可以向下选择,
 * 向左斜下方选择,向右斜下方选择,问最终选择出来的数字的最大值是多少?
 *
  */

 

思路:我们可以先将最后一层的元素进行遍历,存储到dp中,然后从倒数第二列开始向下选择,即每个数有三种相加可能(选择最后一层元素),将相加后的得到数中最大数存储到dp中,然后逐层向上遍历,就可求出每一层元素相加最大和

void func(int[] arr, int i, int j){
    arr[i][j] + func(arr[i+1][j]);//向下面加
    arr[i][j] + func(arr[i+1][j-1]);//向左斜下加
    arr[i][j] + func(arr[i+1][j+1]);//向右斜下加

}

代码: 


public class 三角数组求和的最大值 {
    public static void main(String[] args) {
        int[][] arr = new int[][]{
                {5},
                {12, 6},
                {7, 13, 18},
                {12, 14, 10, 9}
        };
        int[][] dp = new int[arr.length][arr.length];
        int row = arr.length - 1;
        //将最后一层的数赋值给dp,作为已知值
        for (int i = 0; i < arr[row].length; i++) {
            dp[row][i] = arr[row][i];
        }
        int sum = maxSum (arr,dp);
        System.out.println (sum);
//        //递归:
//        int max = func (arr,0,0,dp);
//        System.out.println (max);
    }

    /**
     * 非递归形式
     */
    private static int maxSum(int[][] arr, int[][] dp) {
        //从倒数第二行开始遍历(倒数第一列元素以遍历完赋值给dp)
        for (int i = arr.length - 1 - 1; i >= 0; i--) {
            for (int j = 0; j < arr[i].length; j++) {//从每行的第一个数开始遍历
                //向下面加
                int n1 = arr[i][j] + dp[i+1][j];
                //向右斜下加
                int n2 = arr[i][j] + dp[i+1][j+1];
                int n3 = 0;
                //向左斜下加,需做判断
                if(j > 0){
                    n3 = arr[i][j] + dp[i+1][j-1];
                }
                //将最大值赋值给dp
                dp[i][j] = Math.max (Math.max(n1,n2),n3);
            }
        }
        return dp[0][0];
    }

    /**
     * 递归
     * @param arr
     * @param i
     * @param j
     * @param dp
     * @return
     */
    private static int func(int[][] arr, int i, int j, int[][] dp) {
        //界限判断
        if(i > arr.length-1 || j < 0 || j > arr[i].length-1){
            return 0;
        }
        //防止子规模重复求解
        if(dp[i][j] > 0){
            return dp[i][j];
        }
        int n1 = arr[i][j] + func(arr, i+1, j, dp);
        int n2 = arr[i][j] + func(arr, i+1, j-1, dp);
        int n3 = arr[i][j] + func(arr, i+1, j+1, dp);
        dp[i][j] = Math.max(Math.max(n1, n2), n3);
        return dp[i][j];
    }
}

3.求LIS最长非降子序列问题

 描述:给定一串字符串,求其中最长的字符序列从小到大排序的序列长度

/**
 * 其状态dp[i]表示以第i个元素结尾的最长非降子序列的长度值,
 * 状态转移方程是:dp(i) = max{1, dp(j)+1} 
 * 其中j dp[i]){
                        dp[i] = dp[j] + 1;
                    }
                }
                if(max < dp[i]){
                    max = dp[i];
                }
            }
            return max;
        }
    }

4.求LCS最长(连续/不一定连续)公共子序列问题

   求两个序列的最长公共子序列(连续)的长度

/**
 * 非递归
 * 状态:dp[m][n]表示两个序列的最大公共字段和的值
 * 动态规划方程为:
 * dp[m][n]=1 + dp[m-1][n-1]   (str1[i]=str2[j])
 */
public class test {
    public static void main(String[] args) {
        String str1 = "helloworld";
        String str2 = "helxtld";
        int[][] dp = new int[str1.length () + 1][str2.length () + 1];
        int sum = func (str1, str2, dp);
        System.out.println (sum);
    }

    private static int func(String str1, String str2, int[][] dp) {
        int k = 0;
        int max = 0;
        for (int m = 1; m <= str1.length (); ++m) {
            for (int n = 1; n <= str2.length (); ++n) {
                if (str1.charAt (m - 1) == str2.charAt (n - 1)) {
                    dp[m][n] = 1 + dp[m - 1][n - 1]; // 对角线
                }
                if (max < dp[m][n]) {
                    max = dp[m][n];
                    //公共字段在两个字符串中都有,将k赋成任意一个字符串公共长度就行
                    k = m;
                }
            }
        }
        //substring()方法用于提取字符串中介于两个指定下标之间的字符
        System.out.println (str1.substring ((k-max),k));
        return max;
    }
}

运行结果:

 我们能得到公共字符串长度,和公共字符串

hel
3

求两个序列的最长公共子序列(不一定连续)的长度

public class test {
    public static void main(String[] args) {
        String str1 = "helloworld";
        String str2 = "helxtld";
        int[][] dp = new int[str1.length ()+1][str2.length ()+1];
//        int lcs = func (str1, str2,str1.length ()-1,str2.length ()-1, dp);
        int lcs1 = func (str1,str2,dp);
//        System.out.println (lcs);
        System.out.println (lcs1);
//        for (int i = 0; i < dp.length; i++) {
//            for (int j = 0; j < dp[i].length; j++) {
//                System.out.print (dp[i][j] + " ");
//            }
//            System.out.println ();
//        }
    }
    }

    /**
     * 非递归实现LCS
     * @param str1
     * @param str2
     * @param dp
     * @return
     */
    private static int func(String str1, String str2, int[][] dp) {
        /**
         * dp[m][n] : str1以m位元素结尾,str2以n位元素结尾
         * 的两个字符串的LCS的长度
         *
         * if(xm == yn)
         *    dp[m][n] = 1 + dp[m-1][n-1]
         * else {
         *    dp[m][n] = max{dp[m][n-1], dp[m-1][n]}
         * }
         */
        for(int m=1; m<=str1.length(); m++){
            for(int n=1; n<=str2.length(); n++){
                if(str1.charAt(m-1) == str2.charAt(n-1)){
                    dp[m][n] = 1 + dp[m-1][n-1]; // 对角线
                } else {
                    if(dp[m][n-1] > dp[m-1][n]){
                        dp[m][n] = dp[m][n-1]; // 左边
                    } else {
                        dp[m][n] = dp[m-1][n]; // 上面
                    }
                }
            }
        }
        return dp[str1.length()][str2.length()];
    }

    /**
     * 递归实现
     * @param str1
     * @param str2
     * @param m
     * @param n
     * @param dp
     * @return
     */
    private static int func(String str1, String str2, int m, int n, int[][] dp) {
        if(m < 0 || n < 0){
            return 0;
        }

        if(dp[m][n] > 0){
            return dp[m][n];
        }

        if(str1.charAt(m) == str2.charAt(n)){
            dp[m][n] = 1 + func(str1, str2, m-1, n-1, dp);
            return dp[m][n];
        } else {
            int n1 = func(str1, str2, m, n-1, dp);
            int n2 = func(str1, str2, m-1, n, dp);
            if(n1 > n2){
                dp[m][n] = n1;
            } else {
                dp[m][n] = n2;
            }
            return dp[m][n];
        }
    }
}

上面的代码只是打印出最长公共子序列的长度,如果想要打印最长公共子序列的元素,则要记录dp表的走向,然后用回溯法进行元素打印,代码如下: 

 public static void main(String[] args) {
        String str1 = "helloworld";
        String str2 = "helxtld";
        int[][] dp = new int[str1.length () + 1][str2.length () + 1];
        // 辅助数组,记录LCS元素的走向
        int[][] path = new int[str1.length () + 1][str2.length () + 1];
        int lcs = func (str1, str2, dp, path);
        System.out.println ("不一定连续长度:" + lcs);

        //打印bp表,记录bp表走向
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < dp[i].length; j++) {
                System.out.print (dp[i][j] + " ");
            }
            System.out.println ();
        }
        backstrace (str1, str1.length (), str2.length (), path);
    }

    //打印最长公共子序列元素,需要用递归实现,在回溯回来时打印才能保证正序输出
    private static void backstrace(String str1, int m, int n, int[][] path) {
        if (m < 0 || n < 0) {
            return;
        }
        if (path[m][n] == 1) {
            backstrace (str1, m - 1, n - 1, path);
            System.out.print (str1.charAt (m - 1));
        } else {
            if (path[m][n] == 2) {
                backstrace (str1, m, n - 1, path);
            } else {
                backstrace (str1, m - 1, n, path);
            }
        }
    }

    private static int func(String str1, String str2, int[][] dp, int[][] path) {
        for (int m = 1; m <= str1.length (); ++m) {
            for (int n = 1; n <= str2.length (); ++n) {
                if (str1.charAt (m - 1) == str2.charAt (n - 1)) {
                    dp[m][n] = 1 + dp[m - 1][n - 1]; // 对角线
                    path[m][n] = 1;
                } else {
                    if (dp[m][n - 1] > dp[m - 1][n]) {
                        dp[m][n] = dp[m][n - 1]; // 左边
                        path[m][n] = 2;
                    } else {
                        dp[m][n] = dp[m - 1][n]; // 上面
                        path[m][n] = 3;
                    }
                }
            }
        }
        return dp[str1.length ()][str2.length ()];
    }
}

运行结果:

不一定连续长度:5
0 0 0 0 0 0 0 0 
0 1 1 1 1 1 1 1 
0 1 2 2 2 2 2 2 
0 1 2 3 3 3 3 3 
0 1 2 3 3 3 4 4 
0 1 2 3 3 3 4 4 
0 1 2 3 3 3 4 4 
0 1 2 3 3 3 4 4 
0 1 2 3 3 3 4 4 
0 1 2 3 3 3 4 4 
0 1 2 3 3 3 4 5 
helld

 打印出的bp表,会将对角线上递增的数记录下来,即公共子序列的长度

 

 

你可能感兴趣的:(算法)