动态规划算法

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。在面试笔试中动态规划也是经常作为考题出现,其中较为简单的DP题目我们应该有百分之百的把握顺利解决才可以。

动态规划定义


动态规划实际上是一类题目的总称,并不是指某个固定的算法。动态规划的意义就是通过采用递推(或者分而治之)的策略,通过解决大问题的子问题从而解决整体的做法。动态规划的核心思想是巧妙的将问题拆分成多个子问题,通过计算子问题而得到整体问题的解。而子问题又可以拆分成更多的子问题,从而用类似递推迭代的方法解决要求的问题。

动态规划的解题核心


动态规划的解题核心主要分为两步:

  1. 第一步:状态的定义
  2. 第二步:状态转移方程的定义

在这里,我们为了避免混淆用“状态”这个词来替代“问题”这个词。“问题”表示的含义类似:题目要求解的内容题干中的疑问句这样的概念。状态表示我们在求解问题之中对问题的分析转化

第一步:状态的定义

有的问题过于抽象,或者过于啰嗦干扰我们解题的思路,我们要做的就是将题干中的问题进行转化(换一种说法,含义不变)。转化成一系列同类问题的某个解的情况,比如说:

题目:求一个数列中最大连续子序列的和。

我们要将这个原问题转化为:

状态定义:Fk是第k项前的最大序列和,求F1~FN中最大值。

通过换一种表述方式,我们清晰的发现了解决问题的思路,如何求出F1~FN中的最大值是解决原问题的关键部分。上述将原问题转化成另一种表述方式的过程叫做:状态的定义。这样的状态定义给出了一种类似通解的思路,把一个原来毫无头绪的问题转换成了可以求解的问题。

第二步:状态转移方程的定义

在进行了状态的定义后,自然而然的想到去求解F1~FN中最大值。这也是状态定义的作用,让我们把一个总体的问题转化成一系列问题,而第二步:状态转移方程的定义则告诉我们如何去求解一个问题,对于上述已经转换成一系列问题我们要关注的点就在于:如何能够用前一项或者前几项的信息得到下一项,这种从最优子状态转换为下一个最优状态的思路就是动态规划的核心。 
对于上面的例子题目来说,状态转移方程的定义应该是:

Fk=max{Fk-1+Ak,Ak
Fk是前k项的和,Ak是第k项的值

仔细思考一番,我们能够得到这样的结论,对于前k个项的最大子序列和是前k-1项的最大子序列和Fk与第k项的和、或者第k项两者中较大的。如果大家还是不能理解这个原理建议用演算纸自己计算一番,这里就不过多赘述了。这种状态转移的思路就是DP的核心。

动态规划的应用场景


看过了如何去使用动态规划,那么随之而来的问题就在于:既然动态规划不是某个固定的算法,而是一种策略思路,那么什么时候应该去使用什么时候不能用呢?答案很简短:

对于一个可拆分问题中存在可以由前若干项计算当前项的问题可以由动态规划计算。 
能用动态规划计算的问题存在一种递推的连带关系。

例题1: 数塔取数问题


一个高度为N的由正整数组成的三角形,从上走到下,求经过的数字和的最大值。 
每次只能走到下一层相邻的数上,例如从第3层的6向下走,只能走到第4层的2或9上。

该三角形第n层有n个数字,例如:

第一层有一个数字:5

第二层有两个数字:8 4

第三层有三个数字:3 6 9

第四层有四个数字:7 2 9 5

最优方案是:5 + 8 + 6 + 9 = 28

注意:上面应该是排列成一个三角形的样子不是竖向对应的,排版问题没有显示成三角形。

状态定义: Fi,j是第i行j列项最大取数和,求第n行Fn,m(0 < m < n)中最大值。

状态转移方程:Fi,j = max{Fi-1,j-1,Fi-1,j}+Ai,j

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        long max = 0;
        int[][] dp = new int[n][n];
        dp[0][0] = in.nextInt();
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= i; j++) {
                int num = in.nextInt();
                if (j == 0)
                    dp[i][j] = dp[i - 1][j] + num;
                else
                    dp[i][j] = Math.max(dp[i - 1][j - 1], dp[i - 1][j]) + num;
                max = Math.max(dp[i][j], max);
            }
        }
        System.out.println(max);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

例题2:编辑距离


编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。 
例如将kitten一字转成sitting: 
sitten (k->s) 
sittin (e->i) 
sitting (->g) 
所以kitten和sitting的编辑距离是3。俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。 
给出两个字符串a,b,求a和b的编辑距离。

