动态规划相关算法


动态规划算法


前言

1. 动态规划概念

动态规划是运筹学中用于求解决策过程中的最优化数学方法。每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

2. 基本思想策略

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

3. 动态规划适用的情况

1、重叠子问题均是最优解
2、子问题不影响覆盖问题的后续过程,即子问题可作为单独最优解问题。
3、子问题会被覆盖问题多次使用。

关于重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

4. 个人理解

动态规划求解的是存在重叠子问题的最优解,主打利用空间换时间,较大的覆盖问题可以利用其一个或多个子问题的最优解来求取(状态转移方程),存储这些子问题的最优解的空间即为DP数组。

5. 解决动态规划问题的步骤

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定状态转移方程
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

一、简单DP

509.斐波那契数
70.爬楼梯
746.使用最小花费爬楼梯
62.不同路径
63.不同路径II
343.整数拆分 ★★★
96.不同的二叉搜索树

同上整数拆分,先遍历从小到大的数组,再依次遍历根节点为不同值的情况并进行相加。

二、背包问题

背包问题
From Carl(缺少细节理解) + 个人总结

1. 01背包(求取价值、存放方法数、存放个数(value=1))

void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

求解固定容量X时背包的的价值value:

416.分割等和子集
dp数组:
dp[i]表示当前背包容量为i时最大价值
状态转移方程:
max(放入当前物品,不放入当前物品)的value
1049.最后一块石头的重量II
dp数组:
dp[i]表示背包容量为i时可以放入的最大价值
状态转移方程:
dp[j] = max(dp[j] , dp[j-nums[i]+value[i]);

装满容量X背包,有几种方法(组合数):

背包解决排列组合问题!
不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]种方法。

那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,
已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 dp[5]。
已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 dp[5]。
已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 dp[5]
已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 dp[5]
已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 dp[5]
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
背包解决组合问题的常见状态转移方程如下:

for (int i = 0; i < nums.size(); i++) {
	for (int j = bagSize; j >= nums[i]; j--) {
	    dp[j] += dp[j - nums[i]];
	}
}
494.目标和

求组和类问题,回溯与DP都可以解决,回溯的时间复杂度一般 2 n 2^n 2n,DP为 m ∗ n m*n mn

dp[i]表示当前容量为i时,存放物品组合总和
//初始化,容量为0时,方法为1(什么都不取)
dp[0]=1;
//组合类状态转移方程:不取当前物品组合总和与取了当前物品组合总和
dp[j] += dp[j - nums[i]];

装满容量要求为m与n的二维背包,最多能放多少物品:

474.一和零★★★
vector<vector<int>>dp(m + 1, vector<int> (n + 1, 0));
//遍历顺序:先遍历物品,再遍历背包容量需求m,再遍历背包容量需求n
for (string str : strs) { // 遍历物品
    int oneNum = 0, zeroNum = 0;
    for (char c : str) {
        if (c == '0') zeroNum++;
        else oneNum++;
    }
    for (int i = m; i >= zeroNum; i--) { // 遍历背包容量需求m
        for (int j = n; j >= oneNum; j--) { //遍历背包容量需求n
            dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
        }
    }
}

2. 完全背包

完全背包理论基础

2.1 01背包->完全背包:演化过程

虽然状态转移方程相同dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);,但是含义完全不同!!!

//01背包的遍历
for(int i = 0; i < weight.size(); i++) { // 遍历物品
	for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
		dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]);//二维数组
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);//转为滚动数组
	}
}
//物品数量无限后的完全背包-[朴素]
for(int i = 0; i < weight.size(); i++) { // 遍历物品
	for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
		for(int k = 0; k<=j/weight[i] ;k++){	//遍历物品数量
			dp[j] = max(dp[j], dp[j - k*weight[i]] + k*value[i]);
		}
	}
}
//物品数量无限后的完全背包-[改进]
for(int i = 0; i < weight.size(); i++) { // 遍历物品
	for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
		dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]]+value[i]);//二维数组
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);//转为滚动数组
	}
}

注意

