Java動態規劃

1. 介紹

動態規劃典型的被用於優化遞歸算法,由於它們傾向於以指數的方式進行擴展。動態規劃主要思想是將複雜問題(帶有許多遞歸調用)分解爲更小的子問題,而後將它們保存到內存中,這樣咱們就沒必要在每次使用它們時從新計算它們。java

要理解動態規劃的概念,咱們須要熟悉一些主題:python

  1. 什麼是動態規劃?
  2. 貪心算法
  3. 簡化的揹包問題
  4. 傳統的揹包問題
  5. Levenshtein Distance
  6. LCS-最長的共同子序列
  7. 利用動態規劃的其餘問題
  8. 結論

本文全部代碼均爲java代碼實現。算法

2. 什麼是動態規劃?

動態規劃是一種編程原理,能夠經過將很是複雜的問題劃分爲更小的子問題來解決。這個原則與遞歸很相似,可是與遞歸有一個關鍵點的不一樣,就是每一個不一樣的子問題只能被解決一次。編程

爲了理解動態規劃,咱們首先須要理解遞歸關係的問題。每一個單獨的複雜問題能夠被劃分爲很小的子問題,這表示咱們能夠在這些問題之間構造一個遞歸關係。 讓咱們來看一個咱們所熟悉的例子:斐波拉契數列,斐波拉契數列的定義具備如下的遞歸關係: 數組

在這裏插入圖片描述
注意:遞歸關係是遞歸地定義下一項是先前項的函數的序列的等式。 Fibonacci序列就是一個很好的例子。

因此,若是咱們想要找到斐波拉契數列序列中的第n個數,咱們必須知道序列中第n個前面的兩個數字。bash

可是,每次咱們想要計算Fibonacci序列的不一樣元素時,咱們在遞歸調用中都有一些重複調用,以下圖所示,咱們計算Fibonacci(5)app

在這裏插入圖片描述

例如:若是咱們想計算F(5),明顯的咱們須要計算F(3)F(4)做爲計算F(5)的先決條件。然而,爲了計算F(4),咱們須要計算F(3)F(2),所以咱們又須要計算F(2)F(1)來獲得F(3),其餘的求解諸如此類。less

這樣的話就會致使不少重複的計算,這些重複計算本質上是冗餘的,而且明顯的減慢了算法的效率。爲了解決這種問題,咱們介紹動態規劃。函數

在這種方法中,咱們對解決方案進行建模,就像咱們要遞歸地解決它同樣,但咱們從頭開始解決它,記憶到達頂部採起的子問題(子步驟)的解決方案。 所以,對於Fibonacci序列,咱們首先求解並記憶F(1)F(2),而後使用兩個記憶步驟計算F(3),依此類推。這意味着序列中每一個單獨元素的計算都是O(1),由於咱們已經知道前兩個元素。工具

當使用動態規劃解決問題的時候,咱們通常會採用下面三個步驟:

  1. 肯定適用於所述問題的遞歸關係
  2. 初始化內存、數組、矩陣的初始值
  3. 確保當咱們進行遞歸調用(能夠訪問子問題的答案)的時候它老是被提早解決。

遵循這些規則,讓咱們來看一下使用動態規劃的算法的例子:

3. 貪心算法

下面來以這個爲例子:

Given a rod of length n and an array that contains prices of all pieces of size smaller than n. Determine the maximum value obtainable by cutting up the rod and selling the pieces.
複製代碼

3.1. 對於沒有經驗的開發者可能會採起下面這種作法

這個問題其實是爲動態規劃量身定作的,可是由於這是咱們的第一個真實例子,讓咱們看看運行這些代碼會遇到多少問題:

public class naiveSolution {  
    static int getValue(int[] values, int length) {
        if (length <= 0)
            return 0;
        int tmpMax = -1;
        for (int i = 0; i < length; i++) {
            tmpMax = Math.max(tmpMax, values[i] + getValue(values, length - i - 1));
        }
        return tmpMax;
    }

    public static void main(String[] args) {
        int[] values = new int[]{3, 7, 1, 3, 9};
        int rodLength = values.length;

        System.out.println("Max rod value: " + getValue(values, rodLength));
    }
}
複製代碼

輸出結果:

Max rod value: 17

該解決方案雖然正確,但效率很是低,遞歸調用的結果沒有保存,因此每次有重疊解決方案時,糟糕的代碼不得不去解決相同的子問題。

3.2.動態方法

利用上面相同的基本原理,添加記憶化並排除遞歸調用,咱們獲得如下實現:

public class dpSolution {  
    static int getValue(int[] values, int rodLength) {
        int[] subSolutions = new int[rodLength + 1];

        for (int i = 1; i <= rodLength; i++) {
            int tmpMax = -1;
            for (int j = 0; j < i; j++)
                tmpMax = Math.max(tmpMax, values[j] + subSolutions[i - j - 1]);
            subSolutions[i] = tmpMax;
        }
        return subSolutions[rodLength];
    }

    public static void main(String[] args) {
        int[] values = new int[]{3, 7, 1, 3, 9};
        int rodLength = values.length;

        System.out.println("Max rod value: " + getValue(values, rodLength));
    }
}
複製代碼

輸出結果:

Max rod value: 17

正如咱們所看到的的,輸出結果是同樣的,所不一樣的是時間和空間複雜度。

經過從頭開始解決子問題,咱們消除了遞歸調用的須要,利用已解決給定問題的全部先前子問題的事實。

性能的提高

爲了給出動態方法效率更高的觀點的證據,讓咱們嘗試使用30個值來運行該算法。 一種算法須要大約5.2秒來執行,而動態解決方法須要大約0.000095秒來執行。

4. 簡化的揹包問題

簡化的揹包問題是一個優化問題,沒有一個解決方案。這個問題的問題是 - 「解決方案是否存在?」:

Given a set of items, each with a weight w1, w2... determine the number of each item to put in a knapsack so that the total weight is less than or equal to a given limit K.

給定一組物品,每一個物品的重量爲w1,w2 ......肯定放入揹包中的每一個物品的數量,以使總重量小於或等於給定的極限K

首先讓咱們把元素的全部權重存儲在W數組中。接下來,假設有n個項目,咱們將使用從1到n的數字枚舉它們,所以第i個項目的權重爲W [i]。咱們將造成(n + 1)x(K + 1)維的矩陣MM [x] [y]對應於揹包問題的解決方案,但僅包括起始數組的前x個項,而且最大容量爲y

例如

假設咱們有3個元素,權重分別是w1=2kg,w2=3kg,w3=4kg。利用上面的方法,咱們能夠說M [1] [2]是一個有效的解決方案。 這意味着咱們正在嘗試用重量陣列中的第一個項目(w1)填充容量爲2kg的揹包。

M [3] [5]中,咱們嘗試使用重量陣列的前3項(w1,w2,w3)填充容量爲5kg的揹包。 這不是一個有效的解決方案,由於咱們過分擬合它。

4.1. 矩陣初始化

當初始化矩陣的時候有兩點須要注意:

Does a solution exist for the given subproblem (M[x][y].exists) AND does the given solution include the latest item added to the array (M[x][y].includes).

給定子問題是否存在解(M [x] [y] .exists)而且給定解包括添加到數組的最新項(M [x] [y] .includes)。

所以,初始化矩陣是至關容易的,M[0][k].exists老是false,若是k>0,由於咱們沒有把任何物品放在帶有k容量的揹包裏。

另外一方面,M[0][0].exists = true,當k=0的時候,揹包應該是空的,所以咱們在裏面沒有聽任何東西,這個是一個有效的解決方案。

此外,咱們能夠說M[k][0].exists = true,可是對於每一個k來講 M[k][0].includes = false

注意:僅僅由於對於給定的M [x] [y]存在解決方案,它並不必定意味着該特定組合是解決方案。 在M [10] [0]的狀況下,存在一種解決方案 - 不包括10個元素中的任何一個。 這就是M [10] [0] .exists = trueM [10] [0] .includes = false的緣由。

4.2.算法原則

接下來,讓咱們使用如下僞代碼構造M [i] [k]的遞歸關係:

if (M[i-1][k].exists == True):  
    M[i][k].exists = True
    M[i][k].includes = False
elif (k-W[i]>=0):  
    if(M[i-1][k-W[i]].exists == true):
        M[i][k].exists = True
        M[i][k].includes = True
else:  
    M[i][k].exists = False
複製代碼

所以,解決方案的要點是將子問題分爲兩種狀況:

  1. 對於容量k,當存在第一個i-1元素的解決方案
  2. 對於容量k-W [i],當第一個i-1元素存在解決方案

第一種狀況是不言自明的,咱們已經有了問題的解決方案。

第二種狀況是指了解第一個i-1元素的解決方案,可是容量只有一個第i個元素不滿,這意味着咱們能夠添加一個第i個元素,而且咱們有一個新的解決方案!

4.3. 實現

