DP三步骤:
1.定义数组含义
2.找出递推式
3.赋初始值
1.连续子数组的最大和
题意
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
思想
当前0~i的数组中 , nums[i]参与的子数组的最大和,只有两种情况,
1. 与前面的子数组的最大和相加, PreMaxSum加上nums[i],
2. 不与前面的子数组相加,单独自己成为一个新的子数组。
我们求最大和,那么取两种情况中最大的和。
定义dp[i] 是当前0~i的数组中,以索引i结尾的数组和的最大值
代码
class Solution {
public:
int dp[100010];
int maxSubArray(vector<int>& nums) {
nums.insert(nums.begin(),0); //插入个0方便计算
int ans = -9999999;
for(int i = 1;i<nums.size();i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
ans = max(ans,dp[i]);
}
return ans;
}
};
2.求从(0,0)-(x,y)的路径个数[]
传送门
题意:
给出点(x,y)为要到达的点,题目给出了部分不能走的点,问从(0,0)->(x,y)的路径个数。
设dp[x][y]表示从(0,0)到达(x,y)的路径个数。
递推方程:dp[x][y]=dp[x-1][y]+dp[x][y-1] (x!=0 && y!=0)
dp[x][y]=dp[x-1][y] (y==0 && x!=0)
dp[x][y]=dp[x][y-1] (x==0 && y!=0)
dp[x][y]=1 (x==0 && y==0)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll dp[25][25]; //dp[x][y]表示(0,0)到达(x,y)的路径个数
ll g[25][25];
int main()
{
ll x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
//将不能走的点标记
g[x2][y2]=1;
if(x2>=2 && y2>=1) g[x2-2][y2-1]=1;
if(x2<=18 && y2>=1) g[x2+2][y2-1]=1;
if(x2>=2 && y2<=19) g[x2-2][y2+1]=1;
if(x2<=18 && y2<=19) g[x2+2][y2+1]=1;
if(x2>=1 && y2>=2) g[x2-1][y2-2]=1;
if(x2>=1 && y2<=18) g[x2-1][y2+2]=1;
if(x2<=19 && y2>=2) g[x2+1][y2-2]=1;
if(x2<=19 && y2<=18) g[x2+1][y2+2]=1;
for(int i=0;i<=x1;i++)
{
for(int j=0;j<=y1;j++)
{
if(g[i][j]==0) //如果该点可以到达 递推公式 dp[x][y]=dp[x-1][y]+dp[x][y-1],注意对数组越界的特殊处理
{
if(i==0 && j==0)
dp[i][j]=1;
else if(i==0 && j>0)
dp[i][j]=dp[i][j-1];
else if(i>0 && j==0)
dp[i][j]=dp[i-1][j];
else
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
cout<<dp[x1][y1]<<endl;
}
3.最小路径和
传送门
题意
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,
使得路径上的数字总和为最小。规定只能向下 或者 向右走
思想
1.设dp[i][j] 表示到达点(i,j)最小路径总和
2.由前一步到后一步的变化,可知 dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]; (i!=0 && j!=0)
3.赋初始值 当i=0时,dp[0][j]=dp[0][j-1]+grid[0][j];
当j=0时,dp[i][0]=dp[i-1][0]+grid[i][0];
代码
int minPathSum(vector<vector<int>>& grid) {
int dp[205][205]; //dp[i][j] 表示到达点(i,j)最小路径总和
int m=grid.size();
int n=grid[0].size();
if(m==0 || n==0)
return 0;
dp[0][0]=grid[0][0];
for(int i=1;i<m;i++)
dp[i][0]=dp[i-1][0]+grid[i][0];
for(int i=1;i<n;i++)
dp[0][i]=dp[0][i-1]+grid[0][i];
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[m-1][n-1];
}
4.不同路径
传送门
题意
个机器人位于一个 m x n 网格的左上角 。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
思想
1.设dp[i][j]表示到达点(i,j)的路径条数
2.找出递推式 dp[i][j]=0; (如果点(i,j)为障碍物 且 i!=0 j!=0)
dp[i][j]=dp[i-1][j]+dp[i][j-1]; (如果点(i,j)不为障碍物 且 i!=0 j!=0)
3.赋初始值 当i为0时,由于只能一直往右走,如果未遇到障碍物则为1,如果遇到了障碍物,则障碍物后面的点全为0.
当j为0时,只能往下走....同理
代码
int uniquePathsWithObstacles(vector<vector<int>>& a) {
int dp[105][105];
int m=a.size();
int n=a[0].size();
int flag=0;
for(int i=0;i<m;i++)
{
if(a[i][0]==1)
flag=1;
if(flag==0)
dp[i][0]=1;
else
dp[i][0]=0;
}
flag=0;
for(int i=0;i<n;i++)
{
if(a[0][i]==1)
flag=1;
if(flag==0)
dp[0][i]=1;
else
dp[0][i]=0;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(a[i][j]==1)
dp[i][j]=0;
else
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
5.三角形最小路径和
传送门
题意
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
思想
与上面两个题思想一致,考虑一下边界即可。
代码
int minimumTotal(vector<vector<int>>& triangle) {
int dp[205][205]; //dp[i][j]表示 到达点(i,j)时的路径和
int m=triangle.size();
dp[0][0]=triangle[0][0];
for(int i=1;i<m;i++)
{
for(int j=0;j<=i;j++)
{
if(j==0)
dp[i][j]=dp[i-1][j]+triangle[i][j];
else if(j==i)
dp[i][j]=dp[i-1][j-1]+triangle[i][j];
else
dp[i][j]=min(dp[i-1][j],dp[i-1][j-1])+triangle[i][j];
}
}
int minn=99999999;
for(int i=0;i<m;i++)
minn=min(minn,dp[m-1][i]);
return minn;
}
6.最长回文子串
传送门
题意
给你一个字符串 s,找到 s 中最长的回文子串。
思想
1.设dp[i][j]表示s在下标区间[i,j]是否为回文串
2.找递推式
当子串长度为1时,dp[i][i]=true;
当子串长度为2时,dp[i][j]= (s[i]==s[j]);
当子串长度>2时,dp[i][j]= dp[i-1][j+1] && (s[i]==s[j]);
3.赋初始值
即为所有长度为1的子串赋值为true
代码
string longestPalindrome(string s) {
int dp[1005][1005]; //dp[i][j]表示 s在区间[i,j]是否为回文串
if(s.length()<2)
return s;
int n=s.length();
for(int i=0;i<n;i++)
dp[i][i]=true;
int maxlength=1;
int beg=0,end=0;
for(int l=2;l<=n;l++) //在这里我是遍历长度直接锁定区间左右边界,当然也可以分别设i,j遍历,i从大到小遍历,j从小到大遍历
{
for(int i=0;i<s.length();i++) //遍历左区间
{
int j=l+i-1; //得到右区间
if(j>=n)
break;
if(l==2)
dp[i][j]=(s[i]==s[j]);
else
dp[i][j]=dp[i+1][j-1] && (s[i]==s[j]); //这题核心就是这个
if(dp[i][j]==true && l>maxlength)
{
maxlength=l;
beg=i;
end=j;
}
}
}
return s.substr(beg,end-beg+1);
}
7.分割字符串
题意
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
思想
这题肯定是要dfs进行切割,但是关键点在于dfs找到各个切割部分后需要判断该切割部分是否为回文串,
如果进行双指针在dfs中判断的话会比较耗时,因此采用dp进行预处理,dp[i][j]表示区间[i,j]是否为回文串。
递推式什么的与上题一样。
代码
vector<vector<string>> ret; //存结果集的集合
vector<string> res; //存每一个结果集
bool dp[20][20]; //dp[i][j]表示区间[i,j]是否为回文串,先用dp进行预处理判断,然后再dfs进行分割
int n;
void dfs(int x,string s)
{
if(x==n) //若分割成功
{
ret.push_back(res);
return;
}
for(int i=x;i<n;i++)
{
if(dp[x][i]==true) //如果当前切割满足回文
{
res.push_back(s.substr(x,i-x+1));
dfs(i+1,s);// 存起来后 进入后续继续切割
res.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
n=s.length();
for(int i=0;i<n;i++) //初始化子串为一个字符时 dp为true
dp[i][i]=true;
for(int i=n-1;i>=0;i--)
{
for(int j=i+1;j<n;j++)
{
if(j-i==1)
dp[i][j]=(s[i]==s[j]);
else
dp[i][j] = s[i]==s[j] && dp[i+1][j-1];
}
}
dfs(0,s);
return ret;
}
8.交错字符串
题意
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...
思想
1.设dp[i][j]表示s1的前i个字符 与 s2的前j个字符能否交错组成s3的前i+j个字符
2.找递推式
通过分析可知,若s1的第i个字符等于s3的第i+j个字符,dp[i][j]取决于dp[i-1][j]
同时若s2的第j个字符等于s3的第i+j个字符,dp[i][j]还取决于dp[i][j-1]
因此可得出递推式如下:
dp[i][j]= (dp[i-1][j] && s1[i-1]==s3[i+j-1]) || (dp[i][j-1] && s2[j-1]==s3[i+j-1]);
3.赋初始值
当两个字符串都为空串时,dp[i][j]=true;
当两个字符串中有一个为空串时
dp[i][0]=dp[i-1][0] && (s1[i-1]==s3[i-1]);
dp[0][j]=dp[0][j-1] && (s2[j-1]==s3[j-1]);
代码
bool isInterleave(string s1, string s2, string s3) {
if(s1.length()+s2.length()!=s3.length())
return false; //长度不对应直接输出false
bool dp[105][105];
dp[0][0]=true;
for(int i=1;i<=s1.length();i++)
dp[i][0]=dp[i-1][0] && (s1[i-1]==s3[i-1]);
for(int j=1;j<=s2.length();j++)
dp[0][j]=dp[0][j-1] && (s2[j-1]==s3[j-1]);
for(int i=1;i<=s1.length();i++)
{
for(int j=1;j<=s2.length();j++)
//递推式
dp[i][j]= (dp[i-1][j] && s1[i-1]==s3[i+j-1]) || (dp[i][j-1] && s2[j-1]==s3[i+j-1]);
}
return dp[s1.length()][s2.length()];
}
9.单词切分
题意
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆
分为一个或多个在字典中出现的单词。
思路
1.设dp[i] 表示s的前i个字符组成的串能否拆分成字典中的若干个单词
2.分析可知,dp[i] 与 dp[j](j<i)是有一定关系的:
如果dp[j]能够拆分成字典的若干个单词,且从第j+1个字符-第i-1个字符组成的子串包含在字典中,
那么dp[i]就能够拆分成字典中的若干单词,因此我们枚举j判断是否存在可行的情况
3.赋初始值 dp[0]=true;
代码
bool dp[10005]; //dp[i] 表示s的前i个字符组成的串能否拆分成字典中的单词
bool wordBreak(string s, vector<string>& wordDict) {
dp[0]=true;
set<string> se;
for(int i=0;i<wordDict.size();i++)
se.insert(wordDict[i]);
for(int i=1;i<=s.length();i++)
{
for(int j=0;j<i;j++)
{
if(dp[j]==true && se.find(s.substr(j,i-j))!=se.end())
{
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
10.乘积最大子数组
题意
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),
并返回该子数组所对应的乘积。
思想
第一反应是与累加和最大子数组类似,但是由于包含负数,因此不能与那题完全一样。
累加和子数组只保留了以某个数结尾的子数组累加和的最大值。
乘积最大子数组不仅需要保留以某个数结尾的子数组乘积的最大值同时还要保留以其结尾的子数组的最小值。
这样在处理下一个数的最大值时,有三种情况,三者取max
(1)若其为正数 则为与前一个数保留的最大值相乘
(2)若其为负数 则为与前一个数保留的最小值相乘
(3)只保留自身
同理处理下一个数的最小值时也是类似。
1.设 dpmax[i]表示以下标i结尾的连续数组的乘积的最大值
dpmin[i]表示以下标i结尾的连续数组的乘积的最小值
2.递推式
dpmax[i]=max(dpmax[i-1]*nums[i], max(dpmin[i-1]*nums[i], nums[i]));
dpmin[i]=min(dpmax[i-1]*nums[i], min(dpmin[i-1]*nums[i], nums[i]));
3.赋初始值
dpmax[0]=nums[0];
dpmin[0]=nums[0];
代码
int dpmax[100005]; //dpmax[i]表示以i结尾的连续数组的乘积的最大值
int dpmin[100005]; //dpmin[i]表示以i结尾的连续数组的乘积的最小值
int maxn;
int maxProduct(vector<int>& nums) {
dpmax[0]=nums[0];
dpmin[0]=nums[0];
maxn=nums[0];
for(int i=1;i<nums.size();i++)
{
dpmax[i]=max(dpmax[i-1]*nums[i], max(dpmin[i-1]*nums[i], nums[i]));
dpmin[i]=min(dpmax[i-1]*nums[i], min(dpmin[i-1]*nums[i], nums[i]));
maxn=max(dpmax[i],maxn);
}
return maxn;
}
11.打家劫舍
题意
一个全为正整数的数组,不能取连续的两个,问最大能取到的数的和为多大
思想
1.设dp[i]表示前i个数所能取到的最大和
2.找到递推式
dp[i]=max(dp[i-2]+nums[i-1], dp[i-1]); (i>=2)
3.赋初始值
dp[1]=nums[0];
dp[0]=0;
代码
int dp[120]; //dp[i]表示前i间房屋所能偷到最大价值
int rob(vector<int>& nums) {
dp[0]=0;
dp[1]=nums[0];
for(int i=2;i<=nums.size();i++)
dp[i]=max(dp[i-1],dp[i-2]+nums[i-1]);
return dp[nums.size()];
}
12.最大正方形
题意
在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
思想
1.设dp[i][j]表示以点(i,j)为右下角的最大正方形的边长
2.找递推式
dp[i][j]=min(dp[i-1][j], min(dp[i][j-1], dp[i-1][j-1]))+1;
3.赋初始值
第一行和第一列 dp[i][j] = matrix[i][j]=='1';
代码
int dp[350][350]; //dp[i][j]表示以点(i,j)为右下角的最大正方形的边长
int maximalSquare(vector<vector<char>>& matrix) {
int m=matrix.size();
int n=matrix[0].size();
int maxn=0;
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(i==0 || j==0)
dp[i][j]=matrix[i][j]=='1';
else if(matrix[i][j]=='0')
dp[i][j]=0;
else
dp[i][j]=min(dp[i-1][j], min(dp[i][j-1], dp[i-1][j-1]))+1;
maxn=max(maxn,dp[i][j]);
}
}
return maxn*maxn;
}
13.完全平方数
题意
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。
你需要让组成和的完全平方数的个数最少。
思想
1.设dp[i] 表示和为i的完全平方数的最小数量
2.找递推式
dp[i]=min(dp[i-j*j]+1, dp[i]); //其中j*j为小于i的完全平方数,遍历
3.赋初始值
dp[0]=0;
代码
int dp[10005]; //dp[i] 表示和为i的完全平方数的最小数量
int numSquares(int n) {
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++)
{
dp[i]=999999; //初始设一个比较大的值,用于取min
for(int j=1;j*j<=i;j++)
{
dp[i]=min(dp[i-j*j]+1,dp[i]);
}
}
return dp[n];
}
与这题类似的
14.零钱兑换
题意
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所
需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
代码
int dp[10005]; //dp[i]表示钱数为i时所用的最少硬币数
int coinChange(vector<int>& coins, int amount) {
for(int i=1;i<=amount;i++)
{
dp[i]=999999;
for(int j=0;j<coins.size();j++)
{
if(i>=coins[j])
dp[i]=min(dp[i-coins[j]]+1,dp[i]);
}
}
if(dp[amount]==999999)
return -1;
return dp[amount];
}
15.打家劫舍 III
题意
有一个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。
一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两
个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
思想
1.开两个映射 f和g
f[node] 表示 在选择节点node情况下,node子树上(包括node)所选择的节点的和的最大值
g[node] 表示 在不选择节点node情况下,node子树上(包含node)所选择的节点的和的最大值
2.找递推式
f[node] = node->val + g[node->left] + g[node->right];
g[node] = max(f[node->left],g[node->left]) + max(f[node->right],g[node->right]);
3.赋初始值..(此题直接dfs到底就自动赋了)
代码
map<TreeNode*,int> f,g; //f[node] 表示在选择node节点情况下,node节点的子树上被选择节点的总和最大值
//g[node] 表示在不选择node节点情况下,node节点的子树上被选择节点的总和的最大值
void dfs(TreeNode *root)
{
if(root==NULL)
return;
dfs(root->left);
dfs(root->right);
f[root]=root->val+g[root->left]+g[root->right];
g[root]=max(g[root->left],f[root->left])+max(g[root->right],f[root->right]);
}
int rob(TreeNode* root) {
dfs(root);
return max(f[root],g[root]);
}
15.整数拆分
题意
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可
以获得的最大乘积。
代码
int dp[70]; //dp[i]表示把i拆分成至少两个正整数的和的最大乘积
int integerBreak(int n) {
for(int i=2;i<=n;i++)
{
dp[i]=0;
for(int j=1;j<i;j++)
dp[i]=max(dp[i], max((i-j)*j, dp[i-j]*j));
}
return dp[n];
}