//背包问题中当前物品不取的状态:
dp[i-1][j]
//01背包的状态转移方程:
//当前物品选取时状态:当前物品数量为0时,背包容量正好放下第1个当前物品重量的value
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);

//完全背包的状态转移方程:
//当前物品选取时状态:当前物品数量为k-1时,背包容量正好放下第k个当前物品重量的value
dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i]);

装满容量X背包,有几种组合方法:

518.零钱兑换II
  • 遍历顺序+最后一个物品有没有进背包
    遍历顺序:先遍历物品,后遍历背包

装满容量X背包,有几种序列方法:

  • 遍历顺序+最后一个背包内的物品是哪个
状态转移方程:
简单来说:求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
复杂来说:
 举个例子,nums = [1, 2, 3],target = 35.
假设用123拼凑出35的总组合个数为y。我们可以考虑三种情况:
(1)有效组合的末尾数字为1,这类组合的个数为 x1。我们把所有该类组合的末尾1去掉,那么不难发现,我们找到了一个子问题,x1即为在[123]中凑出35 - 1 = 34的总组合个数。因为我如果得到了和为34的所有组合,我只要在所有组合的最后面,拼接一个1,就得到了和为35且最后一个数字为1的组合个数了。
(2)有效组合的末尾数字为2,这类组合的个数为 x2。我们把所有该类组合的末尾2去掉,那么不难发现,我们找到了一个子问题,x2即为在[123]中凑出35 - 2 = 33的总组合个数。因为我如果得到了和为33的所有组合,我只要在所有组合的最后面,拼接一个2,就得到了和为35且最后一个数字为2的组合个数了。
(3)有效组合的末尾数字为3,这类组合的个数为 x3。我们把所有该类组合的末尾3去掉,那么不难发现,我们找到了一个子问题,x3即为在[123]中凑出35 - 3 = 32的总组合个数。因为我如果得到了和为32的所有组合,我只要在所有组合的最后面,拼接一个3,就得到了和为35且最后一个数字为3的组合个数了。
这样就简单了,y = x1 + x2 + x3。而x1,x2,x3又可以用同样的办法从子问题得到。状态转移方程get!

dp[i] += dp[i - nums[j]];
//dp[i]为容量为i时,背包不包含当前物品的序列组合的个数。
//dp[i-nums[j]]为容量为i-nums[j]时即最后一个物品为当前物品,序列组合的个数。

遍历顺序:

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品
377.组合总和Ⅳ★★★★★
70.爬楼梯

装满容量X的背包,最少能放多少物品:

322.零钱兑换
279.完全平方数

在约束条件下,能否装满容量X的背包:

139.单词拆分★★★★★

3. 多重背包

多重背包问题:有N种物品和一个容量为V 的背包。第 i 种物品最多有 Mi 件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

多重背包和01背包是非常像,每件物品最多有 Mi 件可用,把 Mi 件摊开,其实就是一个01背包问题了。

//法一
for(int i = 0; i < weight.size(); i++) { // 遍历物品
	for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
	}
}
//法二
for(int i = 0; i < weight.size(); i++) { // 遍历物品
	for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
		// 以上为01背包,然后加一个遍历个数
		for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
			dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
		}
	}
}

三、常见动态规划系列问题

1. 间隔选取求和

198.打家劫舍:一排房屋 间隔偷
213.打家劫舍II:环形房屋 间隔偷
337.打家劫舍III:二叉树房屋 间隔偷

一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。

2. 股票买卖

121.买卖股票的最佳时机

122.买卖股票的最佳时机II

两种状态:

  1. 持有股票
    买入
    已买入
  2. 不持有股票
    卖出
    已卖出

123.买卖股票的最佳时机III

五种状态:

  1. 没有操作
  2. 第一次买入
  3. 第一次卖出
  4. 第二次买入
  5. 第二次卖出

188.买卖股票的最佳时机IV 同III

五种状态:

  1. 没有操作
  2. 第一次买入
  3. 第一次卖出
  4. 第二次买入
  5. 第二次卖出

309.最佳买卖股票时机含冷冻期