状态定义:Fi,j表示第一个字符串的前i个字母和第二个字符串的前j个字母需要编辑的次数,求Fn,m,n和m分别是两个字符串的长度。

状态转移方程: 
当Fi,j-1=Fi-1,j时,Fi,j=Fi,j-1; 
当Fi,j-1!=Fi-1,j时,Fi,j=min{Fi-1,j-1,Fi,j-1,Fi-1,j}+1.

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {

        Scanner in = new Scanner(System.in);
        String aStr = in.nextLine();
        String bStr = in.nextLine();
        int aLen = aStr.length();
        int bLen = bStr.length();
        int[][] dp = new int[aLen + 1][bLen + 1];
        for (int i = 0; i < aLen + 1; i++) dp[i][0] = i;
        for (int i = 0; i < bLen + 1; i++) dp[0][i] = i;
        for (int i = 1; i < aLen + 1; i++)
            for (int j = 1; j < bLen + 1; j++)
                if (aStr.charAt(i - 1) == bStr.charAt(j - 1))
                    dp[i][j] = dp[i - 1][j - 1];
                else
                    dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
        System.out.println(dp[aLen][bLen]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

例题3:矩阵取数问题

一个N*N矩阵中有不同的正整数,经过这个格子,就能获得相应价值的奖励,从左上走到右下,只能向下向右走,求能够获得的最大价值。例如:3 * 3的方格。

1 3 3 
2 1 3 
2 2 1

能够获得的最大价值为:11。

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int[] dp = new int[n + 1];
        int[] readArr = new int[n + 1];
        for (int i = 0; i < n; i++) {
            for (int k = 1; k < n + 1; k++)
                readArr[k] = in.nextInt();
            for (int j = 1; j < n + 1; j++)
                dp[j] = Math.max(dp[j], dp[j - 1]) + readArr[j];
        }
        System.out.println(dp[n]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

例题4:背包问题

在N件物品取出若干件放在容量为W的背包里,每件物品的体积为W1,W2……Wn(Wi为整数),与之相对应的价值为P1,P2……Pn(Pi为整数)。求背包能够容纳的最大价值。

题解:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int v = in.nextInt();
        int[] dp = new int[v + 1];
        int[] price = new int[n + 1];
        int[] weight = new int[n + 1];
        long max = 0;
        for (int i = 1; i < n + 1; i++) {
            weight[i] = in.nextInt();
            price[i] = in.nextInt();
        }
        for (int i = 1; i < n + 1; i++)
            for (int j = v; j > 0; j--)
                if (j - weight[i] >= 0)
                    dp[j] = Math.max(dp[j], dp[j - weight[i]] + price[i]);
                else
                    dp[j] = dp[j];
        for (int i = 0; i < v + 1; i++)
            max = max > dp[i] ? max : dp[i];
        System.out.println(max);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

例题5:最长递增子序列

给出长度为N的数组,找出这个数组的最长递增子序列。 
(递增子序列是指,子序列的元素是递增的) 
例如:5 1 6 8 2 4 5 10,最长递增子序列是1 2 4 5 10。

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int[] L = new int[n];
        for (int j = 0; j < n; j++)
            L[j] = in.nextInt();

        double[] B = new double[n + 1];
        B[0] = Integer.MIN_VALUE;
        B[1] = L[0];
        int Len = 1;
        int p, r, m;
        for (int i = 1; i < n; i++) {
            p = 0;
            r = Len;
            while (p <= r) {
                m = (p + r) / 2;
                if (B[m] < L[i])
                    p = m + 1;
                else
                    r = m - 1;
            }
            B[p] = L[i];
            if (p > Len)
                Len++;
        }
        System.out.println(Len);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

例题6:最大子段和

N个整数组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的连续子段和的最大值。 
当所给的整数均为负数时和为0。 
例如:-2,11,-4,13,-5,-2,和最大的子段为:11,-4,13。和为20。

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        long max = 0, subMax = 0;
        for (int i = 0; i < n; i++) {
            subMax += in.nextInt();
            if (max < subMax)
                max = subMax;
            if (subMax < 0)
                subMax = 0;
        }
        System.out.println(max);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

例题7:最长公共子序列Lcs

给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的)。 
比如两个串为:

abcicba 
abdkscab

ab是两个串的子序列,abc也是,abca也是,其中abca是这两个字符串最长的子序列。

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String aStr = in.nextLine();
        String bStr = in.nextLine();
        int aLen = aStr.length();
        int bLen = bStr.length();
        int[][] dp = new int[aLen + 1][bLen + 1];
        for (int i = 1; i < aLen + 1; i++)
            for (int j = 1; j < bLen + 1; j++)
                if (dp[i - 1][j] == dp[i][j - 1] && aStr.charAt(i - 1) == bStr.charAt(j - 1)
                        && dp[i - 1][j] == dp[i - 1][j - 1])
                    dp[i][j] = dp[i - 1][j] + 1;
                else
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        int max = dp[aLen][bLen];
        StringBuilder sb = new StringBuilder();
        while (max > 0) {
            if (dp[aLen - 1][bLen] == dp[aLen][bLen - 1] && dp[aLen - 1][bLen] + 1 == dp[aLen][bLen]) {
                sb.append(aStr.charAt(aLen - 1));
                max--;
                aLen--;
                bLen--;
            } else {
                if (dp[aLen][bLen - 1] == dp[aLen][bLen])
                    bLen--;
                else
                    aLen--;
            }
        }

        System.out.println(sb.reverse().toString());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

例题8:正整数分组

将一堆正整数分为2组,要求2组的和相差最小。 
例如:1 2 3 4 5,将1 2 4分为1组,3 5分为1组,两组和相差1,是所有方案中相差最少的。

题解:

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {

        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        long sum = 0, max = 0;
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = in.nextInt();
            sum += nums[i];
        }
        int[] dp = new int[(int) (sum / 2 + 1)];
        for (int i = 0; i < n; i++)
            for (int j = (int) (sum / 2); j > 0; j--)
                if (j >= nums[i])
                    dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        for (int i = 1; i < sum / 2 + 1; i++)
            max = max > dp[i] ? max : dp[i];
        System.out.println(Math.abs((sum - max) - max));
    }
}

   9、找零钱问题

   有数组penny,penny中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。 给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。 测试样例: [1,2,4],3,3 返回:2

