序列相关的趣题 之一

闲话少叙,直接上题。

(1) 最大子数组和

这个问题已经是toooooooooold了。原问题是:给定一个数组,求一个子数组(连续的一段),它们的和最大。一些细节就是是否允许啥都不选,不过这个无关紧要。其实不少书都把它作为动态规划的题目,我个人倒不太喜欢真的把它作为dp,之所以这么说,我觉得作为dp反倒禁锢了人的思路,有点大材小用的感觉,并且真正的dp比这个复杂得多。这个题解法相当多,扔掉暴力的,也有很多理解。

(1.1)我自己yy的,其实和(1.2)是一样的。连续的一段数我们可以把开头和结尾的负数扔掉,然后非负数连续的加在一起,负数加在一起,形成这样的“堆”:

正 负 正 负 正 负…… 

之所以这样是因为我们同一堆里 要么就都选 要么就都不选,所以可以作为一个数处理。 (这是因为正数堆要选肯定都选了,负数堆要选肯定是为了“连接”到下一个正数上,否则选负数干嘛?)于是我们就一个一个看,先选第一个正的,如果加上负数还大于0,我们就继续选,也就是说只要“前缀”是正的,我们就可以继续选数,前缀是负数的话,我们扔了重选。 其实(1.2)就是这个思路。


(1.2) 经典解法  end[i]表示以a[i]结尾子数组的和,则end[i + 1] = max(end[i] + a[i + 1], a[i + 1]), 其实这一步等价于end[i] >= 0 就取 end[i] + a[i + 1],否则就取a[i + 1],最优解肯定在某个地方结束,所以最终max{end[0..n - 1]}为所求。习惯上把这种东西叫做动态规划……叫什么都好,反正这个确实满足最优子问题,即end[i + 1]是由end[i]推出来的,但是只会这一种解法会影响理解后面的问题。


(1.3) 我个人非常喜欢的解法。因为体现了前缀和的思想。我们考虑prefix[i] = a[0] + a[1] + ...+a[i],即prefix[i]是一个前缀和,那么任何一个子数组就是两个前缀的差了。细节是要定义prefix[-1] = 0。 那么子数组a[i..j] = prefix[j] - prefix[i - 1]。那么我们如何求prefix[i]?循环i即可,prefix[i] = prefix[i - 1] + a[i]。那么如何求(1.1)中的end[i]?要以a[i]结尾最大的,那么我们只要用prefix[i] - prefix[j]就好了,而prefix[j]就是prefix[0..i]的最小值就可以了。所以我们循环i,每次用prefix[i]减去出现过的最小值就可以了……这是一个自然的思路,并且这个思路应用很广泛……感觉虽然(1.2)是动态规划,但是我们不能过于重视,而忽视了(1.3)的存在。


(1.4) 只有理论意义……因为是个O(nlogn)的方法。分治,从中间劈开数组,那么最大子数组要么在左半边,要么在右半边,这两部分递归解决。关键是最大子数组跨越中间点的情况。那么如果我们要求跨越中间点的最大和,我们就简单的从中间点往左扫一遍,看看连续加到哪里最大,同样往右边扫一边看看连续加到哪里最大就好了。这是因为我们确定了它一定要跨过中间点,所以有了一个“着力点”。主要是这部分的复杂度是O(n)的,所以整个递归分治算法的复杂度是O(nlogn)的。

顺便提一句,最优的算法(1.1)-(1.3)都是O(n)的。


(2) 允许交换一次的最大子数组和

比(1)难很多,就是允许交换两个元素,只交换一次,当然可以不交换,仍然是求最大子数组和。如果暴力枚举交换的两个元素,再求最大子数组和会到O(n^3)。尝试过先求普通最大子数组和,再贪心怎么换掉元素之类的,都能找到反例。这个题其实有O(n)的算法,是个经典的dp,如果没理解(1.2)的话或者(1.2)靠死记硬背的话,无法解决这个问题。这是codility的challenge

http://blog.csdn.net/caopengcs/article/details/36899787


(3)  绝对值最小的子数组的和

就是求一个子数组,和的绝对值最小。

 这是codility的练习……,这个无法线性解决。而且死记硬背(1)的话也没用,也不能套用。这个需要用(1.3)的思路,子数组是两个前缀的差,那么我们对每个前缀找到之前的最接近它的前缀就可以了。“最接近”包括比它小的最大的,和比它大的最小的,这个用一个set就可以解决了。stl有著名的lower_bound函数,就是logn时间完成这个事,不然就要自己写二叉树了……

上个代码:

// you can write to stdout for debugging purposes, e.g.
// cout << "this is a debug message" << endl;

#include <set>

int solution(vector<int> &A) {
    // write your code in C++11
    set<int> have;
    have.insert(0);
    int sum = 0, r = 2000000000;
    for (int i = 0;r && (i < A.size()); ++i) {
        set<int>::iterator t = have.lower_bound(sum += A[i]);
        if (t != have.end()) {
            r = min(r, *t - sum);
        }
        if (t != have.begin()) {
            r = min(r, sum - *(--t));
        }
        have.insert(sum);
    }
    return r;
}



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值