leetcode学习记录_动态规划

本人暂时对于动态规划的理解还不是很深,只能说是皮毛甚至是错误的理解,如果有人发现了错误之处,请提醒我,我会尽快改正,非常感谢!


198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 :

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
来源:力扣(LeetCode)

思路:

设偷到第n间房子的最高收益为 f (n) , nums数组存储每间房子的金钱
因为不能连续偷,只能空着一个房间,我们设 n 为当前房屋,
如果偷这间房子,我们当然不能偷 n - 1,只能偷n - 2,所以偷 n 的最高收益就是 f (n - 2) + nums[ n ],

那不偷了?不偷 n 就代表能偷 n - 1 了,
所以不偷 n 的最大收益就是 f ( n - 1)

我用num1 , num2分别存储 f ( n - 2) , f ( n - 1)的值 ,
用res = max( f( n - 2) + nums[ n ] , f ( n - 1)) 来存储当前的最佳选择,即res = f ( n )
当 n++往下走一步的时候, 现在的 f ( n - 2 ) = 上次的 f ( n - 1),
即 num1 = num2 ,
现在f ( n - 1 ) 就等于以前的f ( n ) ,也就是 res嘛

这就是核心思路,下面说说特殊情况和最初的选择,毕竟没有最初的选择的话怎么往后一步步推呢?

先说房子数量 0 的情况,这种情况你偷个啥,都没得偷,return 0;
数量为 1 ,这种情况没得选,只能偷这一家,所以 return nums[ 0 ];
数量为 2 , 这种情况就 2 选 1 , 但是我们暂不用处理,原因见代码注释

因为有 num1 和 num2 所以我们的 n 必须得从第三间房子开始,也就是说nums.size()必须 >= 3 ,
换成数组下标就是说 n 从 2 开始 :
num1 是 f ( n - 2)
所以当 n = = 2 时,num1也是没得选择的只能选nums[ 0 ]
num2 是 f ( n - 1)
当 n = = 2 时,num2是有选择权的,
所以 num2 = max (nums[ 0 ] , nums[ 1 ])
就这样,初始化完成了

开始代码:

class Solution {
public:
    int rob(vector<int>& nums) {
	int N = nums.size();//储存房子数量
	if(N == 0) return 0;
	if(N == 1) return nums[0];
	int num1 = nums[0] , num2 = max(nums[0] , nums[1]) , res = 0;
	for(int n = 2; n < N ; n++)
	{
		res = max(num1 + nums[n] , num2);//保存最佳选择
		num1 = num2;//这两步都是一个道理,往后推一位嘛,毕竟n都++了
		num2 = res;//这里可以发现,循环的最后,num2 = res;
	}
	//这里其实返回res 和 num2都是一样的 , 但是有个房子数量为2的特殊情况,
//为了少写一个if 我就返回num2了 , 因为不进入循环的情况下,
//num2 = max(nums[0] , nums[1]) 已经是最高收益了	
	return num2;
	}
};


213. 打家劫舍 II

第二题是房子首尾相连了,偷了第一家就不能偷最后一家,那么我们就把最后的结果当成两种情况来看,

第一种:没有第一家
第二种:没有最后一家
分别算出来最大收益,然后返回大的那个就好了

把上一题的核心代码复制两遍,改改条件,添加特殊情况,就ok了

代码:

class Solution {
public:
    int rob(vector<int>& nums) {
		int N = nums.size();//储存房子数量
		if(N == 0) return 0;
		if(N == 1) return nums[0];
		//因为不能直接返回num2了,所以要写出来
		if(N == 2) return max(nums[0] , nums[1]);
		//因为首尾相连的规则而新出现的特殊情况
		if(N == 3) return max(nums[2] , max(nums[0] , nums[1]));
		int num1 = nums[0] , num2 = max(nums[0] , nums[1]);
		int res1 = 0,res2 = 0;
		for(int n = 2;n<N-1;n++)//跳过最后一间屋子
		{
			res1 = max(num1 + nums[n], num2);
			num1 = num2;
			num2 = res1;
		}
		num1 = nums[1];num2 = max(nums[2] , nums[1]);//因为要跳过第一间房子,所以重新赋值,
		for(int n = 3;n<N;n++)//跳过第一间屋子
		{
			res2 = max(num1 + nums[n], num2);
			num1 = num2;
			num2 = res2;
		}
		return max(res1 , res2);//比较两种情况,哪个大返回哪个
	}
};


剑指 Offer 46. 把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 :

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”

