上周说要好好整理这道题的,因为确实花了很多时间去完成它。但是今天准备回顾的时候发现草稿纸一扔有很多细节记不起来了,看来还是要打铁趁热,现在我尽量写清楚自己的解题经过。
53. Maximum Subarray - Easy
Description
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
Example:
Given the array [-2,1,-3,4,-1,2,1,-5,4], the contiguous subarray [4,-1,2,1] has the largest sum = 6, output the sum.
More practice:
If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
解题经过
思路
这道题要说是easy我认为是不太合适的,对着那个数组用眼睛扫一下,人脑似乎很容易判断哪个子串最大,但机器呢?遍历所有长度取值的可能子串吗,不用想了,肯定超时的。
最近学了分治的方法,这道题也是在leetcode上搜索分治出来的,所以分治有什么好的解决办法呢?我就一头扎下去往这方面想,怎样划分成规模更小的子问题,然后把他们的结果合并起来?
第一种想法
这是失败的尝试。我想的是,先把数组分成前后两半,分别找到两边的最大子串,然后再看二者是否能合并。这个想法并不容易实现,我遇到了很多的问题,比如怎样才算能合并——左边最大子串往右至右边最大子串往左这一段再算一次最大子串,而三个子串间隔的两段合并成两个负数(一定是负数,要么是空串),这五个值去作比较结果就比较明显了(写出来还是一套复杂的if else)。
总之,很难写,很多细节要想清楚和处理。我写第一版转成了链表结构,写了80多行嫌弃迭代器操作太多,然后第二版用vector下标操作舒服多了,然而写到最后的逻辑判断的时候也80多行了,却发现了一个重大漏洞,额,记不起来了。
第二种想法
这种时候已经非常受挫了,只是隐约觉得非要把整个数组割开来好像不太对,就换了一种。但这种做法仍然没有跳出一个死胡同:我的方法一直是基于合并,合并地越长越好。
合并的时候可以利用一些规律:
- 相邻的同号数字可以合并成更大/小的,于是整个数组可以变成连续的正负正负的形式;
- 头尾的负数肯定会被丢弃;
- 两个正数之间隔的负数绝对值如果同时小于两个整数,那么这两个整数就可以合并。
这样一趟趟去合并,每次也能把问题的规模减小,每次集中的处理三个相邻的数,这样也算是分治了。看一下我的主要递归代码:
int fun(vector<int>& nums) { //正着一遍,倒过来再一遍才能保证最后只剩一个数
if(nums.size() == 1) return nums[0];
vector<int> anotherNums = merge(nums);
reverse(anotherNums.begin(), anotherNums.end());
vector<int> newNums = merge(anotherNums);
return fun(newNums);
}
艰难地实现
一边写一边会发现很多细节问题,比如全为负数的情况要特殊处理,合并的过程判断的逻辑也要不断完善。最后是靠着那个长为1000的样例数组,不断测试和改bug出来的代码,最后有90行。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
//预处理,保证递归前和递归返回结果都是正负正串的形式
vector<int> numsList;
bool sign = (nums[0] >= 0); //true代表正或0,false代表负
int temp = nums[0];
for(int i = 1; i < nums.size(); ++i) {
if((nums[i] >= 0) == sign) temp += nums[i];
else {
numsList.push_back(temp);
sign = (nums[i] >= 0);
temp = nums[i];
}
}
numsList.push_back(temp);
if(numsList[0] < 0) {
vector<int> temp (numsList.begin() + 1, numsList.end());
numsList = temp;
}
if(numsList.back() < 0) numsList.pop_back();
//要顺便处理一种特殊情况,即全为负数
if(numsList.empty()) {
int Max = nums[0];
for(int i = 1; i < nums.size(); ++i)
Max = max(Max, nums[i]);
return Max;
}
return fun(numsList);
}
int fun(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
vector<int> anotherNums = merge(nums);
reverse(anotherNums.begin(), anotherNums.end());
vector<int> newNums = merge(anotherNums);
return fun(newNums);
}
vector<int> merge(vector<int> nums) {
vector<int> newNums;
int left = nums[0];
bool lessThanLeft = false;
for(int i = 1; i < nums.size(); i += 2) {
int middle = nums[i];
int right = nums[i + 1];
if(left + middle >= 0 && middle + right >= 0) {
if(!newNums.empty() && newNums.back() > 0) newNums.push_back(nums[i - 2]);
left = left + middle + right;
lessThanLeft = false;
continue;
}
if(left <= right) {
if(newNums.empty()) {
left = right;
continue;
}
if(lessThanLeft) {
newNums.push_back(nums[i] + nums[i - 1] + nums[i - 2]);
lessThanLeft = false;
left = right;
}
else {
if(!newNums.empty() && newNums.back() < 0) newNums.push_back(left);
else {
newNums.push_back(nums[i - 2]);
newNums.push_back(nums[i - 1]);
}
left = right;
}
}
else {
if(!newNums.empty() && newNums.back() > 0) newNums.push_back(nums[i - 2]);
newNums.push_back(left);
left = right;
lessThanLeft = true;
}
}
if(newNums.empty() || newNums.back() < 0) newNums.push_back(left);
else {
newNums.push_back(nums[nums.size() - 2]);
newNums.push_back(nums[nums.size() - 1]);
}
return newNums;
}
};
反思
1.我试着去分析自己算法的复杂度,发现不行,它太依赖于输入数组的排列情况。最好的一遍扫过就可以了,时间上还是不错的。
2.写出来太辛苦了,只当是一种锻炼吧。
3.写出来太辛苦了,下次不要钻牛角尖了,及时放弃做别的题比较好。要想思路开阔也要有个积累的过程。
借鉴
也看了别人的解法,最高票的解法据说是贪心算法的思想,以及看了另一个十几行的分治算法,都很巧妙。我也不知道吸收了多少东西,先这样吧哈哈。