动态规划实例解析:一个状态转移方程搞定所有步骤

爬楼梯问题的状态转移方程

在编程世界中,有一个经典的问题——爬楼梯问题。这个问题的描述很简单,假设你正在爬楼梯,需要爬到顶部。每次你可以爬 1 或 2 个台阶,你有多少种不同的方法可以爬到顶部呢?虽然问题描述简单,但它却涉及到了动态规划这一重要的编程思想。

我们可以用状态转移方程来描述这个问题。令dp[i]表示爬到第i个台阶的方法总数,那么状态转移方程就是dp[i] = dp[i - 1] + dp[i - 2]。这个方程的含义是:爬到第i个台阶的方法可以从第i - 1个台阶爬一步得到,也可以从第i - 2个台阶爬两步得到,所以dp[i]就是这两种情况的方法数之和。

public class OneMoreClass {
    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

在这段代码中,我们首先处理了n小于等于2的情况,然后定义了一个数组dp来存储每个台阶的方法数,最后通过一个循环来计算dp[i]。这就是动态规划的思想——通过简单的局部操作,推导出全局的最优解。

理解了爬楼梯问题的状态转移方程,我们就可以用同样的思路去解决更复杂的问题,比如接下来要介绍的背包问题。

背包问题的状态转移方程

在爬楼梯问题的基础上,我们来看一个更加复杂的问题,那就是背包问题。背包问题是动态规划中的经典问题,它描述了一个常见的场景:有一个容量为V的背包和N个物品,每个物品有自己的体积和价值,如何选择物品放入背包,使得背包内物品的总价值最大。

为了解决这个问题,我们需要引入状态转移方程。在背包问题中,我们定义dp[i][j]为前i个物品,当前背包的容量为j时,可以装入的最大价值。那么,这个问题的状态转移方程就可以表示为:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])

其中,dp[i-1][j]表示不选择第i个物品的情况,背包的最大价值就是前i-1个物品的最大价值;dp[i-1][j-w[i]] + v[i]表示选择第i个物品的情况,即当前背包的容量减去第i个物品的体积后,前i-1个物品在新的容量下的最大价值,再加上第i个物品的价值。

public class OneMoreClass {
    public int knapsack(int V, int N, int[] weights, int[] values) {
        int[][] dp = new int[N + 1][V + 1];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= V; j++) {
                if (j < weights[i - 1]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
                }
            }
        }
        return dp[N][V];
    }
}

这个问题的解决方案,不仅仅可以应用于背包问题,还可以应用于许多其他的问题,比如接下来我们要讲的最长递增子序列问题。

最长递增子序列的状态转移方程

从背包问题的世界中走出,我们来到了一个新的领域,那就是最长递增子序列的世界。在这个世界里,我们关注的是一个序列的递增性,而不再是背包的容量和物品的价值。最长递增子序列,顾名思义,就是在一个无序的整数序列中,找出一个子序列,使得这个子序列是递增的,并且长度最长。

在这个问题中,我们将使用动态规划的思想来解决。我们定义dp[i]为以第i个数字结尾的最长递增子序列的长度。那么,最长递增子序列的状态转移方程就是:dp[i] = max(dp[j]) + 1,其中0<=j<inum[j]<num[i]。这个状态转移方程的含义是:对于每一个i,我们查看在它之前的所有位置j,找出一个j,使得在j位置的数比i位置的数小,并且dp[j]也就是以j结尾的最长递增子序列的长度最大。然后,我们就可以得到以i结尾的最长递增子序列的长度。

让我们看一个Java代码的例子:

public class OneMoreClass {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        int maxLen = 0;
        for (int i = 0; i < nums.length; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLen = Math.max(maxLen, dp[i]);
        }
        return maxLen;
    }
}

在这个代码中,我们首先初始化了一个dp数组,然后通过两层循环,实现了状态转移方程的计算。最后,我们返回了最长递增子序列的长度。

有了这个状态转移方程,我们就可以解决最长递增子序列的问题了。但是,我们要知道,动态规划的世界并不只有这些,接下来,我们将进入到最大子数组和的世界中,去探索更多的知识。

最大子数组和的状态转移方程

在我们研究最长递增子序列的问题后,又有一种问题值得我们关注,那就是最大子数组和的问题。这个问题的核心是在一个数组中找到一段连续的子数组,使其和最大。

在这个问题中,我们将使用动态规划的方法来解决,核心思想是利用已有的解来解决当前的问题。为了解决这个问题,我们需要定义一个状态转移方程。在这个问题中,我们定义dp[i]为以nums[i]结尾的最大子数组和。那么,状态转移方程就可以定义为dp[i] = max(dp[i-1] + nums[i], nums[i])。这个方程的意思是,如果前一个子数组的和加上当前元素的值大于当前元素的值,那么我们就将当前元素加入到前一个子数组中,否则我们就以当前元素为新的子数组的开始。

现在,让我们通过一段Java代码来实现这个状态转移方程:

public class OneMoreClass {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        dp[0] = nums[0];
        int max = dp[0];
        for (int i = 1; i < n; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

在这段代码中,我们首先初始化dp[0]nums[0],然后从第二个元素开始,使用状态转移方程来更新dp[i],同时记录下最大的子数组和。

在我们理解了最大子数组和的状态转移方程后,下一步我们将会学习另一个重要的问题:最长公共子序列的状态转移方程。

最长公共子序列的状态转移方程

接下来,我们来详细解析一下最长公共子序列的状态转移方程。在动态规划的世界里,最长公共子序列问题是一个经典的问题,它的解决方案也非常有意思。

最长公共子序列(Longest Common Subsequence,LCS)问题是求两个序列的最长公共子序列长度。假设我们有两个序列X和Y,长度分别为m和n,我们可以使用一个二维数组dp[m+1][n+1]来保存中间结果,其中dp[i][j]表示序列X[0…i-1]和Y[0…j-1]的最长公共子序列的长度。

在这个问题中,状态转移方程可以定义为:

if X[i-1] == Y[j-1]{
    dp[i][j] = dp[i-1][j-1] + 1;
} else{
    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}

这个状态转移方程的含义是:如果X的第i个元素和Y的第j个元素相同,那么它们的最长公共子序列长度就是它们各自前一个元素的最长公共子序列长度加1;如果它们不同,那么它们的最长公共子序列长度就是X的前i个元素和Y的前j-1个元素的最长公共子序列长度,和X的前i-1个元素和Y的前j个元素的最长公共子序列长度中的较大值。

下面,让我们用Java代码来实现这个状态转移方程:

public class OneMoreClass {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m+1][n+1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i-1) == text2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

这就是最长公共子序列的状态转移方程的详细解析和Java实现,希望能帮助你理解和掌握这个重要的动态规划问题。

总结

我们一起走过了动态规划的世界,探索了爬楼梯问题、背包问题、最长递增子序列、最大子数组和以及最长公共子序列这五个经典问题。我们学习了如何定义状态,如何建立状态转移方程,如何通过编程实现状态转移。我们看到,无论问题多么复杂,只要我们能够找到问题的状态和状态之间的转移关系,就能够用动态规划的方法找到问题的解。

动态规划是一种强大的工具,它可以帮助我们解决许多看似复杂的问题。但是,动态规划并不是万能的。它需要我们对问题有深入的理解,需要我们能够找到问题的状态和状态转移方程。这需要我们不断地学习,不断地实践,不断地思考。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万猫学社

您的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值