来源:力扣(LeetCode)
思路
也是动态规划,很简单的一题,ps:虽然一开始我脑子没转过来
当前的数字下标为n,假设这时的最大翻译方法为 f ( n )
f ( n ) 怎么算呢?在下标为 n 时,只会有两种情况,
1: 自己单独翻译 如图左叉
2: 和 n - 1 一起翻译 如图右叉在这里插入图片描述
于是一般来说 f ( n ) = f ( n - 1 ) + f ( n - 2 );
特殊情况是什么?特殊情况就是联合翻译时发现数字太大(> 25)或者太小了( < 10 ) 太大无法翻译,太小了说明有个开头是0,也无法翻译,这种情况 f ( n ) = f ( n - 1 )
代码:

class Solution {
public:
    int translateNum(int num) {
		string str = to_string(num);
		int size = str.size();
		if(size == 0) return 0;//特殊情况
		if(size == 1) return 1;
		int num1 = 1 , res = 0 , num2;
		if(str.substr(0,2) >= "10" && str.substr(0,2) <= "25")
		num2 = 2;//从一开始就要确定有没有特错误,有没有<10  , >25
		else
		num2 = 1;
		for(int n = 2;n<size;++n)//从第三个数字开始,下标为2
		{
			string temp = str.substr(i-1,2);
			if(temp >= "10" && temp <= "25")	res = num1 + num2;
			else	res = num2;
			num1 = num2;
			num2 = res;
		}
		return num2;//返回num2可以直接无视size == 2的特殊情况,少写个if
    }
};

发现了没有,和上面的打家劫舍特别特别像,都是先if判断特殊情况,
然后初始化num1 , num2
for循环从 第三个数字开始(下标为2)
后面num1 = num2 ; num2 = res也是一模一样
只有给res赋值的时候变了一点,而给res赋值正是题目的精髓部分


322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
来源:力扣(LeetCode)
前面写的几题都是省略的dp数组,子问题数量都是已知的,(比如上面一题的翻译,要求 f ( n )的话,只要求f(n -1 ) 和 f(n -1 )就行),因此只用两个变量即可完成运算,这题就不行了,因为子问题数未知,你不知道有多少种零钱,所以只能写完整的dp数组了;
所谓dp数组,其实就是f(0)到 f(n)组成的数组,从下往上推答案,一个个的填入dp数组中

代码:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, amount + 1);
        dp[0] = 0;
        for (int i = 0; i <= amount; ++i) 
        {
            for (int& temp : coins) //用循环在多个子问题中取得最优解
            {//这里的if的意思是 跳过无解的子问题
            //,比如我要凑2块钱,零钱有{1, 3, 5}三种,f(1)很显然只能用1来凑,f(2)也是
                if (temp > i) continue;                
                dp[i] = min(dp[i], dp[i - temp] + 1);
            }
        }
//这里的判断        
        return dp[amount] > amount ? -1 : dp[amount];
    }
};


62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?
示例:
在这里插入图片描述
输入:m = 3, n = 7
输出:28

其实这题用高中组合数学可以很快的解决,但奈何学的全忘了,看见题目的时候只能想到动态规划,下面说说思路:

假设去往i , j 处有 F(i , j)中方法,由下图可知,

F( i , j) = F( i-1, j ) +  F( i , j-1)

在这里插入图片描述
于是方程就这么写出来了,接下来是注意初值
在这里插入图片描述
为什么初值是这样的呢?因为要到达两条边界上的任何一个点,都只需要一种方法,那就是直线行走,因为题目说明了只能往下或往右,不能往回走
所以只能一路向前,这样在边界上达到任何一点都只需要一种方法

方程写出了,初值也给定了,接下来填满整个dp数组就好了

代码:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m,vector<int>(n,1));//我这里直接用1填满了整个数组,反正也没差
        for(int i = 1;i<m;++i) 
        {
	        for(int j = 1;j<n;++j)  
	        dp[i][j] = dp[i-1][j]+dp[i][j-1];
        }
        return dp[m-1][n-1];
    }
};

如果用组合的算法算的话,直接算出C(m-1, m+n-2)即可


64. 最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。
在这里插入图片描述

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

来源:力扣(LeetCode)

思路:
动态规划,和上一题思路很像,每一个点都有两种方法到达,左边的格子往右走一格,或者上面的格子往下走一格,我们在这两种情况之中选择花费最小的那个,
状态转移方程:

f(nums[i][j]) = min( f(nums[i-1][j]) , f(nums[i][j-1]) )

然后再确定一下初值:
和上一题一样,数组的左边界和上边界的值是唯一的,没有所谓的最优解
在这里插入图片描述
初值也搞定了,最后就是填满dp数组就完事了
,不过这一题完全可以在原数组上操作,可以不用再浪费O(mn)的空间

