DP做题合集

目录

      • 一、简单级别
        • 1、最大子序和
        • 2、爬楼梯
        • 3、买卖股票的最佳时机
        • 4、打家劫舍
        • 5、区域和检索 - 数组不可变
        • 6、判断子序列
        • 7、使用最小花费爬楼梯
        • 8、除数博弈
      • 二、中等级别
        • 1、最长回文子串
        • 2、不同路径

一、简单级别

1、最大子序和

链接:https://leetcode-cn.com/problems/maximum-subarray/


题目:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

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

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


思路:
变量sum记录子数组的和,初始值为0;res记录和的最大值,初始值为nums[0]。循环遍历目标数组,循环内进行判断,若sum>0,令sum加上nums[i];若sum<=0,令sum等于nums[i],因为此时sum+nums[i]<=nums[i]。并且每次循环res都要更新。

代码

class Solution {
public:
	int maxSubArray(vector<int>& nums) {
		int sum = 0;
		int res = nums[0];
		for (int i = 0; i < nums.size(); i++)
		{
			if (sum > 0)
				sum += nums[i];
			else
				sum = nums[i];
			res = max(sum, res);
		}
		return res;
	}
};

2、爬楼梯

链接:https://leetcode-cn.com/problems/climbing-stairs/


假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

思路:
定义动态数组dp,dp[i]代表到达第i个台阶的方法数。为了方便,定义dp[0]=1,dp[1]=1。到达第n个台阶有两种方法,从第n-1个台阶爬或第n-2个台阶爬,所以关系式为dp[n]=dp[n-1]+dp[n-2](n>1)。

代码

class Solution {
public:
	int climbStairs(int n) {
		int* dp = new int[n + 1];
		dp[0] = 1;
		dp[1] = 1;
		for (int i = 2; i <= n; i++)
			dp[i] = dp[i - 1] + dp[i - 2];
		return dp[n];
	}
};

3、买卖股票的最佳时机

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/


给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。


思路:
把数组变成折线图,有波峰和波谷,我们需要找到最小的谷之后的最大的峰。定义两个变量,Minprice 和 Maxprofit,它们分别对应迄今为止所得到的最小的谷值和最大的利润(卖出价格与最低价格之间的最大差值)。

代码:

class Solution {
public:
	int maxProfit(vector<int>& prices) {
		int Maxprofit = 0int Minprice = INT_MAX;
		for (int i = 0; i < prices.size(); i++)
		{
			if (prices[i] < Minprice)
				Minprice = prices[i];
			else if (prices[i] - Minprice > Maxprofit)
				Maxprofit = prices[i] - Minprice;
		}
		return Maxprofit;
	}
};

4、打家劫舍

链接:https://leetcode-cn.com/problems/house-robber/


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

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

示例 1:

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

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。


思路:
定义变量preMax和curMax,为了方便,初始值都为0。含义如下:在第 i 次循环开始时,preMax记录的是经过 i-2 次循环得到的最高金额,curMax记录的是经过第 i-1 次循环得到的最高金额。循环过程中,先定义变量temp存储当前curMax的值;然后更新curMax,curMax取 preMax加上当前房屋的金额 与 当前的curMax 相比较的最大值;最后更新preMax,令preMax等于temp。循环结束,curMax即为能够偷窃到的最高金额。

代码

class Solution {
public:
	int rob(vector<int>& nums) {
		int preMax = 0;
		int curMax = 0;
		for (int i = 0; i < nums.size(); i++)
		{
			int temp = curMax;
			curMax = max(preMax + nums[i], curMax);
			preMax = temp;
		}
		return curMax;
	}
};

5、区域和检索 - 数组不可变

链接:https://leetcode-cn.com/problems/range-sum-query-immutable/


给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

说明:

你可以假设数组不可变。
会多次调用 sumRange 方法。


思路:
第一种:暴力法。每次调用 sumrange 时,都使用for循环将索引 i 到 j 之间的每个元素相加。
时间复杂度为O(n)。
空间复杂度为O(1)。