解析:设dp[n][m]为使用前n中货币凑成的m的种数,那么就会有两种情况:

             使用第n种货币:dp[n-1][m]+dp[n][m-peney[n]]

              不用第n种货币:dp[n-1][m],为什么不使用第n种货币呢,因为penney[n]>m。

        这样就可以求出当m>=penney[n]时 dp[n][m] = dp[n-1][m]+dp[n][m-peney[n]],否则,dp[n][m] = dp[n-1][m]

代码如下:

[java]  view plain  copy
  1. <span style="font-size:18px;">import java.util.*;  
  2.   
  3. public class Exchange {  
  4.     public int countWays(int[] penny, int n, int aim) {  
  5.         // write code here  
  6.         if(n==0||penny==null||aim<0){  
  7.          return 0;     
  8.         }  
  9.         int[][] pd = new int[n][aim+1];  
  10.         for(int i=0;i<n;i++){  
  11.          pd[i][0] = 1;     
  12.         }  
  13.         for(int i=1;penny[0]*i<=aim;i++){  
  14.          pd[0][penny[0]*i] = 1;     
  15.         }  
  16.         for(int i=1;i<n;i++){  
  17.             for(int j=0;j<=aim;j++){  
  18.                 if(j>=penny[i]){  
  19.                     pd[i][j] = pd[i-1][j]+pd[i][j-penny[i]];  
  20.                 }else{  
  21.                     pd[i][j] = pd[i-1][j];  
  22.                 }  
  23.             }  
  24.         }  
  25.         return pd[n-1][aim];  
  26.     }  
  27. }</span>  

10、走方格问题

  有一个矩阵map,它每个格子有一个权值。从左上角的格子开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。 给定一个矩阵map及它的行数n和列数m,请返回最小路径和。保证行列数均小于等于100. 测试样例: [[1,2,3],[1,1,1]],2,3 返回:4

解析:设dp[n][m]为走到n*m位置的路径长度,那么显而易见dp[n][m] = min(dp[n-1][m],dp[n][m-1]);

 

代码如下:

[java]  view plain  copy
  1. <span style="font-size:18px;">import java.util.*;  
  2.   
  3. public class MinimumPath {  
  4.     public int getMin(int[][] map, int n, int m) {  
  5.         // write code here  
  6.        int[][] dp = new int[n][m];  
  7.         for(int i=0;i<n;i++){  
  8.             for(int j=0;j<=i;j++){  
  9.              dp[i][0]+=map[j][0];      
  10.             }  
  11.         }  
  12.         for(int i=0;i<m;i++){  
  13.             for(int j=0;j<=i;j++){  
  14.              dp[0][i]+=map[0][j];      
  15.             }  
  16.         }  
  17.         for(int i=1;i<n;i++){  
  18.             for(int j=1;j<m;j++){  
  19.              dp[i][j] = min(dp[i][j-1]+map[i][j],dp[i-1][j]+map[i][j]);     
  20.             }  
  21.         }  
  22.         return dp[n-1][m-1];  
  23.     }  
  24.     public int min(int a,int b){  
  25.         if(a>b){  
  26.          return b;     
  27.         }else{  
  28.          return a;     
  29.         }  
  30.     }  
  31. }</span>  

11、走台阶问题

有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。为了防止溢出,请将结果Mod 1000000007 给定一个正整数int n,请返回一个数,代表上楼的方式数。保证n小于等于100000。 测试样例: 1 返回:1

解析:这是一个非常经典的为题,设f(n)为上n级台阶的方法,要上到n级台阶的最后一步有两种方式:从n-1级台阶走一步;从n-1级台阶走两步,于是就有了这个公式f(n) = f(n-1)+f(n-2);

代码如下:

[java]  view plain  copy
  1. <span style="font-size:18px;">import java.util.*;  
  2.   
  3. public class GoUpstairs {  
  4.     public int countWays(int n) {  
  5.         // write code here  
  6.         if(n<=2)  
  7.             return n;  
  8.         int f = 1%1000000007;  
  9.         int s = 2%1000000007;  
  10.         int t = 0;  
  11.         for(int i=3;i<=n;i++){  
  12.          t = (f+s)%1000000007;  
  13.          f = s;  
  14.          s = t;  
  15.         }  
  16.        return t;   
  17.     }  
  18. }</span>  

12、最长公共序列数

给定两个字符串A和B,返回两个字符串的最长公共子序列的长度。例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最长公共子序列。 给定两个字符串A和B,同时给定两个串的长度n和m,请返回最长公共子序列的长度。保证两串长度均小于等于300。 测试样例: "1A2C3D4B56",10,"B1D23CA45B6A",12 返回:6

解析:设dp[n][m] ,为A的前n个字符与B的前m个字符的公共序列长度,则当A[n]==B[m]的时候,dp[i][j] = max(dp[i-1][j-1]+1,dp[i-1][j],dp[i][j-1]),否则,dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);

代码如下:

[java]  view plain  copy
  1. <span style="font-size:18px;">import java.util.*;  
  2.   
  3. public class LCS {  
  4.     public int findLCS(String A, int n, String B, int m) {  
  5.         // write code here  
  6.         int[][] dp = new int[n][m];  
  7.         char[] a = A.toCharArray();  
  8.         char[] b = B.toCharArray();  
  9.        for(int i=0;i<n;i++){  
  10.            if(a[i]==b[0]){  
  11.                dp[i][0] = 1;  
  12.                for(int j=i+1;j<n;j++){  
  13.                    dp[j][0] = 1;  
  14.                }  
  15.                break;  
  16.            }  
  17.              
  18.        }  
  19.          for(int i=0;i<m;i++){  
  20.            if(a[0]==b[i]){  
  21.                dp[0][i] = 1;  
  22.                for(int j=i+1;j<m;j++){  
  23.                    dp[0][j] = 1;  
  24.                }  
  25.                break;  
  26.            }  
  27.              
  28.        }  
  29.        for(int i=1;i<n;i++){  
  30.            for(int j=1;j<m;j++){  
  31.                if(a[i]==b[j]){  
  32.                   dp[i][j] = max(dp[i-1][j-1]+1,dp[i-1][j],dp[i][j-1]);  
  33.                }else{  
  34.                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);  
  35.                }  
  36.                      
  37.            }  
  38.        }   
  39.           
  40.         return dp[n-1][m-1];  
  41.     }  
  42.     public int max(int a,int b,int c){  
  43.         int max = a;  
  44.         if(b>max)  
  45.             max=b;  
  46.         if(c>max)  
  47.             max = c;  
  48.         return max;  
  49.     }  
  50. }</span>  
转载 http://blog.csdn.net/p10010/article/details/50196211

http://blog.csdn.net/QuinnNorris/article/details/77484573

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值