代码:

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int row = grid.size(),col = grid[0].size();
        int res = INT_MAX;
        for(int i = 1;i<row;++i)grid[i][0] = grid[i-1][0]+grid[i][0];//赋初值
        for(int i = 1;i<col;++i)grid[0][i] = grid[0][i-1]+grid[0][i];//赋初值
        for(int i = 1;i<row;++i)
        {
            for(int j = 1;j<col;++j)
            {
                grid[i][j] = min( grid[i][j]+ grid[i-1][j],grid[i][j]+ grid[i][j-1]);
            }
        }
        return grid[row-1][col-1];
    }
};


1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

来源:力扣(LeetCode)

思路:
用两个字符串构成一个二维数组,然后利用动态规划,从下往上,求出最后的公共子序列

在这里插入图片描述
动态规划最重要的就是确定dp数组的意义和状态方程
先说说dp数组的含义,在这一题里,就我现在的思路来说,dp[i][j],代表的是s1的从0开始到下标为i
(即长度为i+1) 的子字符串 与 s2的从0开始到下标为j (即长度为j+1) 的子字符串的最长公共子序列

由上图的情况来看,dp[1][1]就是"AC"和"AB"的最长公共子序列
这个意义也就是上面一些题解里我所说的 f ()
知道了意义,然后来说说状态方程

dp[i][j]由三个因素决定

dp[i-1][j-1]   dp[i-1][j]   dp[i][j-1]

如上图的dp[1][2],此时两个字符串的对应值相等,都为C,此时的dp[1][2]应为dp[0][1]+1,为什么两个维度都要减一?想想看,维度的意义,在这一题里,维度就代表了对应字符串的子串的长度,当两个对应的字符相等时,最长公共字符串肯定得+1啊,那么为什么是dp[i-1][j-1]?因为可以排除重复字符,两边长度都-1,就能去掉重复字符带来的影响,举个例子abb和bbb
假如我们现在比较的是ab 和 bb ,很明显dp[1][1]为1,比较的是abb 和 bb,dp[2][1]为2,而比较的是abb 和 bbb时dp[2][2]还是2,这里就是重复字符的影响了,因为dp[2][1]和dp[2][2]一样,同理dp[1][2]也有可能和dp[2][2]一样,只有dp[1][1]绝对不会出错,肯定比dp[2][2]小1

以上的是当s1[i] == s[j]时状态方程的详细解释

dp[i][j] == dp[i-1][j-1]+1

当s1[i] != s[j]时

这里就是个选择的问题,既然s1[i] != s2[j],那反正这两个字符串也不能改变最大长度了,就只能让他的值从dp[i-1][j]和dp[i][j-1]之间选一个了,反正要选,不如选大的

dp[i][j] == max(dp[i-1][j],dp[i][j-1]);

最后一步啦,当然是赋初值

初值自然是dp数组的两个边界的值,不过这题的初值还挺不好搞的,如果不改变一下思路,就会变得麻烦起来,假设s1的长度为row,s2的长度为col(这里我默认把s1当y轴)

如果坚持要创建dp的大小为[row][col],初值就麻烦起来了,如果两字符串的开头不一样的话,就可能需要好几步,因为后续的字符里可能会有和开头一样的字符,不能确定哪里是0,哪里是1,具体见下面的代码吧

要像简单一点,我知道有两种方法:这两种方法都需要扩展一下dp数组
(一)
大小为dp[row+1][col+1],然后再s1和s2开头都加上一个相同的字符,什么都行啊,空格啊,数字啊,字母什么的都行
这就方便我们赋初值了,因为麻烦的是就是开头不一样,那我们就让他们都有一个一样的新开头,两字符串开头一样,所以,dp数组的上边界和左边界都为1,不明白的画图看看就知道了

然后算出来最大长度以后-1,就是原字符串的最大长度了
(二)
大小为dp[row+1][col+1],上面的方法是在前面加一个共同的字符,第二个方法就从方程下手,dp[i][j]的意义不再代表s1的长度为i+1的字串 和 s2的长度为j+1的子串,而是长度为i和j,即长度减1了,这样的两边界就都从0开始,因为dp[0][i]和dp[i][0]都不可能会有公共子序列,两者都是空的字符串啊,长度为0

说了两个办法,但其实两个方法的本质都是构建了一个虚假的初值,让本该作为状态方程的基础的初值,通过一个虚假的初值用状态方程计算出来,后面的事情就顺利成章了

代码:(不用辅助)

class Solution {
public:
    int longestCommonSubsequence(string s1, string s2) {
    	int n = s1.size(), m = s2.size();
    	int dp[n][m],i = 0;
		for(i = 0;i<n;++i)/******************************************/
		{
			if(s1[i]==s2[0])break;
			dp[i][0] = 0;    //中间这一大段都是赋初值,很麻烦,还是建议用那两个比较方便的方法
		} 
		for(;i<n;++i)dp[i][0] = 1;
		for(i = 0;i<m;++i)
		{
			if(s1[0]==s2[i])break;
			dp[0][i] = 0;
		} 
		for(;i<m;++i)dp[0][i] = 1;	/****************************************/	
		for(int row = 1;row<n;++row)
		{
			for(int col = 1;col<m;++col)
			{
				if(s1[row]==s2[col])
				dp[row][col] = dp[row-1][col-1]+1;
				else
				dp[row][col] = max(dp[row-1][col],dp[row][col-1]);
			}
		}
		return dp[n-1][m-1];
    }
};