下面這何種實現方式,使得事情變得更加容易,咱們建立了一個類Element來存儲元素:

public class Element {  
    private boolean exists;
    private boolean includes;

    public Element(boolean exists, boolean includes) {
        this.exists = exists;
        this.includes = includes;
    }

    public Element(boolean exists) {
        this.exists = exists;
        this.includes = false;
    }

    public boolean isExists() {
        return exists;
    }

    public void setExists(boolean exists) {
        this.exists = exists;
    }

    public boolean isIncludes() {
        return includes;
    }

    public void setIncludes(boolean includes) {
        this.includes = includes;
    }
}
複製代碼

接着,咱們能夠深刻了解主要的類:

public class Knapsack {  
    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);

        System.out.println("Insert knapsack capacity:");
        int k = scanner.nextInt();

        System.out.println("Insert number of items:");
        int n = scanner.nextInt();

        System.out.println("Insert weights: ");
        int[] weights = new int[n + 1];

        for (int i = 1; i <= n; i++) {
            weights[i] = scanner.nextInt();
        }

        Element[][] elementMatrix = new Element[n + 1][k + 1];

        elementMatrix[0][0] = new Element(true);

        for (int i = 1; i <= k; i++) {
            elementMatrix[0][i] = new Element(false);
        }

        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= k; j++) {
                elementMatrix[i][j] = new Element(false);
                if (elementMatrix[i - 1][j].isExists()) {
                    elementMatrix[i][j].setExists(true);
                    elementMatrix[i][j].setIncludes(false);
                } else if (j >= weights[i]) {
                    if (elementMatrix[i - 1][j - weights[i]].isExists()) {
                        elementMatrix[i][j].setExists(true);
                        elementMatrix[i][j].setIncludes(true);
                    }
                }
            }
        }

        System.out.println(elementMatrix[n][k].isExists());
    }
}
複製代碼

惟一剩下的就是解決方案的重建,在上面的類中,咱們知道解決方案是存在的,可是咱們不知道它是什麼。

爲了重建,咱們使用下面的代碼:

List<Integer> solution = new ArrayList<>(n);

if (elementMatrix[n][k].isExists()) {  
    int i = n;
    int j = k;
    while (j > 0 && i > 0) {
        if (elementMatrix[i][j].isIncludes()) {
            solution.add(i);
            j = j - weights[i];
        }
        i = i - 1;
    }
}

System.out.println("The elements with the following indexes are in the solution:\n" + (solution.toString()));  
複製代碼

輸出:

Insert knapsack capacity:  
12  
Insert number of items:  
5  
Insert weights:  
9 7 4 10 3  
true  
The elements with the following indexes are in the solution:  
[5, 1]
複製代碼

揹包問題的一個簡單變化是在沒有價值優化的狀況下填充揹包,但如今每一個單獨項目的數量無限。

經過對現有代碼進行簡單調整,能夠解決這種變化:

// Old code for simplified knapsack problem
else if (j >= weights[i]) {  
    if (elementMatrix[i - 1][j - weights[i]].isExists()) {
        elementMatrix[i][j].setExists(true);
        elementMatrix[i][j].setIncludes(true);
    }
}

// New code, note that we're searching for a solution in the same // row (i-th row), which means we're looking for a solution that
// already has some number of i-th elements (including 0) in it's solution else if (j >= weights[i]) { if (elementMatrix[i][j - weights[i]].isExists()) { elementMatrix[i][j].setExists(true); elementMatrix[i][j].setIncludes(true); } } 複製代碼

5. 傳統的揹包問題

利用之前的兩種變體,如今讓咱們來看看傳統的揹包問題,看看它與簡化版本的不一樣之處:

Given a set of items, each with a weight w1, w2... and a value v1, v2... determine the number of each item to include in a collection so that the total weight is less than or equal to a given limit k and the total value is as large as possible.
複製代碼

在簡化版中,每一個解決方案都一樣出色。可是,如今咱們有一個找到最佳解決方案的標準(也就是可能的最大值)。請記住,此次咱們每一個項目都有無限數量,所以項目能夠在解決方案中屢次出現。

在實現中,咱們將使用舊的類Element,其中添加了私有字段value,用於存儲給定子問題的最大可能值:

public class Element {  
    private boolean exists;
    private boolean includes;
    private int value;
    // appropriate constructors, getters and setters
}
複製代碼

實現很是類似,惟一的區別是如今咱們必須根據結果值選擇最佳解決方案:

public static void main(String[] args) {  
    // Same code as before with the addition of the values[] array
    System.out.println("Insert values: ");
    int[] values = new int[n + 1];

    for (int i=1; i <= n; i++) {
        values[i] = scanner.nextInt();
    }

    Element[][] elementMatrix = new Element[n + 1][k + 1];

    // A matrix that indicates how many newest objects are used
    // in the optimal solution.
    // Example: contains[5][10] indicates how many objects with
    // the weight of W[5] are contained in the optimal solution
    // for a knapsack of capacity K=10
    int[][] contains = new int[n + 1][k + 1];

    elementMatrix[0][0] = new Element(0);

    for (int i = 1; i <= n; i++) {
        elementMatrix[i][0] = new Element(0);
        contains[i][0] = 0;
    }

    for (int i = 1; i <= k; i++) {
        elementMatrix[0][i] = new Element(0);
        contains[0][i] = 0;
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= k; j++) {
            elementMatrix[i][j] = new Element(elementMatrix[i - 1][j].getValue());
            contains[i][j] = 0;

            elementMatrix[i][j].setIncludes(false);
            elementMatrix[i][j].setValue(M[i - 1][j].getValue());

            if (j >= weights[i]) {
                if ((elementMatrix[i][j - weights[i]].getValue() > 0 || j == weights[i])) {
                    if (elementMatrix[i][j - weights[i]].getValue() + values[i] > M[i][j].getValue()) {
                        elementMatrix[i][j].setIncludes(true);
                        elementMatrix[i][j].setValue(M[i][j - weights[i]].getValue() + values[i]);
                        contains[i][j] = contains[i][j - weights[i]] + 1;
                    }
                }
            }

            System.out.print(elementMatrix[i][j].getValue() + "/" + contains[i][j] + " ");
        }

        System.out.println();
    }

    System.out.println("Value: " + elementMatrix[n][k].getValue());
}
複製代碼

輸出:

Insert knapsack capacity:  
12  
Insert number of items:  
5  
Insert weights:  
9 7 4 10 3  
Insert values:  
1 2 3 4 5  
0/0  0/0  0/0  0/0  0/0  0/0  0/0  0/0  0/0  1/1  0/0  0/0  0/0  
0/0  0/0  0/0  0/0  0/0  0/0  0/0  2/1  0/0  1/0  0/0  0/0  0/0  
0/0  0/0  0/0  0/0  3/1  0/0  0/0  2/0  6/2  1/0  0/0  5/1  9/3  
0/0  0/0  0/0  0/0  3/0  0/0  0/0  2/0  6/0  1/0  4/1  5/0  9/0  
0/0  0/0  0/0  5/1  3/0  0/0  10/2  8/1  6/0  15/3  13/2  11/1  20/4  
Value: 20  
複製代碼

6. Levenshtein Distance

另外一個使用動態規劃的很是好的例子是Edit DistanceLevenshtein Distance

Levenshtein Distance就是兩個字符串A,B,咱們須要使用原子操做將A轉換爲B

  1. 字符串刪除
  2. 字符串插入
  3. 字符替換(從技術上講,它不止一個操做,但爲了簡單起見,咱們稱之爲原子操做)

這個問題是經過有條理地解決起始字符串的子串的問題來處理的,逐漸增長子字符串的大小,直到它們等於起始字符串。

咱們用於此問題的遞歸關係以下:

在這裏插入圖片描述
若是 a == bc(a,b)爲0,若是 a = = bc(a,b)爲1。

實現:

public class editDistance {  
    public static void main(String[] args) {
        String s1, s2;
        Scanner scanner = new Scanner(System.in);
        System.out.println("Insert first string:");
        s1 = scanner.next();
        System.out.println("Insert second string:");
        s2 = scanner.next();

        int n, m;
        n = s1.length();
        m = s2.length();

        // Matrix of substring edit distances
        // example: distance[a][b] is the edit distance
        // of the first a letters of s1 and b letters of s2
        int[][] distance = new int[n + 1][m + 1];

        // Matrix initialization:
        // If we want to turn any string into an empty string
        // the fastest way no doubt is to just delete
        // every letter individually.
        // The same principle applies if we have to turn an empty string
        // into a non empty string, we just add appropriate letters
        // until the strings are equal.
        for (int i = 0; i <= n; i++) {
            distance[i][0] = i;
        }
        for (int j = 0; j <= n; j++) {
            distance[0][j] = j;
        }

        // Variables for storing potential values of current edit distance
        int e1, e2, e3, min;

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                e1 = distance[i - 1][j] + 1;
                e2 = distance[i][j - 1] + 1;
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    e3 = distance[i - 1][j - 1];
                } else {
                    e3 = distance[i - 1][j - 1] + 1;
                }
                min = Math.min(e1, e2);
                min = Math.min(min, e3);
                distance[i][j] = min;
            }

        }

        System.out.println("Edit distance of s1 and s2 is: " + distance[n][m]);
    }
}
複製代碼

