动态规划问题一直是我心中永远的痛,说起来它的思想不复杂,就是把原问题分解成一个一个的子问题,逐渐分解下去。再详细一点说,对于某个问题,我们划分不同的状态和确定状态的表示方法,构建状态与状态之间的转移方程(问题与问题间的联系),最后确定问题的边界,解决问题。
话是这么说,但是动态规划的问题实在是太灵活了,一方面很多题目难以确定是不是用动态规划做(说不定是贪心呢),另一方面状态转移方程很难确定,很容易写错。在本篇博文中,我只会针对各个题目分析,不会进行太多大规模的总结(实在是总结不!出!来!啊!),题目难度从简到难,写到哪里算哪里。
1. 机器人走迷宫
题目链接:https://leetcode-cn.com/problems/unique-paths/
这个题目最早在我们数据结构的期末考试题中见到。当时哪懂什么动态规划,看到这个题目两种想法:深搜,排列组合。用深搜会导致溢出,排列组合直接算没有问题,但同样要注意整数溢出的方式,并且,排列组合也只能解决当前的这个问题,如果在迷宫中出现障碍物(不同路径Ⅱ https://leetcode-cn.com/problems/unique-paths-ii/),这个时候排列组合可能就失效了。
综上所述,这个题目最容易理解的做法就是动态规划。怎么理解这个事情呢?其实很简单,我们用dp[i][j]表达到达(i,j)这个地方有多少条不同的路径,这样我们就划分了状态和状态的表示方式。那么状态转移方程是怎样的呢?我们假设在(1,2)这个点,只有(0,2)和(1,1)可以直接到达(1,2),这种到达是没有什么变数的,因此dp[1][2] = dp[0][2]+dp[1][1],因此状态转移方程就是dp[i][j] = dp[i-1][j]+dp[i][j-1] (未考虑在边界的情况,边界更简单,只有一个点可以直接到达当前点)。最后我们再确定问题的边界,边界就是i=0和j=0的点,它们没得选,只有一条路径能到达它们。这样,我们就可以解决这个问题了。作为第一道题,上一下代码。
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0)
dp[i][j] = 1;
else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
}
2. 0-1背包问题
背包问题是非常经典的动态规划问题,有非常著名的背包九讲,可惜我看到后面就看晕了,先从最简单的来吧,上例题!leetcode分割等和子集:https://leetcode-cn.com/problems/partition-equal-subset-sum/submissions/
这个题目初看和背包问题没有什么关系,咱们可以把这个题目翻译一下:我手中有很多个物品,这些物品的大小作为一个数组储存在nums。已知背包的大小刚好是所有物品大小之和的一半,问是否可以把这个背包恰好塞满?这样,我们就把这个问题转化成了一个完全背包问题,只要确定每个物品(每个数)选还是不选就可以了。
同样建一个dp数组,dp[i][j]代表前i个物品是否能填满大小为j的背包。举个例子dp[0][nums[0]]代表第0号元素恰好可以填满大小为nums[0]的背包。
状态转移方程是这个样子的:dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]解释一下这个式子的含义。如果前i-1个都能装满大小为j的背包,那么第i个只要不选,就可以了。如果选第i个,那么我们就要确定前i-1个是否能恰好塞满j-nums[i]这个背包。
class Solution {
public:
bool canPartition(vector& nums) {
if(nums.size()<=1)
return false;
int sum = 0;
for(int i=0;i> dp(nums.size());
int target = sum/2;
for(int i=0;i v(target+1);
dp[i] = v;
}
dp[0][nums[0]] = true;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < target + 1; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= nums[i]) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[nums.size()-1][target];
}
};
3. 编辑距离
https://leetcode-cn.com/problems/edit-distance/
字符串之间的编辑距离,这是一个极其常用的算法,因此这个题目也极其重要,非常容易考到。
同样,我们新建一个二维dp数组,dp[i][j]代表第一个字符串s1前i个字符和第二个字符串s2前j个字符之间的编辑距离。
接下来我们分析一下它的状态转移方程。如果s1[i]==s2[j]那么dp[i][j]=dp[i-1][j-1],因为当前这一位一样,因此编辑距离就没有发生变化。当s1[i]!=s2[i]的时候,事情就变得比较麻烦了。我们分成三种情况来看:
int insert = dp[i][j-1]+1; // 在s2的基础上加上s1[i]
int replace = dp[i-1][j-1]+1; // 把s2[j]替换到s1[i]的位置上去
int remove = dp[i-1][j]+1; //
在上述值种挑最小的然后加1.
class Solution {
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
if(n1==0||n2==0)
return n1|n2;
char[] word1Chars = word1.toCharArray();
char[] word2Chars = word2.toCharArray();
int[][] dp = new int[n1+1][n2+1];
for(int i=0;i<=n1;i++)
dp[i][0] = i;
for(int i=0;i<=n2;i++)
dp[0][i] = i;
for(int i=1;i<=n1;i++)
{
for(int j=1;j<=n2;j++)
{
if(word1Chars[i-1]==word2Chars[j-1])
{
dp[i][j] = dp[i-1][j-1];
}
else
{
int insert = dp[i][j-1]+1;
int replace = dp[i-1][j-1]+1;
int remove = dp[i-1][j]+1;
dp[i][j] = Math.min(Math.min(insert,replace),remove);
}
}
}
return dp[n1][n2];
}
}
4. 最长上升子序列,最长下降子序列
这是所有动态规划里面我觉得最好想到,最容易理解的一个类型,好理解到我丝毫察觉不出来这个是一个动态规划。例题来自NOI,怪盗基德的滑翔翼。这个题目要分别求最长上升子序列和最长下降子序列。
我们新建一个一维DP数组,dp[i]代表前i个元素的最长上升子序列的长度,状态转移方程很好确定,我们依次看前dp的前i-1个值,取最大的加1即可。
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int testNum;
cin >> testNum;
for (int i = 0; i < testNum; i++)
{
int n;
int result = -1;
int result2 = -1;
cin >> n;
vector values(n);
vector dp(n);
vector dp2(n);
for (int j = 0; j < n; j++)
{
cin >> values[j];
}
for (int j = 0; j < n; j++)
{
dp[j] = 1;
for (int k = 0; k < j; k++)
{
if (values[k] < values[j]&& dp[k] + 1>dp[j])
{
dp[j] = dp[k] + 1;
}
}
result = max(result, dp[j]);
}
for (int j = 0; j < n; j++)
{
dp2[j] = 1;
for (int k = 0; k < j; k++)
{
if (values[k] > values[j] && dp2[k] + 1>dp2[j])
{
dp2[j] = dp2[k] + 1;
}
}
result2 = max(result, dp2[j]);
}
cout << max(result, result2) << endl;
}
return 0;
}
5. 最大正方形
https://leetcode-cn.com/problems/maximal-square/
这也是leetcode上面的一道题目。这个题目一眼就能看出要用动态规划,最开始做不出来就是在建立了二维DP数组,确立dp[i][j]是以(i,j)为右下角的没有想明白这个状态转移方程要怎么设置,看来答案之后才明白,我们取的应该是dp[i][j]=min(min(dp[i-1][j],dp[i-1][j-1]),dp[i][j-1])+1。之前老是想取max了,因此没有做出来,这道题目算是简单的,也比较常见。
class Solution {
public:
int maximalSquare(vector>& matrix) {
if(matrix.size()==0)
return 0;
vector> dp;
for(int i=0;i<=matrix.size();i++)
{
vector v(matrix[0].size()+1,0);
dp.push_back(v);
}
int res = 0;
for(int i=1;i<=matrix.size();i++)
{
for(int j=1;j<=matrix[i-1].size();j++)
{
if(matrix[i-1][j-1]=='1')
{
dp[i][j]=min(min(dp[i-1][j],dp[i-1][j-1]),dp[i][j-1])+1;
res = max(res,dp[i][j]);
}
}
}
return res*res;
}
};
6. 最长回文子串
https://leetcode-cn.com/problems/longest-palindromic-substring/
这是最后一道我觉得还算简单的题目(可以做出来,或者看答案很容易懂),后面就尽量挑看答案也看了好久的题目了。
回到这道题目,回文子串问题实在是太经典了,在各种考试中都如果在leetcode上关于回文的题目。
这个题目最开始做的时候,看答案感觉极其巧妙,同样是新建二维数组dp,dp[i][j]代表字符串s从i到j是回文,我们首先可以把所有dp[i][i]设置为true,dp[i][j]为true的要求是s[i]==s[j]并且dp[i+1][j-1]为true。
class Solution {
public:
string longestPalindrome(string s) {
int len=s.size();
if(len==0||len==1)
return s;
int start=0;
int max=1;
vector> dp(len,vector(len));
for(int i=0;i
7. A Mini Locomotive
https://blog.csdn.net/libin56842/article/details/9067241
这个题没做出来有一部分语言上的问题,题目没大读懂,理解有偏差,另外就是这个题目