代码:(方法二)

class Solution {
public:
    int longestCommonSubsequence(string s1, string s2) {
    	int n = s1.size(), m = s2.size();
    	int dp[n+1][m+1];
		for(int i =0;i<=n;++i) dp[i][0] = 0;//初值
		for(int i =0;i<=m;++i) dp[0][i] = 0;//初值
		for(int row = 1;row<=n;++row)
		{
			for(int col = 1;col<=m;++col)
			{//填满dp数组
				if(s1[row-1]==s2[col-1])
				dp[row][col] = dp[row-1][col-1]+1;
				else
				dp[row][col] = max(dp[row-1][col],dp[row][col-1]);
			}
		}
		return dp[n][m];
    }
};

还有一题写法很像的,但是写法像,思路却很难想通,这里先放一下代码,思路日后补充,可以发现写法特别像

72. 编辑距离

思路:

dp数组

先说说dp数组的意义吧,在这一题里,dp[i][j]的意义是word1的长度为i+1的子串变换成word2长度为j+1的子串所需的最少步数,这里强调一下
我们不能从全局来看,必须从子问题往上推,这就是动态规划的精髓和本质

然后试着写出状态方程,先举个例子

abc -> abd
dp[0][0] = 0, 即 a -> a 			(不需要做任何事)
dp[0][1] = 0, 即 a -> ab 			(很明显,在a后面插入一个b即可)
dp[0][2] = 0, 即 a -> abd			(很明显,插入b 和 d)

dp[1][0] = 0, 即 ab -> a 			(很明显,删除 b)
dp[1][1] = 0, 即 ab -> ab 			(不需要做任何事)
dp[1][2] = 0, 即 ab -> abd 			(很明显,插入 d)		

dp[2][0] = 0, 即 abc -> a 			(很明显,删除b 和 c)
dp[2][1] = 0, 即 abc -> ab 			(很明显,删除 c)
dp[2][2] = 0, 即 abc -> abd 		(把c替换成d)			

可以发现,在word1里插入一个字符,和在word2里删除一个字符上一样的,对于最终结果来说没差

所以我们默认有三种操作

在word1中插入一个字符
在word2中插入一个字符		
替换word1 或者 word2 中的某一个字符

插入对应的是两者长度不一样的时候,而替换对应的是两者长度一样,即dp[i][j]中,i == j
比如dp[0][0],dp[1][1]这种

到这里就差不多了,状态转移方程就能写出来了

状态转移方程

word1[i] == word2[j]  时    dp[i][j] = dp[i-1][j-1]

当word1[i] != word2[j] 时 ,就必须进行一次插入或替换,我们在三种操作里选最小值

dp[i][j] == min( dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1 )

为什么要加1就不用我说了吧,因为三种操作都是上一步操作所需的最小步数,到了不得不做出选择的时候,意味着必须比以前多一步操作,即+1;

赋初值

方程也出来了,后面就是给定初值了,和上面那题一样,为了方便赋初值,我们给一个虚假的初值,让真正的初值通过假初值,利用状态方程自动算出来,也是一样两种方法,一种是真正的在字符串前面加上两个一样的字符,一种是不加,但是假装自己加了,在判断的时候不用word[i] 和 word[j]
而用word[i-1] 和 word[j-1],不管加不加,最后的答案都一样,不用-1,因为相同的字符不会增加操作数

class Solution {
public:
    int minDistance(string word1, string word2) {
        int row = word1.size(),col = word2.size();
        word1 = " "+ word1;word2 = " "+ word2;
        int dp[row+1][col+1];
        for(int i = 0;i<=row;++i)dp[i][0] = i;
        for(int i = 0;i<=col;++i)dp[0][i] = i;
        for(int i = 1;i<=row;++i)
        {
            for(int j = 1;j<=col;++j)
            {
                if(word1[i] == word2[j])//改这里就可以了
                dp[i][j] = dp[i-1][j-1];
                else
                dp[i][j] = min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
            }
        }
        return dp[row][col];
    }
};


300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

来源:力扣(LeetCode)