第二种:缓存。定义数组dp,dp[i]的值是索引0到 i 之间的元素和,在构造函数中确定数组dp的值。而sumrange(i,j)=sum[j+1]−sum[i]。
时间复杂度:每次查询的时间 O(1),O(N)预计算时间。由于累积和被缓存,每个sumrange查询都可以用 O(1)时间计算。
空间复杂度:O(n)。

代码

class NumArray {
public:
	int *dp;
	NumArray(vector<int>& nums) {
		dp = new int[nums.size() + 1];
		if (nums.size())
			dp[0] = nums[0];
		for (int i = 1; i < nums.size(); i++)
			dp[i] = nums[i] + dp[i - 1];
	}

	int sumRange(int i, int j) {
		if (i < 1)
			return dp[j];
		else
			return dp[j] - dp[i - 1];
	}
};

6、判断子序列

链接:https://leetcode-cn.com/problems/is-subsequence/


给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:
s = “abc”, t = “ahbgdc”

返回 true.

示例 2:
s = “axc”, t = “ahbgdc”

返回 false.


思路:
直接判断子序列,时间复杂度为O(n),空间复杂度为O(1)。

代码:

class Solution {
public:
	bool isSubsequence(string s, string t) {
		int k1 = s.length()int k2 = t.length();
		int temp = 0;
		for (int i = 0; i < k2; i++)
		{
			if (s[temp] == t[i])
				temp++;
			if (temp == k1)
				return true;
		}
		return false;
	}
};

7、使用最小花费爬楼梯

链接:https://leetcode-cn.com/problems/min-cost-climbing-stairs/


数组的每个索引做为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 costi。

每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。

您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。

示例 1:

输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。

示例 2:

输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。

注意:

cost 的长度将会在 [2, 1000]。
每一个 cost[i] 将会是一个Integer类型,范围为 [0, 999]。


代码
开始时自己写的:

class Solution {
public:
	int minCostClimbingStairs(vector<int>& cost) {
		int length = cost.size();
		int *dp = new int[length + 3];
		dp[0] = cost[0];
		dp[1] = cost[1];
		for (int i = 2; i < length; i++)
			dp[i] = cost[i] + min(dp[i - 1], dp[i - 2]);
		int mincost = min(dp[length - 2], dp[length - 1]);
		delete []dp;
		return mincost;
	}
};

后面看到别人更简单的:

class Solution {
public:
	int minCostClimbingStairs(vector<int>& cost) {
		int f1 = 0, f2 = 0;
		for (int i = 0; i < cost.size(); i++)
		{
			int f0 = cost[i] + min(f1, f2);
			f1 = f2;
			f2 = f0;
		}
		return min(f1, f2);
	}
};

才明白其实不用数组专门存储数值,因为每次求 到当前阶梯所要的最低花费 都要用到 前两层阶梯的最低花费,所以用两个变量记录前两层阶梯的最低花费,一个临时变量记录当前阶梯所要的最低花费 就ok了。

8、除数博弈

链接:https://leetcode-cn.com/problems/divisor-game/


爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。

最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:

选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。

只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。

示例 1:

输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。

示例 2:

输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。

提示:
1 <= N <= 1000


思路1:(本质是数学思想)
如果N是奇数,因为奇数的所有因数都是奇数,因此 N 进行一次 N-x 的操作结果一定是偶数,所以如果 Alice 拿到了一个奇数,那么轮到 Bob 的时候,Bob 拿到的肯定是偶数,这个时候 Bob 只要进行 -1, 还给 Alice 一个奇数,那么这样子Bob 就会一直拿到偶数,到最后Bob 一定会拿到最小偶数2,Alice 就输了。

所以如果游戏开始时Alice拿到N为奇数,那么她必输,也就是false。如果拿到N为偶数,她只用 -1,让Bob 拿到奇数,最后Bob必输,结果就是true。

代码1

class Solution {
public:
	bool divisorGame(int N) {
		if (N % 2)
			return false;
		else
			return true;
	}
};

思路2:(用动态规划做)
用dp[N]记录数字为N的情况下,Alice的输赢情况。如果Alice取了数字x, 那么dp[N]与dp[N -x]输赢情况相反。x可以取的值很多,只要dp[N -xi]中任意一个为False, 那么dp[N]肯定为True, 否则dp[N]为False。

