动态规划(Dynamic programming)详解(含代码)

动态规划(Dynamic Programming, DP)是一种有效的计算机算法设计技术,主要用于解决具有重叠子问题和最优子结构特征的问题,这些问题是无法直接得出最优解,但可以通过求解其各个子问题的最优解来构造原问题的最优解。动态规划的核心思想是避免重复计算,在解决问题的过程中保存已解决的子问题答案,并利用这些答案来构建更大规模问题的解决方案。

以下是一些关键概念和步骤来详细说明动态规划:

核心思想与特点

1. 重叠子问题:在求解过程中,同一个子问题可能被多次计算,动态规划通过存储先前计算的结果(即创建记忆化表或数组),避免了这种重复计算。

2. 最优子结构:原问题的最优解包含其子问题的最优解。这意味着要解决原问题,必须先解决其所有子问题,并以某种方式组合这些子问题的最优解来形成整个问题的最优解。

动态规划的主要步骤

1. 定义状态:明确问题中的状态变量,它们能够描述问题的不同阶段或者大小。

2. 建立状态转移方程:描述如何从较小子问题的状态转移到较大问题的状态,即每个状态的值是如何根据其他状态计算出来的。

3. 初始化基础情况:确定状态空间中最简单的情况,可以直接得出其解,例如在递归树中最底层节点的值。

4. 计算并存储状态:按照一定顺序(通常是自底向上,如从小到大的问题规模)填充一个表格(称为DP表),记录每个状态的最优解。

5. 构造最终解答**:根据填满的DP表,还原出原问题的最优解。

应用示例

- 斐波那契数列:可以用动态规划来高效计算F(n),而不是用递归导致的指数级复杂度。
- 背包问题:0/1背包问题是最经典的动态规划应用之一,其中需要决定在不超过背包容量的情况下选择哪些物品以获得最大价值。
- 矩阵链乘法:最小化多个矩阵相乘所需的总乘法次数,通过合理安排矩阵相乘的顺序。
- 最长公共子序列(LCS):找出两个字符串的最长公共子序列,要求子序列保持原有字符相对顺序。
 

当然,动态规划在实际应用中不仅限于以上提到的基础示例,还包括但不限于以下经典问题:

- 最长递增子序列 (LIS):给定一个数列,寻找其中最长的严格递增子序列的长度。

- 最短路径问题:
   - 迪杰斯特拉算法 (Dijkstra):用于解决带权重的有向图或无向图中的单源最短路径问题,当所有边权均为非负时。
   - 贝尔曼-福特算法 (Bellman-Ford):可以处理有负权重边的最短路径问题,尽管它的时间复杂度较高。

- 区间调度问题:比如活动选择问题,对于一系列有开始和结束时间的活动,选择一组互不冲突的活动使得总的活动数量尽可能多。

- 最长公共前后缀问题:找出两个或多个字符串的最长公共前后缀。

- 整数划分问题:将一个正整数n划分成若干个正整数之和,求不同的划分方法总数。

- 股票买卖问题:考虑在一系列股票价格中买入和卖出以实现最大利润,有多种变种,如一次买卖、多次买卖且买卖不受限制、手续费收取等。

- 编辑距离 (Levenshtein distance):衡量两个字符串之间的差异程度,通过插入、删除或替换操作将一个字符串转换为另一个字符串所需的最少操作数。

动态规划的解题流程通常遵循四个原则:

1. 定义状态和状态集合。
2. 写出状态转移方程,明确当前状态值如何由前一状态或其他相关状态推导出来。
3. 初始化状态集合中的初始条件或边界条件。
4. 根据状态转移方程从初始状态开始逐步计算出目标状态的值。

值得注意的是,动态规划在空间上的优化策略也很重要,有时候可以通过滚动数组或更精细的空间压缩技巧减少内存消耗,尤其是在处理大规模数据时。

最后,动态规划作为一种重要的算法设计技术,强调了数学建模以及对问题结构的理解,通过对问题进行分解和重组,实现了复杂问题的有效求解。在计算机科学和运筹学领域有着广泛的应用。

注意事项

