动态规划(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长度加1
                    dp[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;
        }

        @Override
        public 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; // 假设源节点是0
        DijkstraShortestPath 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)); // 使用简单遍历的方法可能得到10
           System.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]`作为结果。

上一篇:Maven filtering标签 读取变量配置二


下一篇:数据结构初阶:排序