代码2

class Solution {
public:
	bool divisorGame(int N) {
		bool *dp = new bool[N + 2];
		dp[1] = false;
		dp[2] = true;
		for (int i = 3; i <= N; i++)
		{
			dp[i] = false;
			for (int j = 1; j < i; j++)
				if (i%j == 0 && !dp[i - j])
				{
					dp[i] = true;
					break;
				}
		}
		bool temp = dp[N];
		delete []dp;
		return temp;
	}
};

二、中等级别

1、最长回文子串

链接:https://leetcode-cn.com/problems/longest-palindromic-substring/


给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。

示例 2:

输入: “cbbd”
输出: “bb”


思路1:暴力法(会超时)
暴力求解,列举所有的子串,判断是否为回文串,保存最长的回文串。

代码

class Solution {
public:
	string longestPalindrome(string s) {
		int len = s.length();
		string ans = "";
		int max = 0;
		for (int i = 0; i < len; i++)
		{
			for (int j = i + 1; j <= len; j++)
			{
				string temp = s.substr(i, j - i + 1);
				//s.substr(i,j)的含义是从i开始,向后截取j位(包含i)
				if (isPalindromic(temp) && temp.length() > max)
				{
					max = temp.length();
					ans = temp;
				}
			}
		}
		return ans;
	}
	bool isPalindromic(string temp)
	{
		int len = temp.length();
		for (int i = 0; i < len / 2; i++)
		{
			if (temp[i] != temp[len - i - 1])
				return false;
		}
		return true;
	}
};

时间复杂度:两层 for 循环 O(n²),for 循环里边判断是否为回文 O(n),所以时间复杂度为 O(n³)。

空间复杂度:O(1),常数个变量。

思路2:
把原来的字符串倒置,找最长的公共子串。例如 S = “caba” ,S = “abac”,最长公共子串是 “aba”,所以原字符串的最长回文串就是 “aba”。这里用动态规划的方法求最长公共子串。整体思想就是,申请一个二维的数组初始化为 0,然后判断对应的字符是否相等,相等的话array [ i ][ j ] = array [ i - 1 ][ j - 1] + 1 。当 i = 0 或者 j = 0 的时候单独分析,字符相等的话 array [ i ][ j ] 就赋为 1 。array [ i ][ j ] 保存的就是公共子串的长度。

要注意的是,当我们求出最长公共子串后,并不一定是回文串,我们还需要判断该字符串倒置前的下标和当前的字符串下标是不是匹配。例如,S=“abc435cba”,S=“abc534cba”,最长公共子串是 “abc” 和 “cba”,但很明显这两个字符串都不是回文串。

怎么判断呢?比如 S = “caba”,S’ = “abac” ,S’ 中 aba 的下标是 0 1 2 ,倒置前是 3 2 1,和 S 中 aba 的下标符合,所以 aba 就是我们需要找的。我们不需要每个字符都判断,只需要判断末尾字符就可以。

代码

class Solution {
public:
	string longestPalindrome(string s) {
		int length = s.length();
		if (!length)
			return s;
		int maxlength = 0;
		int maxend = 0;
		int **array = new int *[length];
		for (int i = 0; i < length; i++)
			array[i] = new int[length];
		for (int i = 0; i < length; i++)
			for (int j = 0; j < length; j++)
				array[i][j] = 0;
		string origin = s;
		reverse(s.begin(), s.end());
		for (int i = 0; i < length; i++)
			for (int j = 0; j < length; j++)
			{
				if (origin[i] == s[j])
				{
					if (!i || !j)
						array[i][j] = 1;
					else
						array[i][j] = array[i - 1][j - 1] + 1;
				}
				if (array[i][j] > maxlength)
				{
					int beforeRev = length - 1 - j;			//判断下标是否对应
					if (beforeRev + array[i][j] - 1 == i)
					{
						maxlength = array[i][j];
						maxend = i;
					}
				}
			}
		for (int i = 0; i < length; i++)
			delete[]array[i];
		delete[]array;
		return origin.substr(maxend - maxlength + 1, maxlength);
	}
};

