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

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息