序列相关的趣题 之中的一个
闲话少叙。直接上题。
(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; }