Five common strategy of the algorithm - dynamic programming strategy (Dynamic Programming)

Dynamic Programming

  Dynamic Programming is one of five commonly used algorithm strategy, referred to as DP, translated Chinese is "dynamic programming" can be translated tall is this sounds bitter pit of countless people on, because after reading this you may feel algorithm and dynamic programming did not so much, they "dynamic" and "plan" are not too deep expression.
  To give a simple example to understand it first plain, have a rough prototype, to find the largest element in an array, if only one element, and that is it, and then further inside plus an array of elements, recursive relationship is that when you know the current largest element, just bring it compares the current maximum and the new elements added, the larger is the largest array, which is typical of the DP strategy, the solution will save up small problem, solve a big problem when you can Use directly.
  Just said, if you still feel a little confused, do not panic, following a few simple little chestnut let you know what it means.

0, Fibonacci discuss (Yi)

1, the number of path solving (Yi)

2, plays leapfrog discuss the step (Yi)

3, the longest common subsequence problem (too difficult)

4,0-1 knapsack problem (in general)

Fibonacci discuss

  1 is a first number, the second number is 1, starting from the third number, the number behind each equal to the number of front and two. Requirements: input a n, n-th output of Fibonacci numbers.
  The first example is our last section discusses the recursive divide and conquer strategy when the number of columns cited --Fibonacci problem, it is too classic, so to repeatedly come up with.
  If we analyze in depth section on said recursive method to solve the Fibonacci series, you will find there has been a lot of repetitive operations, such as your computing f (5) when you want to calculate f (4) and f (3), calculated f (4) is calculated again (3) and f (2), calculates f (3), and also calculates f (2) and f (1), see the following FIG.

Here Insert Picture Description

For f (3) and f (2) were repeated operations, this is because 5 is relatively small, if you want to calculate f (100), then you may have to wait forever it has not been performed (manual funny), and interested friends can try, anyway, I've tried.

public static int fibonacci(int n){//递归解法
    if(n == 1) return 1;
    else if(n == 2) return 1;
    else return fibonacci(n - 1) + fibonacci(n - 2);
}

The above is a recursive solution, the code looks very straightforward, but the complexity of the algorithm has reached O (2 ^ n), the complexity of the index level, plus if n is large stack will result in greater memory overhead, very inefficient.
Let us talk about DP strategy to solve this problem
we know that the root causes leading to inefficient algorithm is recursive big stack memory overhead, the more the smaller the number of times to repeat the calculation, that since we have a smaller number, such as f (2), f (3 ) ... these calculated, why double counting, then you use the DP strategy, the calculated f (n) saved. We look at the code:

/**
 * 对斐波那契数列求法的优化:如果单纯使用递归,那重复计算的次数就太多了,为此,我们对其做一些优化
 * 假设最多计算到第100个斐波那契数
 * 用arr这个数组来保存已经计算过的Fibonacci数,以确保不会重复计算某些数
 */
private static int arr[] = new int[100];
public static int Fibonacci(int n){
    if(n <= 2){
        return 1;
    }else{
        if(arr[n] != 0) //判断是否计算过这个f(n)
            return arr[n];
        else{
            arr[n] = Fibonacci(n-1)+Fibonacci(n-2);
            return arr[n];
        }
    }
}

arr array is initialized to 0, arr [I] represented on f (i), each first determine arr [I] is 0, if 0 indicates not calculated, the recursive computation, if not zero, indicates that has calculated that it would be returned directly.

This has the advantage of largely avoiding duplicate calculations, but the overhead of stack memory, although there is reduced but not very significant, because as long as there is recursive, it will inevitably have a large stack memory overhead. So to calculate the Fibonacci sequence against a recurrence of the way we have, in fact, this idea is also in line with DP strategy, saved all the calculated values.

//甚至可以使用递推来求解Fibonacci数列
public static int fibonacci(int n){
    if(n <= 2) return 1;
    int f1 = 1, f2 = 1, sum = 0;
    for(int i = 3; i <= n; i++){
        sum = f1+f2;
        f1 = f2;
        f2 = sum;
    }
    return sum;
}

Solving the number of paths

A robot can only step in the right or laid down, ask it tries to reach the lower right corner "Finish", how many different paths there are? (Mesh 7 * 3)

Here Insert Picture Description

DP strategies like the most important problem is to find the state transition equation is precisely the most difficult step.

Here Insert Picture Description

  • 1, we can be seen by this fact, to reach the (i, j) also two cases, one is from (i, j-1) take a right step to reach the other is from the (i-1, j) take the step to reach down, so the path of these two cases is that all the numbers together to the number of paths (i, j) of.

    Whereby the state transition equation are listed:

    f(i,j)=f(i-1,j)+f(i,j-1)

  • 2, according to the idea of ​​DP, is already calculated and stored for later reuse a result, here we consider a two-dimensional array to store the f (i, j).
  • 3, from small to large scale computing problem, the problem of large-scale de-multiplexing calculated using small scale of the problem.
    Code