思路:这一题我一开始就是想不通,脑子卡住了,于是放弃,然后过了两天又回去看了一看,终于理解了(这里说的是最朴素的O(n^2)的动态规划的方法,没有优化

dp数组的意义

dp数组的意义是什么呢?dp[i]就代表末尾是nums[i]的最长递增子序列,注意这里的末尾是真真正正的末尾,即这个子序列的最后一个元素就是nums[i]

状态方程

这题的时间复杂度为O(n^2)的原因就是因为状态方程与前面的许多状态有关系

我们看这题要从另外的角度去看,不能只盯着递增这一个条件,我们求dp[i]的时候要从头遍历nums,看看i这个下标之前有多少个值比nums[i]小,毕竟只有小于nums[i],递增序列才有可能增长

但是可能会出现 1 3 2 1 5这种情况 , 那怎么判断呢?当我们遍历到5并且与3进行比较的时候,不会出问题,但我们与2进行比较的时候,能发现已经有三个数小于5了,但是不要紧,我们不是计数,而是比较子串1 3 2的递增序列长度(dp[2])+1的值与dp[i]本身,这样的条件就可以避免这种驼峰情况,因为dp[]是从下往上一步步构建的,每次都这样赋值,所以dp[i]的值总是正确的,就算2小于5又怎样,dp[2]还不是等于dp[1]?所以dp[i]不会因为驼峰的情况出错,+1又是为什么呢?因为对于被比较的dp[L]来说,后面有一个满足递增的数,那序列长度肯定得+1啊

这里用文字来说明还挺绕的,但是做动画我又嫌麻烦,直接放一个leetcode的官方题解链接吧,里面的ppt做的很好👉很便于理解

初值

就算整个数组都不递增,最少长度也为1

如果还是看不懂,就看看代码和注释吧,不难理解的

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int size = nums.size(),res = 0;
        if(size <= 1)return size;//特殊情况
        vector<int>dp(size,1);//建立dp数组并初始化
        
        for(int R = 0;R<size;++R)//一步步的扩张右边界
        {
            for(int L = 0;L<=R;++L)//每次L都从0开始遍历到右边界
            {
				//一旦nums[L]<nums[R],就进行比较并赋值            
                if(nums[R]>nums[L])dp[R] = max(dp[R],dp[L]+1);
            }
            //要取dp数组中的最大值,因为最后一个不一定是最大的
            //不明白的就多看看上面dp数组的意义
            res = max(res,dp[R]);
        }
        return res;
    }
};


剑指 Offer 42. 连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。
示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6

解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

这一题明明这么简单,以前还做过,但是再次复习的时候还是会花一点时间来理解,还是没掌握啊

思路:

两个变量 :sum和res
sum来记录连续子数组的和,一旦发现sum+nums[i] < nums[i],我们就重置sum,因为前面的和 加起来都比不过nums[i],那要你们干嘛?放弃,直接从nums[i]重新计数吧

if(nums[i] > sum+nums[i])
sum = nums[i];
else 
sum += nums[i]

写简洁一点就是这样:
sum = max(sum + nums[i], nums[i]);

然后用res更新最大sum就完事了

代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int res INT_MIN , sum = 0;
        for(int num : nums)//这里用了for的另一种写法,能达到目的就行,没啥区别
        {
            sum = max(sum + num , num);
            res = max(res  , sum);      
        }
        return res;
    }
};


139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"

来源:力扣(LeetCode)

思路:

动态规划

dp数组的含义:

dp[i]代表原字符串到i位时能不能从字典中切割

状态方程:

这一题的状态方程感觉不是很好用式子表达啊,或许有点绕

具体思路就是:默认从第一个字符开始切割,遍历字典,遇见能切出来的就在对应的dp数组处赋值true,表示能切割

然后下一次从能切割的(即dp[i] == true)处继续往后搜索,再次遍历字典,从i处找出所有能切割的地方,给dp赋值,然后继续…

当我们遍历完字符串后,只要看dp数组的最后一位即可,如果是true,就代表整个字符串都可被字典切割,

因为下一次搜索需要从上一次dp[i] == true处开始。所以其确实是动态规划,从下往上,但是确实不太好用公式来表达

初值:

如果给定dp数组size是字符串size+1,就很方便后续的赋值,
但是不赋初值肯定也是可以的,就是会麻烦一点
这种细节真的烦,明明思路都明白了,还得纠结细节

有初值:

class Solution {
public:
    bool wordBreak(string& s, vector<string>& wordDict) {
        vector<bool>dp(s.size()+1,false);//长度+1
        dp[0] = true;//初值
        for(int i = 0;i<s.size();++i)
        {
            if(dp[i] == false) continue;
            for(auto&word : wordDict)
            {
                if(i+word.size()<=s.size() && s.substr(i,word.size()) == word)
                dp[i+word.size()] = true;
            }
        }
        return dp[s.size()];
    }
};

无初值:

class Solution {
public:
    bool wordBreak(string& s, vector<string>& wordDict) {
        vector<bool>dp(s.size(),false);
        for(int i = 0;i<s.size();++i)
        {
            if(i!=0 && dp[i-1] == false) continue;
            for(auto&word : wordDict)
            {
                if(i+word.size()<=s.size() && s.substr(i,word.size()) == word)
                dp[i+word.size()-1] = true;
            }
        }
        return dp[s.size()-1];
    }
};


今天终于干掉了困扰我很久的正则表达式匹配,这题确实难啊,看了各路题解才搞懂

10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = “aa” p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
来源:力扣(LeetCode)

先说个搞笑的
我一开始始终不明白.*为什么能匹配所有字符串,
那时我想的是:这玩意不是只能匹配重复字符串吗,比如aaaaaaaa,bbbbbbb这种

毕竟只有一个. 嘛,只能代表某个字符而已吧,而后面的*也不过是在重复这个字符而已

后来终于想通

.*其实是代表不定量个'.' 不是先用'.' 取代替某个字符后再用'*'重复,而是直接用'*'重复'.'
就像这样: .....................
这样的话确实就可以匹配任意长度的任意字符串了

说说动态规划的思路吧:

dp数组的意义

首先是dp数组的意义,我默认用y轴代表字符串s,x轴代表匹配串p
dp是一个bool数组,dp[i][j] == true时,

代表能用p[0,j]子串 成功 匹配s[0,i]子串
反之就代表不能匹配
所以最后只要看dp[s.size()][p.size()]的值即可知道两串是否能匹配

状态转移方程

所有动态规划题里,转台转移方程都是最重要的,只要知道了方程,其他的都是小事

首先来个字符相等的情况:

s[i] == p[j]
这种情况,两者字符都相等了,所以当前位置肯定没有异常,不过我们还是不能断然下定结论
这个位置拥有成功匹配的可能性,但是具体能不能成功匹配(true还是false)
还得看前面的状态dp[i-1][j-1],如果dp[i-1][j-1]不成,那么dp[i][j]也不成
于是可得:dp[i][j] = dp[i-1][j-1]

p[j] == '.'    
同上  

上面的是当前字符相等时的情况,但是就算不相等,我们还是有机会的,因为有*的存在

字符不一样的情况下,怎么才能成功匹配呢?必须依靠*的效果,因为*的效果是复制若干个前面字符
所以当前位置必须是*,即p[j]=='*'
然后我们看看'*'前面的字符p[j-1]——如果它是'.'或者说它就等于s[i]
那这时我们通过'*'的复制功能还是能使这个位置的两个字符相等
但是和上面的情况一样,仅仅是有匹配成功的资格而已,具体能不能匹配,也得结合前面的状态
dp[i-1][j] 和 dp[i][j-2]
dp[i][j-2]其实同时代表了dp[i][j-1],
dp[i][j-1]——————————
前面说了,当前位置有资格,并且当前位置是*,那如果我们把*当作复制1次的情况时,就可以不需要这个*了
反正没有这个*,我们也能与他前面的字符相等,所以只要dp[i][j-1]为true,dp[i][j]也为true
dp[i][j-2]——————————
这个是复制0次的情况,即当前*和它前面的字符都消失了,没了,所以只要dp[i][j-2]为true,
我们就可以通过在当前位置复制0次,达到匹配的目的
dp[i-1][j]——————————
这个就比较特殊了,前面说过了,这个位置有资格,那假如s[i-1]就已经能匹配到这个位置了呢?
就说明s[i]==s[i-1],	那两者都相等了,我有*在手,再复制一次不就完事了
这个s[i] == s[i-1]是dp[i-1][j]为true的条件,我们不用管,
如果不相等的话,dp[i-1][j]是不会为true的,自然不会影响我们

上面的是p[j-1]通过各种手段能与s[i]相等的情况
下面的是字符p[j-1]怎么样都不可能与s[i]相等了,这时我们就得抛弃p[j-1]了
即*复制0次,这时起决定作用的就是dp[i][j-2],因为此时就相当于没有*与它前面的字符了
很自然的就dp[i][j] = dp[i][j-2]

这一题最最最难的地方就是上面的状态了,因为*的不定量复制操作真的太复杂了,特别难确定状态

最后来一个简单的状态,即字符不相等,而且p[j]也不为*,这种情况我们终于能确定状态了,就是fasle

初值

作为一个dp数组,当然得有初值,不然怎么算出后面的状态

这种二维dp数组,一般初值都是给边界赋值,而且都有些像技巧可以简化赋值

再说一句,因为dp数组的长度变长了所以判断两串的字符时,就不能直接用

普通赋值:

当然赋值不能脱离基本法,即状态转移方程,所以才会那么麻烦啊,因为方程本身就不简单!!!烦死了
普通的赋值,这题就算普通的赋值,dp数组的长度都得+1,因为就算是空字符串,也能匹配…
因为’ * ‘可以复制0次,就代表空字符串了
首先得知道,’ * ‘不可能单独出现
所以对于两边界的初值,dp[0][0]一定为true,空串匹配空串,没啥问题
对于左边界但是只要s的长度增加,就不可能与长度为0的p串匹配了
对于上边界,p串就算长度不为0,也还是能匹配空串的,就麻烦在这啊
怎么才能匹配空串呢,很明显,还是靠’ * ‘复制0次,并且’ * '不可能单独出现,所以只有出现

a*b*c*d*

这种情况时,才能将’ * '所在的位置设为true,一旦中间被打乱了,

例如a*b*cc*

后续的’ * '就不在能匹配空串了
这里先设置整个dp数组都为fasle,就可以利用循环快速的完成赋初值

代码:

        bool dp[sLen+1][pLen+1];
        fill_n(&dp[0][0],(sLen+1)*(pLen+1),false);

        dp[0][0] = true;
        for (int i = 1; i < pLen + 1; i++)
        {
            if (p[i - 1] == '*')
            {
                dp[0][i] = dp[0][i - 2];
            }
        }

一般的编译器不允许用变量构建数组,所以可以改成

vector<vector<bool>>dp (sLen, vector<bool>(pLen, false));

完整代码:

class Solution {
public:
    bool isMatch(string s, string p) {
        int sLen = s.size(), pLen = p.size();
        bool dp[sLen+1][pLen+1];
        fill_n(&dp[0][0],(sLen+1)*(pLen+1),false);
        dp[0][0] = true;
        for (int i = 1; i < pLen + 1; i++)//赋初值
        {
            if (p[i - 1] == '*')
            {
                dp[0][i] = dp[0][i - 2];
            }
        }
        for(int i = 1;i<=sLen;++i)
        {
            for(int j = 1;j<=pLen;++j)
            {
                if(s[i-1] == p[j-1] || p[j-1]=='.')
                {
                    dp[i][j] = dp[i-1][j-1];
                }
                else if(p[j-1] == '*')
                {
                    if(s[i-1]==p[j-2] || p[j-2] == '.')
                    {
                        dp[i][j] = dp[i][j-1] || dp[i-1][j] || dp[i][j-2];
                    }
                    else
                    {
                        dp[i][j] = dp[i][j-2];
                    }
                }/*
                else
                {        这里因为建数组的时候就默认为false了,所以可以省略
                	dp[i][j] = false;
                }*/
            }
        }
        return dp[sLen][pLen];
    }
};

简化赋值:

老办法,两字符串的动态规划,很多时候都可以再字符串开头插入一个完全不相关的开头,利用这个开头和状态方程计算出本来的初值以及后续,因为这个加开头本身也满足状态方程,所以才能用它计算后续的结果

对于这一题,麻烦就麻烦在就算p串长度不为0,它依旧能匹配空字符串,那我就让你永远不可能匹配空字符串,在p串开头加一个空格,当然不是空格也可以,这样就不可能出现

a*b*c*d*

这种麻烦东西了,因为都被我在开头处打乱了(题目有说’ * '不会单独出现),哈哈

然后对应的在s串开头也加一个

这样,两边界的值就全是false了,除了dp[0][0]依旧是true,

完整代码:

可以发现核心代码完全没动过的,只是改了赋初值的方法而已

class Solution {
public:
    bool isMatch(string s, string p) {
        p = " "+p;//
        s = " "+s;//
        int sLen = s.size(), pLen = p.size();
        bool dp[sLen+1][pLen+1];
        fill_n(&dp[0][0],(sLen+1)*(pLen+1),false);
        dp[0][0] = true;
        for(int i = 1;i<=sLen;++i)
        {
            for(int j = 1;j<=pLen;++j)
            {
                if(s[i-1] == p[j-1] || p[j-1]=='.')
                {
                    dp[i][j] = dp[i-1][j-1];
                }
                else if(p[j-1] == '*')
                {
                    if(s[i-1]==p[j-2] || p[j-2] == '.')
                    {
                        dp[i][j] = dp[i][j-1] || dp[i][j-2] || dp[i-1][j];
                    }
                    else
                    {
                        dp[i][j] = dp[i][j-2];
                    }
                }
            }
        }
        return dp[sLen][pLen];
    }
};

写完以后才会觉得这题原来这么简单,唉


718. 最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。

这题也不难,动态规划

dp数组的意义

这一题dp数组的意义,在我看来,其实是判断对应的字符是否相等,>0就是相等, ==0就是不相等

只是在两字符相等时候顺便求一下子数组的长度而已,那么怎么顺便呢?很简单,看看前面的两个字符是否相等,相等就在它的长度上+1,如果前面两字符不等,就为1

刚好前面我们说过了,不相等就为0,于是两种情况就统一了:可简化为前两字符的长度+1

状态转移方程

如果nums1[i] == nums2[j], dp[i][j] = dp[i-1][j-1]+1, 上面有解释
如果不等,dp[i][j] = 0

初值

二维dp,照样给个假值,用它来算原本的初值,于是像字符串那样在前面插一个不相关的数字,正好题目说了数组中的数字>=0,那我就插一个-1,一个-2吧,这样初值(即dp数组两边界的值就全为0了)然后填满dp的时候就从1开始,因为0是初值啊
代码:

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        nums1.insert(nums1.begin(), -1);
        nums2.insert(nums2.begin(), -2);
        int size1 = nums1.size(), size2 = nums2.size();
        vector<vector<int>>dp(size1,vector<int>(size2,0));
        //int dp[size1][size2];
        //fill_n(&dp[0][0], (size1)*(size2), 0);
        int res = 0;
        for(int i = 1;i<size1;++i)
        {
            for(int j = 1;j<size2;++j)
            {
                if(nums1[i]==nums2[j])
                {
                    dp[i][j] = dp[i-1][j-1]+1;
                }
                res = max(res,dp[i][j]);
            }
        }
        return res;
    }
};