五种状态:
1.持有股票
1.1 买入
1.2 已买入

2.不持有股票
2.1 非冷冻期
2.1.1 卖出的当前
2.1.2 卖出的第三天及以后
2.2 冷冻期

714.买卖股票的最佳时机含手续费 同II

两种状态:

  1. 持有股票
    买入
    已买入
  2. 不持有股票
    卖出
    已卖出

3. 子序列相关

300.最长递增子序列LIS

法一:动态规划
dp[i]的定义:
dp[i]表示从前向后以nums[i]结尾最长上升子序列(一定要包含nums[i])的长度。
注意:dp[i]一定要是以nums[i]作为末尾,也就是最大值的上升子序列,只有这样,nums[i+1],nums[i+2],nums[i+3]…才可以通过与nums[i]比较继承i之前的上升子序列的数据。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        int result = 0;
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
            }
            if (dp[i] > result) result = dp[i]; // 取长的子序列
        }
        return result;
    }
};

法二:动态规划+二分查找
优化法一的内存循环,将内存循环改为二分查找,时间复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn)。但是二分查找前提是有序序列,所以增加一个数组tails[]记录已有上升子序列。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        //创建数组存储递增子序列
        vector<int> tails(nums.size(),0);
        tails[0] = nums[0];
        int index=0;	//记录上升子序列长度
        for(int i=1 ; i<nums.size() ; i++){
            //通过遍历nums与当前递增子序列进行比较(添加或替换)
            if(nums[i]>tails[index]){  		 //大于则添加
                tails[++index] = nums[i];
            }
            else if(nums[i]==tails[index]){  //等于则跳过
                continue;
            }
            else{   						 //小于则替换
                //找到tails中第一个大于或等于nums[i]的元素并替换
                //二分查找
                int left = 0, right = index;
                while(left<=right){
                    int mid = (left+right)/2;
                    if(nums[i]>tails[mid]){
                        left = mid+1;
                    }else{
                        right = mid-1;
                    }
                }
                tails[left] = nums[i];
            }
        }
        return index+1;
    }
};

674.最长连续递增序列

最长连续递增序列与LIS区别在于:
连续递增序列只需要将nums[i]与nums[i-1]比较;
递增子序列需要

718.最长重复子数组

最长重复子数组
状态方程:

//dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
dp[i][j] = dp[i - 1][j - 1] + 1; 
//转滚动数组
dp[j] = dp[j - 1] + 1;

LCS最长公共子序列

1143.最长公共子序列LCS问题

区别在于这里不要求是连续的了,但要有相对顺序
LCS
确定递推公式
主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同

  • 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
  • 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。
int longestCommonSubsequence(string text1, string text2) {
	vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
	string s;
	for (int i = 1; i <= text1.size(); i++) {
		for (int j = 1; j <= text2.size(); j++) {
			if (text1[i - 1] == text2[j - 1]) {
				s += text1[i - 1];
				dp[i][j] = dp[i - 1][j - 1] + 1;
			}
			else {
				dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
			}
		}
	}
	cout << s << endl;//输出一个最长子序列
	return dp[text1.size()][text2.size()];
}
1035.不相交的线LCS问题

不相交的线,等于子序列问题LCS
本质还是求取最长公共子序列!

583.两个字符串的删除操作LCS问题

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
本质还是求取两字符串的最长公共子序列!

72.编辑距离(转变字符串) ★★★★★

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

dp[i][j]//表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。

注意状态数组的初始化
bjjl

if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])
    增 
    删
    换

子序列剩余问题

392.判断子序列

双指针(极优)

115.不同的子序列★★★ 腾讯后端

题目:给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
不同子序列
注意初始化问题,初始化时由状态转移矩阵dp[]的含义进行初始化。

回文子串、子序列【区间动态规划[i,j]】

5.最长回文子串★★★★★

字符串中最长的回文子串
回文子串
由于dp[i][j]需要用到dp[i+1][j-1]的结果进行分析进行分析,所以i从最大值开始递减,j从(j-1>=i+1)处开始递增。