时间复杂度:两层循环 O(n²)。
空间复杂度:一个二维数组 O(n²)。

空间复杂度可以再优化:
分析一下循环,i=0,j=0,1,2…8 更新一列,然后 i=1,再更新一列,而更新的时候我们只需要上一列的信息,更新第 3 列的时候,第 1 列的信息是没有用的。所以只需要一个一维数组就可以了。但是更新 array [i] 的时候我们需要 array [i-1] 的信息,假设 array [3] = array [2] + 1,更新 array [4] 的时候, 我们需要 array [3] 的信息,但是 array [3] 在之前已经被更新了,所以 j 不能从0到 8,应该倒过来,array [8]=array [7] + 1,array [7] = array [6] + 1, 这样更新 array [8] 的时候用 array [7],用完后才去更新 array [7],保证了不会出错。

代码

class Solution {
public:
	string longestPalindrome(string s) {
		int length = s.length();
		if (!length)
			return s;
		int maxlength = 0;
		int maxend = 0;
		int *array = new int [length];
		string origin = s;
		reverse(s.begin(), s.end());
		for (int i = 0; i < length; i++)
			for (int j = length-1; j >= 0; j--)
			{
				if (origin[i] == s[j])
				{
					if (!i || !j)
						array[j] = 1;
					else
						array[j] = array[j - 1] + 1;
				}
				else
					array[j] = 0;		//之前不用置0是因为用的是不同的列
				if (array[j] > maxlength)
				{
					int beforeRev = length - 1 - j;
					if (beforeRev + array[j] - 1 == i)
					{
						maxlength = array[j];
						maxend = i;
					}
				}
			}
		delete[]array;
		return origin.substr(maxend - maxlength + 1, maxlength);
	}
};

时间复杂度 O(n²)。
空间复杂度降为 O(n)。

思路3:扩展中心
回文串一定是对称的,所以可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。由于存在奇数的字符串和偶数的字符串,所以需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 n+n-1 个中心。

代码

class Solution {
public:
	string longestPalindrome(string s) {
		int length = s.length();
		if (!length)
			return s;
		int start = 0, end = 0;
		for (int i = 0; i < length; i++)
		{
			int len1 = ExpandAroundCenter(s, i, i);
			int len2 = ExpandAroundCenter(s, i, i + 1);
			int len = max(len1, len2);
			if (len > end - start)
			{
				start = i - (len - 1) / 2;
				end = i + len / 2;
			}
		}
		return s.substr(start, end - start + 1);
	}
	int ExpandAroundCenter(string s, int left, int right)
	{
		int L = left, R = right;
		while (L >= 0 && R < s.length() && s[L] == s[R])
		{
			L--;
			R++;
		}
		return R - L - 1;
	}
};

时间复杂度:O(n²)。
空间复杂度:O(1)。

2、不同路径

链接:https://leetcode-cn.com/problems/unique-paths/


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

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

问总共有多少条不同的路径?
DP做题合集_第1张图片
例如,上图是一个7 x 3 的网格。有多少可能的路径?

说明:m 和 n 的值均不超过 100。

示例 1:

输入: m = 3, n = 2
输出: 3

解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右

示例 2:

输入: m = 7, n = 3
输出: 28


思路:
到达某个网格 i 的路径数等于到达i - 1的路径数加上到达i - m的路径数。

代码:

typedef struct grid
{
	int data;
	bool flag = false;			//flag区分两类网格,最左边一列和最上边一行为一类,其它为另一类
};
class Solution {
public:
	int uniquePaths(int m, int n) {
		grid * array = new grid[m * n + 1];
		for (int i = 0; i < n; i++)
		{
			array[i * m].data = 1;
			array[i * m].flag = true;
		}
		for (int i = 1; i < m; i++)
			array[i].data = 1;
		for (int i = m; i < m * n; i++)
		{
			if (!array[i].flag)
				array[i].data = array[i - 1].data + array[i - m].data;
		}
		int temp = array[m * n - 1].data;
		delete []array;
		return temp;
	}
};

你可能感兴趣的:(DP)