算法笔记:动态规划(Dynamic programming)

动态规划问题一直是我心中永远的痛,说起来它的思想不复杂,就是把原问题分解成一个一个的子问题,逐渐分解下去。再详细一点说,对于某个问题,我们划分不同的状态和确定状态的表示方法,构建状态与状态之间的转移方程(问题与问题间的联系),最后确定问题的边界,解决问题。

话是这么说,但是动态规划的问题实在是太灵活了,一方面很多题目难以确定是不是用动态规划做(说不定是贪心呢),另一方面状态转移方程很难确定,很容易写错。在本篇博文中,我只会针对各个题目分析,不会进行太多大规模的总结(实在是总结不!出!来!啊!),题目难度从简到难,写到哪里算哪里。

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<int>& nums) {
        if(nums.size()<=1)
            return false;
        int sum = 0;
        for(int i=0;i<nums.size();i++)
        {
            sum+=nums[i];
        }
        if(sum%2==1)
            return false;
        vector<vector<bool>> dp(nums.size());
        int target = sum/2;
        for(int i=0;i<nums.size();i++)
        {
            vector<bool> 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 <iostream>
#include<string>
#include <algorithm>
#include <vector>
#include<math.h>
#include<queue>
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<int> values(n);
		vector<int> dp(n);
		vector<int> 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<vector<char>>& matrix) {
        if(matrix.size()==0)
            return 0;
        vector<vector<int>> dp;
        for(int i=0;i<=matrix.size();i++)
        {
            vector<int> 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<vector<int>>  dp(len,vector<int>(len));
        for(int i=0;i<len;i++)
        {
            dp[i][i]=1;
            if(i<len-1&&s[i]==s[i+1])
            {
                dp[i][i+1]=1;
                max=2;
                start=i;
            }
        }
        for(int l=3;l<=len;l++)
        {
            for(int i=0;i+l-1<len;i++)
            {
                int j=l+i-1;
                if(s[i]==s[j]&&dp[i+1][j-1]==1)
                {
                    dp[i][j]=1;
                    start=i;
                    max=l;
                }
            }
        }
        return s.substr(start,max);
    }
};

7.  A Mini Locomotive

https://blog.csdn.net/libin56842/article/details/9067241

这个题没做出来有一部分语言上的问题,题目没大读懂,理解有偏差,另外就是这个题目

发布了85 篇原创文章 · 获赞 100 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/caozixuan98724/article/details/100591732