輸出:

Insert first string:  
man  
Insert second string:  
machine  
Edit distance of s1 and s2 is: 3  
複製代碼

若是你想了解更多關於Levenshtein Distance的解決方案,咱們在另外的一篇文章中用python實現了 Levenshtein Distance and Text Similarity in Python, 使用這個邏輯,咱們能夠將許多字符串比較算法歸結爲簡單的遞歸關係,它使用Levenshtein Distance的基本公式

7. 最長共同子序列(LCS)

這個問題描述以下:

Given two sequences, find the length of the longest subsequence present in both of them. A subsequence is a sequence that appears in the same relative order, but not necessarily contiguous.
複製代碼

給定兩個序列,找到兩個序列中存在的最長子序列的長度。子序列是以相同的相對順序出現的序列,但不必定是連續的.

闡明:

若是咱們有兩個字符串s1="MICE"s2="MINCE",最長的共同子序列是MI或者CE。可是,最長的公共子序列將是「MICE」,由於結果子序列的元素沒必要是連續的順序。

遞歸關係與通常邏輯:

在這裏插入圖片描述
咱們能夠看到, Levenshtein distanceLCS之間只有微小的差異,特別是移動成本。

LCS中,咱們沒有字符插入和字符刪除的成本,這意味着咱們只計算字符替換(對角線移動)的成本,若是兩個當前字符串字符a [i]b [j] 是相同的,則成本爲1。

LCS的最終成本是2個字符串的最長子序列的長度,這正是咱們所須要的。

Using this logic, we can boil down a lot of string comparison algorithms to simple recurrence relations which utilize the base formula of the Levenshtein distance

使用這個邏輯,咱們能夠將許多字符串比較算法歸結爲簡單的遞歸關係,它使用Levenshtein distance的基本公式。

實現:

public class LCS {  
    public static void main(String[] args) {
        String s1 = new String("Hillfinger");
        String s2 = new String("Hilfiger");
        int n = s1.length();
        int m = s2.length();
        int[][] solutionMatrix = new int[n+1][m+1];
        for (int i = 0; i < n; i++) {
            solutionMatrix[i][0] = 0;
        }
        for (int i = 0; i < m; i++) {
            solutionMatrix[0][i] = 0;
        }

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                int max1, max2, max3;
                max1 = solutionMatrix[i - 1][j];
                max2 = solutionMatrix[i][j - 1];
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    max3 = solutionMatrix[i - 1][j - 1] + 1;
                } else {
                    max3 = solutionMatrix[i - 1][j - 1];
                }
                int tmp = Math.max(max1, max2);
                solutionMatrix[i][j] = Math.max(tmp, max3);
            }
        }

        System.out.println("Length of longest continuous subsequence: " + solutionMatrix[n][m]);
    }
}
複製代碼

輸出:

Length of longest continuous subsequence: 8  
複製代碼

8.利用動態規劃的其餘問題

利用動態規劃能夠解決不少問題,下面列舉了一些:

  1. 分區問題:給定一組整數,找出它是否能夠分紅兩個具備相等和的子集
  2. 子集和問題:給你一個正整數的數組及元素還有一個合計值,是否在數組中存在一個子集的的元素之和等於合計值。
  3. 硬幣變化問題:鑑於給定面額的硬幣無限供應,找到得到所需變化的不一樣方式的總數
  4. k變量線性方程的全部可能的解:給定k個變量的線性方程,計算它的可能解的總數
  5. 找到醉漢不會從懸崖上掉下來的機率:給定一個線性空間表明距離懸崖的距離,讓你知道酒鬼從懸崖起始的距離,以及他向懸崖p前進並遠離懸崖1-p的傾向,計算出他的生存機率

9.結論

動態編程是一種工具,能夠節省大量的計算時間,以換取更大的空間複雜性,這在很大程度上取決於您正在處理的系統類型,若是CPU時間很寶貴,您選擇耗費內存的解決方案,另外一方面,若是您的內存有限,則選擇更耗時的解決方案。

原文:stackabuse.com/dynamic-pro…

做者: Vladimir Batoćanin

譯者:lee

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息