string longestPalindrome(string s) {
	// dp[i][j] 表示 s[i..j] 是否是回文串
	vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
	int maxLen = 1;//记录长度
	int begin = 0; //记录首位
	for (int i = s.size() - 1; i >= 0; i--){
		for (int j = i; j < s.size(); j++){
			//左右两侧相同
			if (s[i] == s[j]){
				if (j - i + 1 <= 2){	//短串
					dp[i][j] = true;
					if (j - i + 1 > maxLen) {
						begin = i;
						maxLen = j - i + 1;
					}
				}
				else{	//长串
					if (dp[i + 1][j - 1]){	//判断内串是否为回文子串
						dp[i][j] = true;
						if (j - i + 1 > maxLen) {
							begin = i;
							maxLen = j - i + 1;
						}
					}
				}
			}
		}
	}
	return s.substr(begin, maxLen);
}
647.回文子串

返回字符串中 回文子串 的数目。同上,最长回文子串

int countSubstrings(string s) {
	vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
	int result = 0;
	for (int i = s.size() - 1; i >= 0; i--) {  // 
		for (int j = i; j < s.size(); j++) {
			if (s[i] == s[j]) {
				if (j - i <= 1) { // 短串
					result++;
					dp[i][j] = true;
				}
				else if (dp[i + 1][j - 1]) { // 长串
					result++;
					dp[i][j] = true;
				}
			}
		}
	}
	return result;
}
516.最长回文子序列 ★★★★★

字符串中最长的 回文子序列长度

