无后效性
定义
无后效性是指如果在某个阶段上过程的状态已知,则从此阶段以后过程的发展变化仅与此阶段的状态有关,而与过程在此阶段以前的阶段所经历过的状态无关。利用动态规划方法求解多阶段决策过程问题,过程的状态必须具备无后效性。
简单的说,就是在计算后面的数值时,只于当前的数值有关而与之前的数值无关。
例题
leetcode-53最大子序和
[题目描述]
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
[分析]
状态的定义:以nums[i]结尾的连续子数组的最大值
状态的转移:
if(dp[i-1]>0) dp[i] = dp[i-1]+nums[i]
if(dp[i-1]<=0) dp[i] = nums[i]
for循环找到最大值
在状态的计算过程中我们可以发现,后面状态的计算只与当前状态的值有关,而与此阶段之前的值无关,所以具有无后效性
[代码实现]
int maxSubArray(vector<int>& nums) {
if(nums.size() == 0)
return 0;
//dp[i] 表示以nums[i]结尾的连续最大子序和
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
for(int i=1;i<nums.size();i++){
if(dp[i-1] > 0)
dp[i] = dp[i-1]+nums[i];
else
dp[i] = nums[i];
}
int ans = nums[0];
//循环遍历找到最大值
for(int i=0;i<dp.size();i++)
ans = max(ans,dp[i]);
return ans;
}
有后效性
相当于在计算当前的数值时,不仅与当前状态有关,还与当前状态之前的状态的有关。
例题
leetcode-17.16按摩师
[题目描述]
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。(相当于打家劫舍)
[分析]
方法1:设计一维状态向量,进行分类讨论
状态的定义:考虑nums[0,…,i]里预约的最长时间
状态的转移:因为这个时候不限定nums[i]是否预约,所以要分情况讨论
接收预约:dp[i] = nums[i]+dp[i-1]
不接受预约:dp[i] = dp[i-1]
二者取最大值
方法2:增加一维向量,设计成二维向量,减少分类讨论
状态的定义:考虑nums[0,…,i]的最大预约时长。
dp[i][0]:区间 [0,i] 里接受预约请求,并且下标为 i 的这一天不接受预约的最大时长
dp[i][1]:区间 [0,i] 里接受预约请求,并且下标为 i 的这一天接受预约的最大时长
通过增加维度,消除后效性
状态的转移:
//dp[i][0]表示第i天不接受预约的最大时长
//max(第i-1天不接受预约的最大时长,第i-1天接受预约的最大时长)
dp[i][0] = max(dp[i-1][0],dp[i-1][1]);
//dp[i][1]表示第i天接受预约的最大时长
//第i-1天不接受预约的最大时长+nums[i]
dp[i][1] = dp[i-1][0]+nums[i];
[代码实现]
方法1
int massage(vector<int>& nums) {
if(nums.size() == 0)
return 0;
//dp[i]表示考虑nums[0,...i]集合中能找到的最长时间
vector<vector<int>> dp(nums.size(),vector<int>(2,0));
dp[0][0] = 0;
dp[0][1] = nums[0];
for(int i=1;i<nums.size();i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = nums[i]+dp[i-1][0];
}
return max(dp[nums.size()-1][0],dp[nums.size()-1][1]);
}
方法2
int massage(vector<int>& nums) {
if(nums.size() == 0)
return 0;
//dp[i]表示考虑nums[0,...i]集合中能找到的最长时间
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
for(int i=1;i<nums.size();i++)
dp[i] = max(dp[i-1],nums[i]+(i-2>=0?dp[i-2]:0));
return dp[nums.size()-1];
}
leetcode-152最大乘积子数组
[题目描述]
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
[分析]
依旧采取之前状态的定义
状态的定义:以nums[i]为结尾的最大子数组的乘积
但是我们在实例中可以发现,有负数的出现,这样的话一个正数乘以负数,即最大值乘以负数就变成了最小值。因此最大值和最小值是可以相互转化的。在计算的时候,当前状态的值还有可能与之前的状态的值有关,存在后效性。可以通过消除后效性减少分类讨论的难度
[消除后效性]
方法:设计二维状态变量
状态的定义
dp[i][j]:以nums[i]为结尾的连续子数组的最值(j:0-最小值;j:1=最大值)
dp[i][0] : 以nums[i]为结尾的连续子数组的最小值
dp[i][1] : 以nums[i]为结尾的连续子数组的最大值
状态的转移
dp[i][0] = min(dp[i-1][0],nums[i]*dp[i-1][1]) if(nums[i]<0)
dp[i][1] = max(dp[i-1][1],nums[i]*dp[i-1][0]) if(nums[i]<0)
dp[i][0] = min(dp[i-1][0],nums[i]*dp[i-1][0]) if(nums[i]>=0)
dp[i][1] = max(dp[i-1][1],nums[i]*dp[i-1][1]) if(nums[i]>=0)
[代码实现]
int maxProduct(vector<int>& nums) {
if(nums.size() == 0)
return 0;
//dp[i]表示以nums[i]为结尾的最小(最大)乘积子数组的乘积
vector<vector<int>> dp(nums.size(),vector<int>(2,0));
dp[0][0] = nums[0];
dp[0][1] = nums[0];
for(int i=1;i<nums.size();i++){
if(nums[i] > 0){
dp[i][0] = min(nums[i],nums[i]*dp[i-1][0]);
dp[i][1] = max(nums[i],nums[i]*dp[i-1][1]);
}
else{
dp[i][0] = min(nums[i],nums[i]*dp[i-1][1]);
dp[i][1] = max(nums[i],nums[i]*dp[i-1][0]);
}
}
int ans = -INT_MAX;
for(int i=0;i<nums.size();i++){
ans = max(dp[i][1],ans);
}
return ans;
}
[参考]
1、https://leetcode-cn.com/problems/maximum-product-subarray/solution/dong-tai-gui-hua-li-jie-wu-hou-xiao-xing-by-liweiw/
2、https://leetcode-cn.com/problems/the-masseuse-lcci/solution/dong-tai-gui-hua-by-liweiwei1419-8/