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)维的矩阵M。M [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 = true但M [10] [0] .includes = false的原因。
4.2.算法原则
接下来,让我们使用以下伪代码构造M [i] [k]的递归关系:
if (M[i-1][k].exists == True):M[i][k].exists = TrueM[i][k].includes = Falseelif (k-W[i]>=0):if(M[i-1][k-W[i]].exists == true):M[i][k].exists = TrueM[i][k].includes = Trueelse:M[i][k].exists = False
因此,解决方案的要点是将子问题分为两种情况:
对于容量k,当存在第一个i-1元素的解决方案
对于容量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 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:12Insert number of items:5Insert weights:9 7 4 10 3trueThe elements with the following indexes are in the solution:[5, 1]
背包问题的一个简单变化是在没有价值优化的情况下填充背包,但现在每个单独项目的数量无限。
通过对现有代码进行简单调整,可以解决这种变化:
// Old code for simplified knapsack problemelse 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 solutionelse 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[] arraySystem.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=10int[][] 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:12Insert number of items:5Insert weights:9 7 4 10 3Insert values:1 2 3 4 50/0 0/0 0/0 0/0 0/0 0/0 0/0 0/0 0/0 1/1 0/0 0/0 0/00/0 0/0 0/0 0/0 0/0 0/0 0/0 2/1 0/0 1/0 0/0 0/0 0/00/0 0/0 0/0 0/0 3/1 0/0 0/0 2/0 6/2 1/0 0/0 5/1 9/30/0 0/0 0/0 0/0 3/0 0/0 0/0 2/0 6/0 1/0 4/1 5/0 9/00/0 0/0 0/0 5/1 3/0 0/0 10/2 8/1 6/0 15/3 13/2 11/1 20/4Value: 20
6. Levenshtein Distance
另一个使用动态规划的非常好的例子是Edit Distance或Levenshtein Distance。
Levenshtein Distance就是两个字符串A,B,我们需要使用原子操作将A转换为B:
字符串删除
字符串插入
字符替换(从技术上讲,它不止一个操作,但为了简单起见,我们称之为原子操作)
这个问题是通过有条理地解决起始字符串的子串的问题来处理的,逐渐增加子字符串的大小,直到它们等于起始字符串。
我们用于此问题的递归关系如下:
如果a == b则c(a,b)为0,如果a = = b则c(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 s2int[][] 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 distanceint 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:manInsert second string:machineEdit 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 distance和LCS之间只有微小的差别,特别是移动成本。
在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.利用动态规划的其他问题
利用动态规划可以解决很多问题,下面列举了一些:
分区问题:给定一组整数,找出它是否可以分成两个具有相等和的子集
子集和问题:给你一个正整数的数组及元素还有一个合计值,是否在数组中存在一个子集的的元素之和等于合计值。
硬币变化问题:鉴于给定面额的硬币无限供应,找到获得所需变化的不同方式的总数
k变量线性方程的所有可能的解:给定k个变量的线性方程,计算它的可能解的总数
找到醉汉不会从悬崖上掉下来的概率:给定一个线性空间代表距离悬崖的距离,让你知道酒鬼从悬崖起始的距离,以及他向悬崖p前进并远离悬崖1-p的倾向,计算出他的生存概率9.结论
动态编程是一种工具,可以节省大量的计算时间,以换取更大的空间复杂性,这在很大程度上取决于您正在处理的系统类型,如果CPU时间很宝贵,您选择耗费内存的解决方案,另一方面,如果您的内存有限,则选择更耗时的解决方案。
原文:https://stackabuse.com/dynamic-programming-in-java/
作者:Vladimir Batoćanin
译者:lee
解耦Java模块的设计策略
上篇好文:
使用 Spring Data JPA 进行分页和排序
文章对你是否有帮助呢?
别忘记点右上角按钮分享给更多人哦~
点击在看,和我一起帮助更多开发者!返回搜狐,查看更多