动态规划
题目描述:
给出一个整数数组,求得一个非空的子数组(连续一段数),使得它的和最大。
1.解题思路一:分治
将数组分成等长的两部分,最后的答案要么在左边存在,要么再右边存在,要么是跨越中心点的某个连续数组,只有这三种情况了
先分:两个基本等长的子数组,分别求解T(n/2)
后合:跨越中心点的最大子数组合,全部枚举出来 O(n)
时间复杂度是:O(n*log n)
//时间复杂度:O(nlogn)
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n == 1){
return nums[0];
}
int mid = n>>1; //除以二
//左边是0~mid-1,右边是mid~n-1
vector<int> left(mid);
vector<int> right(n-mid);//构造两个左右vector数组
for(int i = 0;i < mid;i++)
{
left[i] = nums[i];
}
for(int i = 0,j = mid;i < right.size();i++,j++)
{
right[i] = nums[j];
}
int answer = max(maxSubArray(left),maxSubArray(right));
//这个answer是左右两部分里面的某一个
//接下来就要假设是不是跨越中心点的子数组,注意这个是假设的,不是最终答案,最终答案是与上面的answer对比,求出三部分中最大的就是真正的答案
//注意下面这个是每次分半都要经过的一步
int now = nums[mid-1];//假设从mid-1开始走,往0(前面)走
int may = now;
for(int i = mid-2;i >= 0;--i){
may = max(may,now+=nums[i]);
//往左走的最大值,我们将其放在may中
}
now = may;//往左走完了,这个值确定了,may
for(int i = mid;i < n;++i){
may = max(may,now += nums[i]);
//往右走的最大值,我们将其更新到may中
}
return max(answer,may);
}
总结一下上面代码大致的意思:
我们得到一个数组,对其进行分半,left and right,每次分半都分别对其left 和 right求一个answer = max(maxSubArray(left),maxSubArray(right)),当然这个max开始是求不到的,因为我们没有返回值,于是乎,继续对这个left和right进行分半,直到啥时候呢?到left 和 right 他们都只剩一个一个元素,这时候只能返回这一个元素了,哈哈,终于有返回了,立马比较得出answer,现在就是开始合的过程了
从最下面开始,返回了一个answer之后,执行下面的代码,now = nums[mid-1],显而易见,这个mid从最底端的0开始,左边走完确定一个may=nums[0],再往右边走,确定一个跨越中心点的最大值(这个跨越中心点在左两端),may = max( nums[0] , nums[0]+nums[1] )值,再与两端的max比较,即与nums[0] 和 nums[1]中较大的值比较,之后按照这个顺序继续往上合,每个max值都比较过,就得出了真正最大的子数组的最大值
下面是我写的一个大致的流程:
先从上倒下分开,再从下开始比较往上走,这是给出的测试用例
2.解题思路二:动态规划
我们假设以dp[i]表示以a[i]结尾的最大子数组的和
dp[i] = max(dp[i-1] + a[i] , a[i]);
这个式子怎么理解呢?
对于每个位置的最优解,我们考虑两种情况:
1.包含前一个元素,那这个等式就结果就是dp[i-1]+a[i](既然包含前一个元素dp[i-1],前一个元素也可能包含前一个元素,也可能不包含,这样递推回去就能得到最优解)
2.不包含前一个元素,那就是a[i],将dp[i]设置成a[i]
(这里的意思是,加了上一个最优解还不如不加,也就是整个前面的最优解是个负数,拉了后腿,才被舍弃)
并且,我们每次都可以使用一个变量answer来保存当前的最大值,先保存a[0],之后遇到新的最优解,刷新answer,之后每次得到新的dp[i],就与之前的最大做对比,不断刷新变量answer,最后返回它即可。
//时间复杂度O(n),空间复杂度O(1)
int maxSubArray(vector<int>& nums) {
int m = nums.size();
if(m == 1)
{
return nums[0];
}
vector<int> dp(m);
int answer = nums[0];
dp[0] = nums[0];
for(int i = 1;i < m;i++)
{
dp[i] = max(dp[i-1]+nums[i],nums[i]);
answer = max(dp[i],answer);
}
return answer;
}
下面也有一张自己画的流程图,这个比较简单一点:
下面这张表是运行过程中存在的,函数返回之后就销毁了,实际上我们只保存了answer这个一个变量,空间复杂度是O(1)
3.解题思路三:线性枚举
我们假设总数:sum[i] = a[0] + a[1] + a[2] + …… +a[i] i>=0
假设:sum[-1] = 0
则对0 <= i <= j,任意这样的i,j两数
a[i] + a[i+1] + ….. +a[j] = sum[j] - sum[i-1]
根据这个规律,我们想要的不就是这样的一个最大值么?一段区间内的连续值总和最大
So我们可以求得当前的sum[j],取的i-1值一定是使得sum[i-1]最小,然后用一个变量记录sum的最小值,最后用sum[j]减去这个最小值,就得到一个最大值,不断刷新这个最大值,返回
//时间复杂度O(n),空间复杂度O(1)
int maxSubArray(vector<int>& nums) {
int sum = nums[0];
int minsum = min(0,sum); //和sum[-1] = 0作比较
int answer = nums[0];
for(int i = 1;i < nums.size();i++){
sum += nums[i]; //这是一直的总数,j值
answer = max(answer,sum - minsum);
minsum = min(minsum,sum);
}
return answer;
}
总结一下这个题目,思路并不是很难,但是与上面的都不太相同:
就是相当于轮询一个j,我们从1开始,记录一个sum[j]总数,同时从0开始记录一个minsum(一开始的minsum和0作对比,因为是负数的话就直接取它,是正数的话就取0,也就相当于略过这一位,从下一位开始)。
之后从1开始,刷新 answer = max(answer , sum - minsum),要使得sum - minsum最大,那就minsum一直应该是最小,也就是记录下最小的那个minsum,也就是说有一个位置确定了,除非找到比那个位置更小的。它一直与sum比较,sum也就是指前 i 项,也就是最小的前 i 项数。
一直比较新得到的那个answer,返回。
有什么错误还请指出,谢谢