int longestPalindromeSubseq(string s) {
	//dp[i][j]为[i,j]内最长回文子序列的长度,子序列不需要考虑内部具体情况
    vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
    for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
    for (int i = s.size() - 1; i >= 0; i--) {
        for (int j = i + 1; j < s.size(); j++) {
            if (s[i] == s[j]) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][s.size() - 1];
}

Hot 100残渣

10.正则表达式匹配 ★★

关于正则表达式,用于查找匹配
状态:dp[i][j]表示s的前i个能否被p的前j个匹配

转移方程:
1 s[i]==p[j]: dp[i][j]=dp[i-1][j-1];
2 p[j]=='.'	: dp[i][j]=dp[i-1][j-1];
3 p[j]=='*' : 分情况讨论
* 的含义是 匹配零个或多个前面的那一个元素,所以要考虑他前面的元素 p[j-1]。p[j-1]可以匹配>=0* 跟着前一个字符走,前一个能匹配上 s[i]* 才能有用,前一个都不能匹配上 s[i]* 也无能为力,只能让前一个字符消失,也就是匹配 00 次前一个字符。
3.1 p[j-1] != s[i] : dp[i][j] = dp[i][j-2]
前一个字符p[j-1]不匹配s[i],此时dp[i][j]的状态由前前个字符p[i-2]与s[i]匹配的结果决定
3.2 p[j-1] == s[i] or p[j-1] == "."3.2.1 dp[i][j] = dp[i-1][j] // 多个字符匹配的情况	
	3.2.2 dp[i][j] = dp[i][j-1] // 单个字符匹配的情况
	3.2.3 dp[i][j] = dp[i][j-2] // 没有匹配的情况	
bool isMatch(string s, string p) {
	s = " " + s;//防止该案例:""\n"c*"
	p = " " + p;
	int m = s.size(), n = p.size();
	vector<vector<bool>>dp(m+1,vector<bool>(n+1,false));
	dp[0][0] = true;
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= n; j++) {
			if (s[i - 1] == p[j - 1] || p[j - 1] == '.') {  //匹配
				dp[i][j] = dp[i - 1][j - 1];
			}
			else if (p[j - 1] == '*') {   //讨论p[j-1]与s[i]
				if (s[i - 1] != p[j - 2] && p[j - 2] != '.')
					dp[i][j] = dp[i][j - 2];
				else {
					dp[i][j] = dp[i][j - 1] || dp[i][j - 2] || dp[i - 1][j];

				}
			}
		}
	}
	return dp[m][n];
}
32.最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
方法1:动态规划
dongtaiu

  1. s [ i ] = ′ ) ′ s[i]=')' s[i]=) s [ i − 1 ] = ′ ( ′ s[i−1]='(' s[i1]=(,也就是字符串形如 “……()”“……()”,推出:
    d p [ i ] = d p [ i − 2 ] + 2 dp[i]=dp[i−2]+2 dp[i]=dp[i2]+2
  2. s [ i ] = ′ ) ′ s[i]=')' s[i]=) s [ i − 1 ] = ′ ) ′ s[i−1]=')' s[i1]=),也就是字符串形如 “……))”“……))”如上图,推出:
    i f s [ i − d p [ i − 1 ] − 1 ] = ‘ ( ’ if s[i−dp[i−1]−1]=‘(’ ifs[idp[i1]1]=(
    d p [ i ] = d p [ i − 1 ] + d p [ i − d p [ i − 1 ] − 2 ] + 2 ; dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2; dp[i]=dp[i1]+dp[idp[i1]2]+2;
    上图dp[7]=‘)’,dp[6]=‘)’ , 此时dp[7-1-dp[6]]=dp[4]=‘(’ , 所以dp[7]=dp[6]+2+dp[3];
85.最大矩形 ★★★★【Amazon+字节】

两方法:动态规划、栈。
方法一:动态规划
221. 最大正方形演变而来。
最大正方形
最大正方形中用 d p [ i ] [ j ] dp[i][j] dp[i][j]来表示,以 i , j i,j i,j为右下角元素的正方形。

状态转移:

  • 如果该位置的值是0,则 p ( i , j ) = 0 p(i,j)=0 p(i,j)=0,因为当前位置不可能在由 1组成的正方形中;
  • 如果该位置的值是1,则 d p ( i , j ) dp(i,j) dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 d p dp dp值决定。具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 1,状态转移方程如下:
    d p ( i , j ) = m i n ( d p ( i − 1 , j ) , d p ( i − 1 , j − 1 ) , d p ( i , j − 1 ) ) + 1 dp(i, j)=min(dp(i−1, j), dp(i−1, j−1), dp(i, j−1))+1 dp(i,j)=min(dp(i1,j),dp(i1,j1),dp(i,j1))+1

演变为求取最大矩形时,使用正方形的状态转移方程已经不能满足。
最大矩形
建立辅助矩阵: r e s : ( r i g h t [ j ] − l e f t [ j ] ) ∗ h e i g h t [ j ] res:(right[j]-left[j])*height[j] res(right[j]left[j])height[j]
h e i g h t [ ] height[] height[]:从上到下的高度。
10100 1 0 1 0 0 10100
20211 2 0 2 1 1 20211
31322 3 1 3 2 2 31322
40030 4 0 0 3 0 40030
l e f t [ ] left[] left[]:从左向右,出现连续‘1’的string的第一个坐标。
00200 0 0 2 0 0 00200
00222 0 0 2 2 2 00222
00000 0 0 0 0 0 00000
00030 0 0 0 3 0 00030
r i g h t [ ] right[] right[]:从右向左,出现连续‘1’的string的最后一个坐标。
15355 1 5 3 5 5 15355
15355 1 5 3 5 5 15355
15355 1 5 3 5 5 15355
15545 1 5 5 4 5 15545

int maximalRectangle(vector<vector<char>>& matrix) {
	int result = 0;
	int m = matrix.size(), n = matrix[0].size();
	vector<int>height(n, 0);
	vector<int>left(n, 0);
	vector<int>right(n, n);
	for (int i = 0; i < m; i++) {	//从上向下逐行遍历
		int curLeft = 0, curRight = n;
		for (int j = 0; j < n; j++) {	//记录高度
			if (matrix[i][j] == '1') height[j]++;
			else height[j] = 0;
		}
		for (int j = 0; j < n; j++) {	//记录左下标
			if (matrix[i][j] == '1') {
				left[j] = max(curLeft, left[j]);//左下标取靠右值
			}
			else {
				left[j] = 0;
				curLeft = j + 1;
			}
		}
		for (int j = n - 1; j >= 0; j--) {	//记录右下标
			if (matrix[i][j] == '1') {
				right[j] = min(curRight, right[j]);//右下标取靠左值
			}
			else {
				right[j] = n;
				curRight = j;
			}
		}
		for (int j = 0; j < n; j++) {	//遍历求取当前i行的最大矩阵面积
			result = max(result, (right[j] - left[j]) * height[j]);
		}
	}
	return result;
}

方法二:栈
84. 柱状图中最大的矩形演变而来。
最大矩形单调栈
1、先使用动态规划计算每一层的柱子的高度
2、单调栈计算每一层的最大矩阵面积(与柱状图中最大矩形一样解法, 必须先理解 84题解)

int maximalRectangle(vector<vector<char>>& matrix) {
	//dp[i][j]:i*j范围的最大矩阵面积
	int result = 0;
	int m = matrix.size(), n = matrix[0].size();
	//求取每层的高度柱状图
	vector<vector<int>>heights(m + 1, vector<int>(n + 2, 0));
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= n; j++) {
			if (matrix[i - 1][j - 1] == '1') heights[i][j] = heights[i - 1][j] + 1;
			else heights[i][j] = 0;
		}
	}

	//开始求取逐层的最大面积,同84题
	for (int i = 1; i <= m; i++) {
		stack<int>st;
		for (int j = 0; j <= n + 1; j++) {
			while (!st.empty() && heights[i][j] < heights[i][st.top()]) {
				int curHeight = heights[i][st.top()];
				st.pop();
				result = max(result, curHeight * (j - st.top() - 1));
			}
			st.push(j);
		}
	}
	return result;
}
124.二叉树中的最大路径和

