目录
一、动态规划知识点
动态规划几乎是所有算法认证和竞赛中必考的一个知识点,将大问题分解成更简单的子问题,对整体问题的最优解决方案取决于子问题的最优解决方案。本文将详细介绍动态规划的知识点和经典例题。
1、动态规划一般用来解决什么问题?
常用于求解计数问题(求方案数)和最值问题(最大价值、最小花费)
如何识题?很简单的一个办法就是判断当前的结果是不是取决于当前的状态,并且当前的状态是不是和之前的状态有关(记忆化),如果是,基本就是用动态规划解题了。一个小tips就是dp[][]..[]=ans;dp的维度坐标存储的是状态,可能一个时刻是由多个角度的状态构成的,所以dp的维度可能是多维的,ans即当前状态对应的结果。
2、用动态规划求解的问题需要满足哪些条件
(1)重叠子问题:记录子问题的结果,每个子问题只计算一次
(2)最优子结构:大问题的最优解包含小问题的最优解,可以通过小问题的最优解推导出大问题的最优解
(3)无后效性:“未来与过去无关”(这个性质需要好好体会一下),这是dp的必要条件,只有这样才能降低算法的复杂度,应用dp才有意义。如果不满足无后效性,比如在计算斐波那契数列的fib(n)时,还要重新计算fib(n-1)和fib(n-2),时间并没有优化。换句话说,只关心前面的结果,不关心前面的过程,并且可以把前面的结果直接拿来用
3、动态规划的2种编程方法
(1)自顶向下与记忆化【递归】
先考虑大问题,再缩小到小问题。为了避免递归时重复计算子问题,可以在子问题得到解决时就保存结果(存入数组),再次需要这个结果时,直接返回保存的结果就可以了
(2)自底向上与制表递推【迭代】
先解决子问题,再递推到大问题,通常通过填写多维表格完成,编码时采用若干for循环语句填表,根据表中的结果,逐步计算出大问题的解决方案
4、动态规划有哪些优化方式?
如滚动数组缩小空间复杂度,将会在后面详细介绍
二、题目一
思路
1、这里dp主要的目的就是记忆化,记忆已经被识别的字符串,比不记忆每次遍历都从头比较一遍要快很多。枚举分割点是dp中常见的一种做法,因为当前状态不仅取决于上一个状态,而取决于之前的所有状态
代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//枚举分割点
//dp:已经被识别的字符串,记忆化
int n=wordDict.size();
unordered_map<string,int> have;//记录存在的单词数
for(int i=0;i<n;i++) have.insert({wordDict[i],1});
bool dp[310]={false};//dp[i]:截至到索引i的字符串是否已被识别
dp[0]=true;//一个单词也没有肯定为true,向前垫一位
for(int i=1;i<=s.size();i++)
{
//分割点
for(int j=0;j<i;j++)
{
//i本身就像前推了一位
if(dp[j]&&have.find(s.substr(j,i-j))!=have.end())
{
dp[i]=true;
break;
}
}
}
return dp[s.size()];
}
};
三、题目二
思路
1、注意一:这和枚举分割点一样,要枚举之前所有小于当前元素的位置,而不是仅仅和上一个更小的位置有关【一定要仔细想当前状态可能和前面的哪些状态有关】。枚举完以后,比较要不要把当前元素加在dp[j]这个序列后面,如果加就是dp[j]+1,如果不加就不管前面的,只等于自己自身的【初始化dp数组都为1】
2、注意二:最长长度不一定是最后一个位置的,要枚举所有位置的最长序列,然后找到最大值
3、注意三:不要一上来就对数组操作,一定要判断数组的特殊情况,比如这里的空数组,不然很可能会有些测试点通过不了
代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0; // 处理空数组的情况
vector<int> dp(n, 1); // 初始化 dp 数组,每个元素初始值为 1
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int max_length = 0;
for (int len : dp) {
max_length = max(max_length, len);
}
return max_length;
}
};
四、题目三
思路
1、当前乘积的值同样是和当前的状态有关,所以考虑动态规划,这题相对简单的一个点是规定了要求子序列连续,那么就不用枚举前面的所有分割点,只用考虑要不要追加到上一个状态后面。如果追加,就dp[i-1]*nums[i],如果不追加,当前i就没有上一个邻接点,就等于dp[i]
2、易错点,正负交替乘,负负得正,可能后面的最优解是建立在前面的最差解基础上的
3、如何解决正负交替相乘的问题?
ans=正数*num,num越大,ans越大
ans=负数*num,num越小,ans越大
所以分别记录两个状态数组,一个记录最大乘积,一个记录最小乘积
如果是正数,则和最大乘积相乘后进行比较
如果是负数,则和最小乘积相乘后进行比较
4、注意:const的定义放在类的外面,不能放在里面
const int N = 2e4 + 10;
在类内部作为数组大小时会出错,因为 C++ 中不允许在类中用 const
常量来定义数组大小【 C++ 中数组的大小通常需要是一个编译时常量 】
代码
const int N=2e4+10;
class Solution {
public:
//易错点,正负交替乘,负负得正,可能后面的最优解是建立在前面的最差解基础上的
//注意是连续子数组,所以不用枚举分割点
int dp1[N];//最大乘积
int dp2[N];//最小乘积
//dp[i]:截止到索引i的最大乘积
//对于每个dp都求最优解
int maxProduct(vector<int>& nums) {
//int dp1[N];//最大乘积
//int dp2[N];//最小乘积
int n=nums.size();
if(n==1) return nums[0];
//初始化
for(int i=0;i<n;i++){dp1[i]=dp2[i]=nums[i];}
for(int i=1;i<n;i++)
{
//1.当前数为正数
//最大乘积去比较末尾的最大乘积
//最小乘积去比较末尾的最小乘积
if(nums[i]>=0)
{
dp1[i]=max(dp1[i],dp1[i-1]*nums[i]);
dp2[i]=min(dp2[i],dp2[i-1]*nums[i]);
}
else
{
dp1[i]=max(dp1[i],dp2[i-1]*nums[i]);
dp2[i]=min(dp2[i],dp1[i-1]*nums[i]);
}
}
int ans=dp1[0];
//找子数组的最大值
for(int i=1;i<n;i++)
{
ans=max(ans,dp1[i]);
}
return ans;
}
};
参考:
【1.】罗勇军、郭卫斌《算法竞赛·上册》