当然也可以假装这么做,然后在判断字符是否相等时

就不能用nums1[i]==nums1[j],因为dp数组的大小和两数组的长度已经不对应了,下标得-1
	 即nums1[i-1]==nums1[j-1]

代码

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int size1 = nums1.size(), size2 = nums2.size();
        //int dp[size1+1][size2+1];
        //fill_n(&dp[0][0], (size1+1)*(size2+1), 0);
        vector<vector<int>>dp(size1+1,vector<int>(size2+1,0));
        int res = 0;
        for(int i = 1;i<=size1;++i)
        {
            for(int j = 1;j<=size2;++j)
            {
                if(nums1[i-1]==nums2[j-1])
                {
                    dp[i][j] = dp[i-1][j-1]+1;
                }/*********因为初始化就是0,所以可以省略这一步
                else
                {
                	dp[i][j] = 0;
                }*/
                res = max(res,dp[i][j]);
            }
        }
        return res;
    }
};


368. 最大整除子集

给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足:
answer[i] % answer[j] == 0 ,或
answer[j] % answer[i] == 0
如果存在多个有效解子集,返回其中任何一个均可。

示例 1:

输入:nums = [1,2,3]
输出:[1,2]
解释:[1,3] 也会被视为正确答案。

来源:力扣(LeetCode)

这题思路很简单,只要把它当成最长上升子序列来做就行了

维护一个窗口,右边界往右扩张
而左边界每次都从头开始,判断当前数字是否满足nums[R]%nums[L]==0,满足就更新最大长度

(这里注意一下,因为min对max取余也是0,所以我们先排序,保证nums[R]>nums[L])

如果是求最大长度。就这么简单,可惜题目求得是子数组,麻烦死了,看了题解,发现大佬们都是用两个数组,一个记录长度,一个记录前一个数字的下标,这里前一个数字是指子数组的前一个数字,我们要用这个往前推出完整的子子数组

如果不懂得就去看看最长上升子序列把,核心思路就是这个,剩下的就是表面功夫而已

代码:

class Solution {
public:
    vector<int> largestDivisibleSubset(vector<int>& nums) {
        sort(nums.begin(),nums.end());//排序
        int size = nums.size();
        vector<int>dpLen(size);//记录长度
        vector<int>dpIdx(size);//记录子数组中前一个数字的下标
        for(int R = 0;R<size;++R)
        {
//先给个默认值,长度为1,下标为当前下标,
//表示默认长度的为1的时候,前面没有数字,因此只能从自己的下标往后推        
            dpLen[R] = 1;dpIdx[R] = R;
            for(int L = 0;L<R;++L)
            {
                if(nums[R]%nums[L]==0)
                {
                    if(dpLen[L]+1>dpLen[R])//因为还要更新下标,所以就用if,不用max了
                    {
                        dpLen[R] = dpLen[L]+1;
                        dpIdx[R] = L;
                    }
                }
            }
        }
//返回的是迭代器,于是直接减去begin 得到最长子数组的末尾在数组中的下标     
        int idx = max_element(dpLen.begin(),dpLen.end())-dpLen.begin();
        //因为也要算下标,所以为了不浪费时间,就先取得下标,再用下标取值
        int maxlen = dpLen[idx];//用下标得到数值,
        vector<int>res;
        while(res.size()!=maxlen)//直到长度达到最大长度
        {
            res.push_back(nums[idx]);//先把末尾填进去
            idx = dpIdx[idx];//然后idx开始往前回溯
        }
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

timathy33

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

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

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

打赏作者

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

抵扣说明:

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

余额充值