设计二叉树,首先考虑如何遍历;接着考虑操作节点遍历节点如何处理。

class Solution {
public:
    int maxPathSum(TreeNode* root) {
        //二叉树首先思考如何遍历
        //需要先了解子节点,再判断父节点是否选择连接,选择后序遍历
        maxSum(root);
        return result;
    }
private:
    int maxSum(TreeNode* root){
        if(root==nullptr) return 0;
        //判断子节点的value值,小于0就舍弃取0
        int leftVal =  max(maxSum(root->left),0);
        int rightVal = max(maxSum(root->right),0);
        //不可以连接父节点的情况,左中右(右中左)
        int soloVal = leftVal + rightVal + root->val;
        result = max(result , soloVal);
        //可以连接父节点的情况
        int conVal = root->val+max(leftVal , rightVal);
        return conVal;
    }
    int result=INT_MIN; 
};
152.乘积最大子数组

找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
注意:当前位置的最优解未必是由前一个位置的最优解转移得到的,例如[-2,1,-4]。

int maxProduct(vector<int>& nums) {
	//遍历观察数据变化
	int maxn = nums[0], minn = nums[0], ans = nums[0];
	for (int i = 1; i < nums.size(); i++) {
		int mx = maxn, mn = minn;
		//状态转移方程:当前位置最优值为
		//1、i-1的连续子数组乘积最大值 X 当前位置i的值
		//2、i-1的连续子数组乘积最小值 X 当前位置i的值
		//3、当前位置i的值
		maxn = max(nums[i], max(mx * nums[i], mn * nums[i]));
		minn = min(nums[i], min(mx * nums[i], mn * nums[i]));
		ans = max(maxn, ans);
	}
	return ans;
}
312. 戳气球★
312.戳气球 ★ 【腾讯QQ+快手】

状态:当前气球 k 是 [i,j] 区间内最后一个待戳破气球
dp[i][j] 表示开区间 ( i , j ) 能拿到的的金币,k 是这个区间 最后一个 被戳爆的气球,枚举 i 和 j ,遍历所有区间,i - j 能获得的最大数量的金币等于 戳破当前的气球获得的金钱加上之前 i - k 、k - j 区间中已经获得的金币

LCP57.打地鼠 周赛题

总结

参考博客:
代码随想录
力扣HOT100
力扣剑指offer
CodeTop

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值