- 动态规划不总是适用所有问题,只有当问题满足上述特定性质时才有效。
- 确定状态转移方程往往是最具挑战性的一步,需要深入理解问题的本质。
- 动态规划的空间复杂度和时间复杂度取决于问题的具体形式,有时可通过进一步优化来减少空间需求。

 

1、以下是使用Java实现斐波那契数列的动态规划版本,以避免递归带来的栈溢出问题:

public class FibonacciDP {public static long fibonacci(int n) {if (n <= 0) return 0;if (n == 1) return 1;// 创建一个动态规划数组,只存储最后两个值即可,以节省空间long prevPrev = 0;long prev = 1;long current;// 动态规划填充过程,迭代计算每一个斐波那契数for (int i = 2; i <= n; i++) {current = prevPrev + prev;// 更新前两个值prevPrev = prev;prev = current;}return current;}public static void main(String[] args) {System.out.println(fibonacci(10));  // 输出第10个斐波那契数}
}

这段代码会从第三项开始(索引为2的位置),通过迭代的方式逐次计算斐波那契数列的每一项,并始终保持最新的两项数值,从而避免了递归造成的栈溢出问题。同时,仅保留了必要的两项数值以节省空间。如果不需要考虑空间优化,也可以使用一个数组来存储所有已计算过的斐波那契数,以便于解释动态规划的过程。
 

2、这里是使用Java实现最长公共子序列(Longest Common Subsequence, LCS)问题的代码示例:

import java.util.Scanner;public class LongestCommonSubsequence {public static int lcsLength(String str1, String str2) {int m = str1.length();int n = str2.length();// 初始化二维动态规划数组int[][] dp = new int[m + 1][n + 1];// 动态规划填充过程for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (str1.charAt(i - 1) == str2.charAt(j - 1)) {// 如果两个字符相等,则LCS的长度等于上一个字符对应LCS长度加1dp[i][j] = dp[i - 1][j - 1] + 1;} else {// 否则,LCS的长度等于左上角和左方的最大值dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}// 返回最后一行最后一列的值,即LCS的长度return dp[m][n];}public static void main(String[] args) {Scanner scanner = new Scanner(System.in);System.out.println("请输入第一个字符串:");String str1 = scanner.nextLine();System.out.println("请输入第二个字符串:");String str2 = scanner.nextLine();scanner.close();int result = lcsLength(str1, str2);System.out.println("最长公共子序列的长度为: " + result);}
}

这段代码首先定义了一个二维数组`dp`来存储子问题的解。然后通过两层循环遍历两个输入字符串的字符,比较当前字符是否相等,根据比较结果更新`dp`数组。最后返回`dp`数组右下角的值,这个值就是两个输入字符串的最长公共子序列的长度。
 

3、以下是0/1背包问题(每个物品最多选一件)的Java实现,采用动态规划的方法:

import java.util.Scanner;public class KnapsackProblem {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);// 输入物品数量System.out.print("Enter the number of items: ");int n = scanner.nextInt();// 输入背包容量System.out.print("Enter the capacity of the knapsack: ");int capacity = scanner.nextInt();// 输入物品的重量和价值int[] weights = new int[n];int[] values = new int[n];System.out.println("Enter weight and value pairs for each item.");for (int i = 0; i < n; i++) {System.out.print("Weight of item " + (i + 1) + ": ");weights[i] = scanner.nextInt();System.out.print("Value of item " + (i + 1) + ": ");values[i] = scanner.nextInt();}scanner.close();// 使用动态规划计算最大价值int[][] dp = new int[n + 1][capacity + 1];for (int i = 1; i <= n; i++) {for (int w = 0; w <= capacity; w++) {// 不选择当前物品dp[i][w] = dp[i - 1][w];// 如果背包还能容纳当前物品,则比较选择和不选择当前物品的最大价值if (weights[i - 1] <= w) {dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);}}}// 输出背包内物品的最大价值System.out.println("The maximum value that can be put in the knapsack is: " + dp[n][capacity]);}
}

在这段代码中,首先读取用户输入的物品数量、背包容量以及每个物品的重量和价值。接着,创建一个二维动态规划数组`dp`,其中`dp[i][j]`表示在面对前`i`个物品并且背包容量为`j`时能装入的最大价值。

通过双重循环遍历所有物品和背包容量范围,依据动态规划的状态转移方程进行计算,最终得到背包所能装载物品的最大价值。

4、下面是一个基于动态规划实现矩阵链乘法(Matrix Chain Multiplication,MCM)的Java代码示例,用于计算最小化矩阵乘法所需的操作数(即乘法次数):

import java.util.Scanner;class MatrixChainMultiplication {// 动态规划数组,dp[i][j] 表示计算矩阵链 A[i]...A[j] 所需的最小乘法次数private static int[][] dp;// 计算矩阵链乘法的最小代价public static int matrixChainOrder(int[] p) {// 获取矩阵链的长度int n = p.length - 1;// 初始化动态规划数组dp = new int[n + 1][n + 1];for (int i = 1; i <= n; i++) {dp[i][i] = 0;}// 自底向上填充dp数组for (int len = 2; len <= n; len++) {for (int i = 1; i <= n - len + 1; i++) {int j = i + len - 1;dp[i][j] = Integer.MAX_VALUE;for (int k = i; k <= j - 1; k++) {// 计算加入括号后的代价,并加上两边部分的代价int q = dp[i][k] + dp[k + 1][j] + p[i - 1] * p[k] * p[j];if (q < dp[i][j]) {dp[i][j] = q;}}}}// 返回最小代价,即dp[1][n]return dp[1][n];}public static void main(String[] args) {Scanner scanner = new Scanner(System.in);System.out.println("Enter the sequence of matrix dimensions separated by space:");String[] input = scanner.nextLine().split(" ");int n = input.length;int[] p = new int[n];// 将输入的维度转换为整数数组for (int i = 0; i < n; i++) {p[i] = Integer.parseInt(input[i]);}// 计算最小乘法次数int minCost = matrixChainOrder(p);System.out.println("Minimum number of scalar multiplications needed is: " + minCost);scanner.close();}
}

在这个例子中,`p` 数组存储了矩阵链的维度,其中 `p[i]` 表示矩阵 `A[i]` 的列数同时也是矩阵 `A[i+1]` 的行数。代码首先初始化动态规划数组,然后通过三层嵌套循环来填充这个数组,最终返回 `dp[1][n]`,即计算全部矩阵链乘法的最小代价。在循环体内部,通过计算不同括号分割点 `k` 下的代价并选择最小值来更新 `dp[i][j]`。

5、以下是一个简单的Dijkstra算法在Java中实现的例子,用于解决无负权边的加权图中的单源最短路径问题。这里采用了邻接列表的方式来存储图,并使用优先队列(Java中的PriorityQueue)来选取当前已知最短距离节点。

import java.util.*;public class DijkstraShortestPath {private static class Edge implements Comparable<Edge> {int to;int weight;public Edge(int to, int weight) {this.to = to;this.weight = weight;}@Overridepublic int compareTo(Edge other) {return Integer.compare(this.weight, other.weight);}}private List<List<Edge>> adjList;private int[] dist;private boolean[] visited;private int source;public DijkstraShortestPath(List<List<Edge>> adjList, int source) {this.adjList = adjList;this.source = source;}public void dijkstra() {int V = adjList.size();dist = new int[V];Arrays.fill(dist, Integer.MAX_VALUE);dist[source] = 0;visited = new boolean[V];PriorityQueue<Edge> pq = new PriorityQueue<>();pq.offer(new Edge(source, 0));while (!pq.isEmpty()) {Edge uEdge = pq.poll();int u = uEdge.to;if (visited[u]) continue;visited[u] = true;for (Edge e : adjList.get(u)) {int v = e.to;int alt = dist[u] + e.weight;if (alt < dist[v]) {dist[v] = alt;pq.offer(new Edge(v, alt));}}}}public int[] getDistances() {return dist;}public static void main(String[] args) {// 构建邻接列表表示的加权图List<List<Edge>> graph = new ArrayList<>();// ...此处添加图的节点和边...int sourceNode = 0; // 假设源节点是0DijkstraShortestPath dsp = new DijkstraShortestPath(graph, sourceNode);dsp.dijkstra();int[] shortestDistances = dsp.getDistances();// 输出从源节点到所有节点的最短距离for (int i = 0; i < shortestDistances.length; i++) {System.out.println("Shortest distance from node " + sourceNode + " to node " + i + " is: " + shortestDistances[i]);}}
}

注意:在实际运行此代码之前,需要构建一个表示图的邻接列表。这里没有展示具体的图构建过程,可以根据实际情况添加节点和边。此外,`Edge` 类型是为了方便存储边的信息,包括目标节点编号和边的权重。在主函数中,初始化一个DijkstraShortestPath对象,并传入邻接列表和源节点编号,然后调用 `dijkstra()` 方法计算最短路径,最后调用 `getDistances()` 方法获取计算出的所有节点到源节点的最短距离。

6、贝尔曼-福特算法可以处理带有负权重边的图,下面是使用Java实现贝尔曼-福特算法的一个示例:

import java.util.*;public class BellmanFord {private static class Edge {int src, dest, weight;public Edge(int src, int dest, int weight) {this.src = src;this.dest = dest;this.weight = weight;}}public static void bellmanFord(List<Edge> edges, int vertices, int source) {// 初始化距离数组,所有节点距离都设为无穷大,除了源节点int[] dist = new int[vertices];Arrays.fill(dist, Integer.MAX_VALUE);dist[source] = 0;// 进行V-1轮迭代,因为至少需要V-1次才能找到可能存在负权环的情况for (int i = 0; i < vertices - 1; i++) {for (Edge edge : edges) {int u = edge.src;int v = edge.dest;int weight = edge.weight;// 如果找到了一条更短的路径,就更新dist数组if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;}}}// 检查是否存在负权回路for (Edge edge : edges) {int u = edge.src;int v = edge.dest;int weight = edge.weight;if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {System.out.println("Graph contains negative weight cycle");return;}}// 输出从源节点到所有节点的最短路径for (int i = 0; i < vertices; i++) {System.out.println("Shortest distance from source to vertex " + i + " is: " + dist[i]);}}public static void main(String[] args) {// 示例图,假设有这样一些边及其权重List<Edge> edges = Arrays.asList(new Edge(0, 1, -1),new Edge(0, 2, 4),new Edge(1, 2, 3),new Edge(1, 3, 2),new Edge(2, 1, 2),new Edge(2, 4, 3),new Edge(3, 2, 1),new Edge(3, 4, 5));int vertices = 5; // 图的顶点数int source = 0; // 源节点编号bellmanFord(edges, vertices, source);}
}

在这个示例中,`Edge` 类用来表示图中的每一条边,包括起始节点、终止节点和权重。`bellmanFord` 函数接收边的列表、顶点数和源节点编号作为参数,进行V-1轮松弛操作,然后检查是否有边的权重可以再次更新,若存在则表示存在负权环。如果没有负权环,输出的就是从源节点到所有其他节点的最短路径长度。在主函数中,创建了一个图的边集示例并调用 `bellmanFord` 函数计算最短路径。

7、区间调度问题的贪心算法实现通常指的是选择不相交区间使得选中的区间数量最大化。以下是一个基于贪心策略(每次都选择结束时间最早的区间)的Java实现示例:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;public class IntervalScheduling {static class Interval {int start;int end;public Interval(int start, int end) {this.start = start;this.end = end;}}public static int maxNonOverlappingIntervals(List<Interval> intervals) {// 先按区间结束时间升序排列intervals.sort(Comparator.comparingInt(interval -> interval.end));int count = 1;int lastEnd = intervals.get(0).end;for (int i = 1; i < intervals.size(); i++) {Interval current = intervals.get(i);if (current.start >= lastEnd) {// 当前区间不与前面已选区间的任何一个相交count++;lastEnd = current.end;}}return count;}public static void main(String[] args) {List<Interval> intervals = Arrays.asList(new Interval(1, 3),new Interval(2, 5),new Interval(4, 6),new Interval(7, 9));System.out.println("Maximum number of non-overlapping intervals: " + maxNonOverlappingIntervals(intervals));}
}

在这个示例中,首先定义了一个`Interval`类来表示每个区间。然后定义了一个`maxNonOverlappingIntervals`方法,该方法接受一个区间列表,并首先按照结束时间对区间进行排序。接下来,维护一个计数器`count`表示已选择的不相交区间数,以及一个变量`lastEnd`来记录最后选择的区间的结束时间。

在遍历排序后的区间列表时,如果当前区间的开始时间大于等于`lastEnd`,则说明它可以与已选择的区间一起构成一个不相交的集合,于是增加计数器并更新`lastEnd`。最后返回计数器的值,即为最大不相交区间的数量。

注意,这个实现假设了区间是左闭右开的(例如,区间 `[1, 3]` 包含起点1,但不包含终点3)。如果区间是左闭右闭的,只需稍作调整即可适应。

8、最长公共前后缀问题通常是指找到一组字符串中的最长公共前后缀,也就是既是前缀又是后缀的最长子串。这里是最长公共前缀问题,这是一个更为常见的问题。下面提供一个Java实现找到一组字符串的最长公共前缀:

public class LongestCommonPrefix {public static String longestCommonPrefix(String[] strs) {if (strs == null || strs.length == 0) {return "";}String prefix = strs[0];for (int i = 1; i < strs.length; i++) {while (strs[i].indexOf(prefix) != 0) {// 如果prefix不是strs[i]的前缀,不断缩短prefix直到找到合适的prefix = prefix.substring(0, prefix.length() - 1);// 当prefix为空时,说明没有公共前缀,跳出循环if (prefix.isEmpty()) {break;}}// 如果某次循环后prefix为空,后面的字符串无需再比较if (prefix.isEmpty()) {break;}}return prefix;}public static void main(String[] args) {String[] strs = {"flower", "flow", "flight"};System.out.println("Longest common prefix: " + longestCommonPrefix(strs));}
}

这个程序首先检查输入字符串数组是否为空或为空数组,如果是,则直接返回空字符串。然后将第一个字符串作为初始公共前缀,依次与其他字符串进行比较,每次将公共前缀与下一个字符串的前缀进行匹配,如果不匹配,则缩短公共前缀,直至找到所有字符串的最长公共前缀,或者公共前缀变为空字符串(表明没有公共前缀)。最后返回找到的最长公共前缀。

9、整数划分问题有不同的变种,一种常见的是计算将一个正整数n划分成若干个正整数之和的不同方法数目。下面是一个使用动态规划方法计算整数划分的方案数的Java实现:

public class IntegerPartition {public static int countPartitions(int n) {// 初始化动态规划数组int[] dp = new int[n + 1];dp[0] = 1; // 划分0的方法数为1,即空划分// 动态规划填充过程for (int i = 1; i <= n; i++) {for (int j = 1; j <= i; j++) {// 对于每个i,尝试将其划分为j(从1到i)dp[i] += dp[i - j]; // 加上划分i-j的所有方法数}}return dp[n]; // 返回划分n的方法数}public static void main(String[] args) {int n = 5;System.out.println("Number of ways to partition integer " + n + " is: " + countPartitions(n));}
}

另外,如果希望列出所有具体的划分组合,那么需要额外的数据结构来存储和打印结果,而非仅仅计算方案数。下面是一个生成所有划分的Java实现:

import java.util.ArrayList;
import java.util.List;public class IntegerPartitionWithDetails {public static List<List<Integer>> generatePartitions(int n) {List<List<Integer>> allPartitions = new ArrayList<>();List<Integer> currentPartition = new ArrayList<>();backtrack(allPartitions, currentPartition, n, 1);return allPartitions;}private static void backtrack(List<List<Integer>> allPartitions, List<Integer> currentPartition, int remaining, int start) {if (remaining == 0) {allPartitions.add(new ArrayList<>(currentPartition));return;}for (int i = start; i <= remaining; i++) {currentPartition.add(i);backtrack(allPartitions, currentPartition, remaining - i, i); // 注意这里的start仍然为i,保证不会出现重复的划分currentPartition.remove(currentPartition.size() - 1); // 回溯,撤销上一步的选择}}public static void main(String[] args) {int n = 5;List<List<Integer>> partitions = generatePartitions(n);System.out.println("All partitions of integer " + n + ":");for (List<Integer> partition : partitions) {System.out.println(partition);}}
}

第一段代码用于计算划分的方案数,第二段代码则是用于生成所有可能的划分组合。

10、股票买卖问题通常指的是以下两种变体:

        1. 单次买卖:
           给定一个股票价格的序列,找到一个买入点和一个卖出点,使得在买入后卖出可以获得最大的利润。假设只能进行一次买卖操作。  

   import java.util.Arrays;public class StockTradingSingleTransaction {public int maxProfit(int[] prices) {if (prices.length < 2) return 0; // 如果只有一天数据,无法交易int minPrice = prices[0];int maxProfit = 0;for (int i = 1; i < prices.length; i++) {// 更新最低价minPrice = Math.min(minPrice, prices[i]);// 计算当前卖出的最大利润(基于当前最低价)maxProfit = Math.max(maxProfit, prices[i] - minPrice);}return maxProfit;}public static void main(String[] args) {StockTradingSingleTransaction solver = new StockTradingSingleTransaction();int[] prices = {7, 1, 5, 3, 6, 4};System.out.println("最大利润: " + solver.maxProfit(prices)); // 应输出7}}

        2. 多次买卖(允许任意次数交易):
   在这个版本的问题中,可以任意多次买入和卖出股票,只要不同时持有两份或多份股票。  

   public class StockTradingMultipleTransactions {public int maxProfit(int[] prices) {if (prices.length < 2) return 0;int profit = 0;for (int i = 1; i < prices.length; i++) {// 只要有利润就可以卖出再买入if (prices[i] > prices[i - 1]) {profit += prices[i] - prices[i - 1];}}return profit;}// 或者使用动态规划方法处理这个问题,考虑每两天之间的最大利润差值public int maxProfitDynamicProgramming(int[] prices) {int n = prices.length;if (n <= 1) return 0;// dp[i][0] 表示第i天结束时,手上没有股票的最大利润// dp[i][1] 表示第i天结束时,手上持有一份股票的最大利润int[][] dp = new int[n][2];dp[0][0] = 0; // 第一天结束时,未持有股票没有利润dp[0][1] = -prices[0]; // 第一天结束时,如果持有股票,则亏损第一天的股票价格for (int i = 1; i < n; i++) {dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); // 不持有股票的情况,前一天没持有则直接加上今天的收益,前一天持有今天卖出dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // 持有股票的情况,前一天没持有今天买入,前一天持有则判断是否换手}return dp[n - 1][0]; // 最终答案是最后一天未持有股票的最大利润}public static void main(String[] args) {StockTradingMultipleTransactions solver = new StockTradingMultipleTransactions();int[] prices = {7, 1, 5, 3, 6, 4};System.out.println("最大利润(多次买卖): " + solver.maxProfit(prices)); // 使用简单遍历的方法可能得到10System.out.println("最大利润(多次买卖,动态规划): " + solver.maxProfitDynamicProgramming(prices)); // 应输出13}}

请注意,第二个问题也可以通过更复杂的动态规划解决方案来解决,特别是当交易次数有限制时(如最多两次买卖),或者有手续费等情况时。上述`maxProfitDynamicProgramming`方法就是针对多次买卖问题的一个经典动态规划解决方案。

11、编辑距离(Levenshtein distance)问题可以使用动态规划方法解决,以下是一个Java实现示例:

public class EditDistance {public static int calculateEditDistance(String str1, String str2) {int len1 = str1.length();int len2 = str2.length();int[][] dp = new int[len1 + 1][len2 + 1];// 初始化状态数组的第一行和第一列for (int i = 0; i <= len1; i++) {dp[i][0] = i;}for (int j = 0; j <= len2; j++) {dp[0][j] = j;}// 动态规划填充过程for (int i = 1; i <= len1; i++) {for (int j = 1; j <= len2; j++) {if (str1.charAt(i - 1) == str2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];} else {dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;}}}// 返回dp数组右下角的元素,即两个字符串的编辑距离return dp[len1][len2];}public static void main(String[] args) {String string1 = "kitten";String string2 = "sitting";System.out.println("Edit distance between '" + string1 + "' and '" + string2 + "' is: " + calculateEditDistance(string1, string2));}
}

这个Java程序首先初始化了一个二维数组`dp`,其中`dp[i][j]`代表字符串`str1`的前`i`个字符与字符串`str2`的前`j`个字符之间的编辑距离。通过迭代遍历两个字符串的字符,根据当前字符是否相等以及上一步的编辑距离状态,计算出当前位置的编辑距离。最后返回`dp[len1][len2]`作为结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/784520.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ER图与关系模型

1、试画出数据库系统的三级模式结构图。 2、试画出数据库系统的组成图。 3、学校运动会有来自全校各学院运动员组成的代表团参赛各类竞赛项目。一个 代表团由多名运动员组成&#xff1b;一个运动员可以参加不同项目的比赛&#xff0c;而一个竞赛项目允许多名运动员参赛。为了…

ADB(Android Debug Bridge)操作命令详解及示例

ADB&#xff08;Android Debug Bridge&#xff09;是一个强大的命令行工具&#xff0c;它是Android SDK的一部分&#xff0c;主要用于Android设备&#xff08;包括真实手机和平板电脑以及模拟器&#xff09;的调试、系统控制和应用程序部署。 下面是一些ADB的常用命令&#xff…

【力扣】200.岛屿数量(染色法DFS深搜)

岛屿数量 题目描述 链接:力扣&#xff1a;200.岛屿数量 给你一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆…

小程序开发调试指南

调试指南 一、Vconsole 一些线上问题需要在真机中进行调试或日志输出&#xff0c;为方便查看前端日志的输出&#xff0c;App 端提供了调试模式, 打开调试模式后面板界面中会显示 vConsole 按钮&#xff0c;点击后可打开手机端 vConsole界面查看日志输出。 1.如何开启 点击小…

数字示波器

数字示波器 综述&#xff1a;本文讲述了数字示波器的电路组成。 一&#xff0e;定义 显示电信号波形的仪器 二&#xff0e;组成 由模拟前端处理电路、电源电路、单片机电路、控制电路、触发电路、校准电路组成。 1&#xff09;模拟前端处理电路 将输入的模拟信号处理后传…

CAN(1) 入门介绍

1.基本概念 CAN 是控制器局域网络 (Controller Area Network) 的简称&#xff0c;它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的&#xff0c;并最终成为国际标准(ISO11519以及ISO11898),是国际上应用最广泛的现场总线之一。 CAN 总线协议已经成为汽车计算机控制系…

OmniGraffle Pro for mac 出色的图形设计软件

OmniGraffle Pro是一款非常出色的图形设计软件&#xff0c;它主要适用于Mac和iPad平台&#xff0c;可以用来轻松绘制各种精美的图表、示意图和界面设计。 软件下载&#xff1a;OmniGraffle Pro for mac中文注册激活版 以下是OmniGraffle Pro的一些主要特点和功能&#xff1a; 界…

短剧猫H5页面html源码

短剧猫H5页面html源码&#xff0c;包含一个接口&#xff0c;像俩天块样式发送剧名回复网盘链接&#xff0c;文件上传解压就能用。 源码免费下载地址抄笔记 (chaobiji.cn)https://chaobiji.cn/

【IP组播】PIM-SM的RP、RPF校验

目录 一&#xff1a;PIM-SM的RP 原理概述 实验目的 实验内容 实验拓扑 1.基本配置 2.配置IGP 3.配置PIM-SM和静态RP 4.配置动态RP 5.配置Anycast RP 二&#xff1a; RPF校验 原理概述 实验目的 实验内容 实验拓扑 1.基本配置 2.配置IGP 3.配置PIM-DM 4.RPF校…

F280049的JTAG接口与仿真器XDS100V3六线解法,仿真器供电

虽然程序更新和调试主要用CAN、SCI接口加上二次bootloader以及上位机。但是仿真器在初期或者有时候&#xff0c;还是要用一用的。板子上尽量以最小空间留一个仿真器的接插件。 用低成本的仿真器XDS100V3,用六根线实现连接。有几点改动&#xff0c;在此记录下。 根据数据手册中…

SOC内部集成网络MAC外设+ PHY网络芯片方案:MII/RMII 接口与 MDIO 接口

一. 简介 本文来了解一下常用的一种网络硬件方案&#xff1a;SOC内部集成网络MAC外设 PHY网络芯片方案。 其中涉及的 MII接口&#xff0c;RMII接口&#xff08;MII接口与RMII接口二选一&#xff09;&#xff0c;MDIO接口&#xff0c;RJ45。 二. MII/RMII 接口&#xff0c;M…

2024系统架构师---面向对象架构风格的概念与应用

面向对象架构风格是一种以对象为中心的软件设计和开发方法&#xff0c;强调数据&#xff08;属性&#xff09;和行为&#xff08;方法&#xff09;的封装、继承和多态。在面向对象架构中&#xff0c;系统被视为一组相互作用的对象集合&#xff0c;每个对象代表了现实世界中的实…

JavaWeb学习笔记01

一、教程简介 全新JAVAWEB&#xff08;里程碑版&#xff09; 一套更适合后端工程师学习的WEB教程 All in Java 1、后端 ① Spring全家桶及微服务框架 ② 高性能数据库和消息组件 ③ Web攻击防护安全控制手段 ④ 其他第三方SDK生态环境 ...... 2、前端 ① 视图三大件&…

【备忘录】Docker 2375远程端口安全漏洞解决

最近为了项目需要&#xff0c;把docker 的远程端口2375 给开放了。不出意外出意外了。没多久&#xff0c;网站报流量告警&#xff0c;第一反应就是开放2375这个端口问题导致&#xff0c;毫不迟疑直接切换服务器。关闭该台服务器的docker服务&#xff0c;并逐步清理掉挖矿进程&a…

存放自定义数据类型的大/小根堆定义

要将小于&#xff08;<&#xff09;运算符重载函数改为适用于小根堆&#xff08;即最小堆&#xff09;&#xff0c;您需要确保当传入对象的值小于当前对象的值时&#xff0c;函数返回true。这样&#xff0c;当您构建堆时&#xff0c;具有较小值的节点会被放置在较高的层次&a…

从0到1手把手撸码搭建后台管理系统

从0到1手把手撸码搭建后台管理系统 第一章:系统介绍开发目的与过程 要使用 Vue3 搭建后台&#xff0c;你可以按照以下步骤进行&#xff1a; 安装 Vue3&#xff1a;使用包管理工具&#xff08;如 npm 或 yarn&#xff09;安装 Vue3。选择构建工具&#xff1a;选择一个适合的构…

课时81:流程控制_循环控制_continue实践

1.4.3 continue实践 学习目标 这一节&#xff0c;我们从 基础知识、简单实践、小结 三个方面来学习。 基础知识 简介 continue命令是在处理过程中跳出循环的一种简单方法。可以使用continue命令跳出当前的循环直接进入到下一个循环&#xff0c;包括for、while、until等。co…

Day35:学习尚上优选项目

学习计划&#xff1a;完成尚硅谷的尚上优选项目 学习进度&#xff1a;尚上优选项目 知识点&#xff1a; 四、 搭建平台管理端前端环境 权限管理模块-用户管理 开发为用户分配角色接口用户管理前端测试 权限管理模块-菜单管理 菜单管理需求菜单表设计开发菜单管理CRUD接口开…

给web开发零基础小白的简明入门教程

1.网页三大件&#xff1a;HTML、CSS、JS HTML&#xff08;HyperText Markup Language超文本标记语言&#xff09;&#xff1a;构成网页的结构&#xff0c;结构由元素&#xff08;标签&#xff09;组成。 CSS&#xff08;Cascading Style Sheets层叠样式表&#xff09;&#xf…

选择排序及其优化

目录 思想&#xff1a; 代码&#xff1a; 代码优化&#xff1a; 需要注意的特殊情况&#xff1a; 可能出现的所有特殊情况&#xff1a; 优化完成代码&#xff1a; 思想&#xff1a; 每一次遍历数组&#xff0c;选择出最大或最小的数&#xff0c;将其与数组末尾或首位进行…