/**
 * 此题是求解路径个数,让你从(1,1)走到某个特定的位置,求一共有多少种走法
 * @param i
 * @param j
 * @return
 */
public static int Count_Path(int i, int j){
    int result[][] = new int[i][j];
    for (int k = 0; k < i; k++) {           //将二维数组初始化为1
        Arrays.fill(result[k],1);
    }
    for (int k = 1; k < i; k++) {
        for (int l = 1; l < j; l++){
                result[k][l] = result[k-1][l]+result[k][l-1];
        }
    }
    return result[i-1][j-1];
}

Xiaoqing leapfrog discuss steps

  又是那只熟悉的青蛙,和上节递归与分治中相同的例题,一只青蛙一次可以跳上1级台阶,也可以跳上2级,求该青蛙跳上一个n级的台阶共有多少种跳法。详细思路可以看看上一篇文章——递归与分治策略。
我们下面先回顾一下上次用的递归算法:

public static int Jump_Floor1(int n){
    if(n <= 2){
        return n;
    }else{  //这里涉及到两种跳法,1、第一次跳1级就还有n-1级要跳,2、第一次跳2级就还有n-2级要跳
    return Jump_Floor1(n-1)+Jump_Floor1(n-2);
    }
}

其实和第一个例子斐波那契一样,之所以把它又拉出来讨论,是因为它的递归解法中涉及的重复计算实在太多了,我们需要将已经计算过的数据保存起来,以避免重复计算,提高效率。这里大家可以先自己试着改一下其实和第一个例子的改进方法是一样的,用一个数组来缓存计算过的数据。

/**
 * 看完递归的方法不要先被它的代码简洁所迷惑,可以分析一下复杂度,就会发现有很多重复的计算
 * 而且看完这个会发现和Fibonacci的递归方法有点像
 * @非递归
 */
private static int result[] = new int[100];
public static int Jump_Floor2(int n){
    if(n <= 2){
        return n;
    }else{
        if(result[n] != 0)
            return result[n];
        else{
            result[n] = Jump_Floor2(n-1)+Jump_Floor2(n-2);
            return result[n];
        }
    }
}

下面将难度做一提升,我们来讨论一道DP策略里的经典例题——最长公共子列问题

最长公共子序列问题

给定两个序列,需要求出两个序列最长的公共子序列,这里的子序列不同于字串,字串要求必须是连续的一个串,子序列并没有这么严格的连续要求,我们举个例子:

比如A = "LetMeDownSlowly!" B="LetYouDownQuickly!" A和B的最长公共子序列就是"LetDownly!"

比如字符串1:BDCABA;字符串2:ABCBDAB,则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA

我们设 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y),要找它们的最长公共子序列就是要求最优化问题,有以下几种情况:

  • 1、n = 0 || m = 0,不用多说最长的也只能是0,LCS(n,m) = 0
  • 2、X(n) = Y(m),说明当前序列也是相等的,那就给这两个元素匹配之前的最长长度加一,即LCS(n,m)=LCS(n-1,m-1)+1
  • 3、X(n) != Y(m),这时候说明这两个元素并没有匹配上,那所以最长的公共子序列长度还是这两个元素匹配之前的最长长度,即max{LCS(n-1,m),LCS(n,m-1)}
    由此我们可以列出状态转移方程:(用的别人的图)

Here Insert Picture Description

我们可以考虑用一个二维数组来保存LCS(n,m)的值,n表示行,m表示列,作如下演示,比如字符串1:ABCBDAB,字符串2:BDCABA;

1、先对其进行初始化操作,即将m=0,或者n=0的行和列的值全填为0

Here Insert Picture Description

2、判断发现A != B,则LCS(1,1) = 0,填入其中

Here Insert Picture Description

3、判断B == B,则LCS(1,2) = LCS(0,1)+1=1,填入其中

Here Insert Picture Description

4、判断B != C,则LCS(1,3)就应该等于LCS(0,3)和LCS(1,2)中较大的那一个,即等于1,通过观察我们发现现在的两个序列是{B}和{ABC}在比较,即使现在B != C,但是因为前面已经有一个B和其匹配了,所以长度最少已经为1了,所以当C未匹配时,子序列的最大值是前面的未比较C和B时候的最大值,所以填1

5、再比较到B和B,虽然两者相等,但是只能是LCS(n-1,m-1)+1,所以还是1,因为一个B只能匹配一次啊,举个例子:就好像是DB和ABCB来比较,当第一个序列的B和第二个序列的第二个B匹配时,就应该是D和ABC的最长子序列+1,所以如下填表:

Here Insert Picture Description

6、掌握规律后,我们直接完整填完这个表

Here Insert Picture Description

代码实现:

/**
 * 求最长公共子序列问题
 * Talk is cheap, show me the code!
 * 参考公式(也是最难的一步):
 *           { 0                             i = 0, j = 0
 * c[i][j] = { c[i-1][j-1]+1                 i,j>0, x[i] == y[i]
 *           { max{c[i-1][j],c[i][j-1]}      i,j>0, x[i] != y[i]
 * 参考书目:算法设计与分析(第四版)清华大学出版社    王晓东 编著
 * 参考博客:https://www.cnblogs.com/hapjin/p/5572483.html
 * 比如A = "LetMeDownSlowly!"   B="LetYouDownQuickly!"   A和B的最长公共子序列就是"LetDownly!"
 * @param x
 * @param y
 * @Param c 用c[i,j]表示:(x1,x2....xi) 和 (y1,y2...yj) 的**最长**公共子序列的长度
 * @return  最长公共子序列的长度
 */

//maybe private method
private static int lcsLength(String x, String y, int[][] c){
    int m = x.length();
    int n = y.length();
    //下面是初始化操作,其实也没必要,因为Java中默认初始化为0,其他语言随机应变
    for (int i = 0; i <= m; i++) c[i][0] = 0;
    for (int i = 0; i <= n; i++) c[0][i] = 0;

    //用一个序列的每个元素去和另一个序列分别比较
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if(x.charAt(i-1) == y.charAt(j-1)){     //如果遇到相等的,就给序列的上一行上一列的加1
                c[i][j] = c[i-1][j-1]+1;
            }else if(c[i-1][j] >= c[i][j-1]){       //取上一次最大的,保证最长子序列的最长要求
                c[i][j] = c[i-1][j];
            }else{
                c[i][j] = c[i][j-1];
            }
        }
    }
    return c[m][n];
}

0-1背包问题

也是很经典的一道算法题:0-1背包问题说的是,给定背包容量W,一系列物品{weiht,value},每个物品只能取一件,计算可以获得的value的最大值。
最优解问题,当然是我们DP,最难的一步还是状态转移方程,我们先把方程给出来,再对其进行讨论.

m[i][j] = max{ m[i-1][j-w[i]]+v[i] , m[i-1][j]}

m [i] [j] represents 1,2, ..., i th items, the maximum capacity of the backpack when the value j, W [i] denotes the i-th weight of the article, V [i] denotes the i-th item the value
we used a two-dimensional array to store the m, i represents 1,2, ..., i th items, j represents the capacity of the backpack
, for each item, the maximum value currently to be calculated, two situations : 1, this article into them, this article will not go into

  • 1, we do not consider it into them, well understood, m [i-1] [j] is not the i-th largest value items into the backpack, hold is 1,2, ..., i -1 article, knapsack capacity j
  • 2, consider the case of a bag, since you want it to go into, to which it previously left in the backpack good location, m [i-1] [ jw [i]] indicates the i-th backpack give articles the place vacated space is then available backpack jw [i], in which the space available 1,2, ..., i-1 th maximum value of the article is put m [i-1] [jw [i]] , which only need to put together to give v [i] to, i.e. m [i-1] [jw [i]] + v [i].
    Therefore, the state transition equation is taken into a bag and hold the two cases the maximum

    m[i][j] = max{ m[i-1][j-w[i]]+v[i] , m[i-1][j]}

Code

/**
 * 此函数用于计算背包中能存放的最大values
 * @param m     m[i][j]用于记录1,2,...,i个物品在背包容量为j时候的最大value
 * @param w     w数组存放了每个物品的重量weight,w[i]表示第i+1个物品的weight
 * @param v     v数组存放了每个物品的价值value,v[i]表示第i+1个物品的value
 * @param C     C表示背包最大容量
 * @param sum   sum表示物品个数
 * 状态转移方程: m[i][j] = max{ m[i-1][j-w[i]]+v[i] , m[i-1][j]}
 *            m[i-1][j]很好理解,就是不将第i个物品放入背包的最大value
 *            m[i-1][j-w[i]]+v[i]表示将第i个物品放入背包,m[i-1][j-w[i]]表示在背包中先给第i个物品把地方腾出来
 *            然后背包可用空间就是j-w[i],在这些可用空间里1,2,...,i-1个物品放的最大value就是m[i-1][j-w[i]],那
 *            最后再加上第i个物品的value,就是将第i个物品放入背包的最大value了
 */
public static void knap(int[][] m, int[] w,int[] v, int C, int sum){
    for(int j = 0; j < C; j++){     //初始化   stuttering
        if(j+1 >= w[0]){        //第一行只有一个物品,如果物品比背包容量大就放进去,否则最大value只能为0
            m[0][j] = v[0];
        }else{
            m[0][j] = 0;
        }
    }
    for(int i = 1; i < sum; i++){
        for(int j = 0; j < C; j++){
            int a = 0, b = 0;       //a表示将第i个物品放入背包的value,b表示不放第i个物品
            if(j >= w[i])
                a = m[i-1][j-w[i]]+v[i];
            b = m[i-1][j];
            m[i][j] = (a>b?a:b);
        }
    }
}

Guess you like

Origin www.cnblogs.com/vfdxvffd/p/12302575.html