Before the Beginning

本文为 Clouder 原创文章,原文链接为 https://www.codein.icu/gci-subarray/,转载时请将本段放在文章开头显眼处。如进行了二次创作,请明确标明。

前言

在颓废地水 Telegram 的时候,在 Codeforces 群里看到有人发了张谷歌面试的题的图,还是有那么一些意思的,向神犇求助后有所收获,写一篇题解。

题目难度不大,如何优雅地解决才是问题。

题面

给定一个无序数组 \(A\),长度为 \(N\),元素皆为非负整数,要求找到一段连续的子序列使得其和为 \(S\)。

思路

暴力的思路非常简单,枚举左右端点乱搞就是了。复杂度大概是 \(O(n^3)\) 的。

考虑稍加优化,预处理出前缀和,依然枚举左右端点,复杂度为 \(O(n^2)\)。

这是最直观的想法了,然而要求复杂度为 \(O(n)\),就必须找到更优的算法。

哈希表法

既然有了前缀和,那么这一段子序列可以用数学语言来表示一下:

\(S = s_i - s_j(j \leq i)\)

其中 \(s\) 代表前缀和。

稍加变换,就可以变为:

\(s_i - S = s_j(j \leq i)\)

问题转化为是否存在 \(j \in [1,i]\) 使得 \(s_j = s_i - S\)。

那么可以顺着扫一遍,判断之前是否有 \(s_j = s_i - S\),再将 \(s_i\) 的值记录下来。

伪代码:

for(int i = 1;i<=n;++i)
{
s[i] = s[i - 1] + a[i];
if(map[s[i] - S] != 0)
return make_pair(map[s[i] - S] + 1,i);
map[s[i]] = i;
}

那么复杂度的瓶颈就在于这个 map 如何实现了。使用红黑树可以做到稳定的 \(O(n\log n)\),而使用哈希表可以做到 \(O(n)\)。

然而哈希表的复杂度相当玄学,并且在元素值域过大时表现并不好。

有没有更稳定的、优雅的解决方法呢?

双指针扫描法

这是与神犇讨论后产生的解法,笔者认为相当优雅,并且顺路膜拜了神犇。

双指针扫描发,或者说对撞指针法?网上的资料较少,我只能大致讲一下。

拿经典的两数之和来举例子吧。

首先保证数组有序,要求找到两个数和为定值。

那么初始化左指针为数组开头,右指针为数组末尾。

判断两数相加,若大于目标值,则右指针左移。若小于目标值,则左指针右移。

那么两个指针重合时终止。很容易证明复杂度为 \(O(n)\)。

相信这个还是很容易理解的。

那么这道题,只是将两数之和变成了两数之差,也可以使用相类似的双指针法。

要求:

\(S = s_i - s_j(j \leq i)\)

先预处理出前缀和数组,由于元素都是非负整数,前缀和数组天然单调递增。

发现右指针右移时单调递增,左指针右移时单调递减,因此满足了单调性。

如果空数组也是可选的,那么右指针初始和左指针位置相同。

伪代码:

int lp = 0,rp = 0;
while(lp <= rp && lp >= 0 && rp <= n)
{
if(s[rp] - s[lp] == S)
return make_pair(lp + 1,rp);
if(s[rp] - s[lp] < S)
++rp;
else
++lp;
}

那么复杂度就是相当稳定的 \(O(n)\)了。

双指针扫描法证明

至于双指针法的正确性,感性理解很容易,但严谨证明,笔者觉得还是有些难度的。(当然是笔者太弱了)

在借鉴了 chend大佬的两数之和正确性证明 后,笔者也尝试自证一下。

使用数学归纳法证明算法运行过程中 \(\forall a \in [0,L],b \in [L+1,R]\),\(s_b - s_a \neq S\)。

  1. 初始时,不考虑空数组的情况,从 \(L = 0,R = 1\) 开始,若成立则算法退出,否则命题成立。
  2. 假定 \(\forall a \in [0,L],b \in [L+1,R]\) 中命题已成立,

    欲证 \(\forall a \in [0,L+1],b \in [L+2,R]\) 中命题成立,

    若 \(s_{R} - s_{L+1} = S\) 则算法结束,因此要证明命题,

    即证 \(\forall b \in [L+2,R]\) 都有 \(s_b - s_{L+1} \neq S\)。

    使用反证法证明,假定 \(\forall b \in [L+2,R]\) 有 \(s_b - s_{L+1} = S\),若 \(b = R\) 则算法已结束,因此 \(b \in [L+2,R - 1]\)。

    那么由单调性,有 \(s_b - s_{L} >= S\),且如果取等号则算法在先前已结束,因此 \(s_b - s_L > S\)。

    根据定义,当 \(s_b - s_L > S\) 时,右指针将会固定在 \(b\) 的正确位置,左指针会直接移动到 \(L+1\),而右指针不会到达当前的 \(R\) 的位置,矛盾。

    因此在算法运行过程中,若 \(a \in [0,L],b \in[L+1,R]\) 中命题成立,则\(a \in [0,L+1],b \in [L+2,R]\) 中命题成立。
  3. 同理可证明 \(a \in [0,L],b \in [L+1,R+1]\) 中命题成立。

那么在算法运行过程中,根据定义移动指针可始终保证命题成立,不会漏掉 \(s_b - s_a = S\) 的情况。

由于笔者水平问题,证明并不严谨,读者可看大佬原文自行证明。

结语

做题容易,优雅地切题难,切完要证更难啊……

对指点笔者的两位神犇表达膜拜之情。

附上代码包,包含两种方法和数据生成器、检验器和对拍器。

为了实现方便,哈希表使用了 map 容器来代替。

蓝奏云下载

[GoogleInterview]连续子序列问题的更多相关文章

  1. lintcode 最长上升连续子序列 II(二维最长上升连续序列)

    题目链接:http://www.lintcode.com/zh-cn/problem/longest-increasing-continuous-subsequence-ii/ 最长上升连续子序列 I ...

  2. 最大连续子序列乘积(DP)

    题目来源:小米手机2013年校园招聘笔试题 题目描述: 给定一个浮点数序列(可能有正数.0和负数),求出一个最大的连续子序列乘积. 输入: 输入可能包含多个测试样例.每个测试样例的第一行仅包含正整数 ...

  3. DP专题训练之HDU 1231 最大连续子序列

    Description 给定K个整数的序列{ N1, N2, ..., NK },其任意连续子序列可表示为{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j < ...

  4. HDU 1231 最大连续子序列(水题)

    题目链接: 传送门 最大连续子序列 Time Limit: 1000MS     Memory Limit: 32768 K Description 给定K个整数的序列{ N1, N2, ..., N ...

  5. HDU-1231 简单dp,连续子序列最大和,水

    1.HDU-1231 2.链接:http://acm.hdu.edu.cn/showproblem.php?pid=1231 3.总结:水 题意:连续子序列最大和 #include<iostre ...

  6. HDU 1231:最大连续子序列 解题报告

    第一次写博客, 自己总结写出了一道题感觉值得保存. 自己总结的规律:求最大连续子序列, 可以先求包括第N项在内的前N项最大值, (因为每一项都求过后, 前N项最大值最大的那一个元素所连续的序列即为最大 ...

  7. [ACM_其他] 总和不小于S的连续子序列的长度的最小值——尺缩法

    Description: 给定长度为n的整数数列,A[0],A[1],A[2]….A[n-1]以及整数S,求出总和不小于S的连续子序列的长度的最小值.如果解不存在,则输出0. Input: 输入数据有 ...

  8. UVa 108 - Maximum Sum(最大连续子序列)

    题目来源:https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=3&pa ...

  9. HDU 1231 最大连续子序列 &&HDU 1003Max Sum (区间dp问题)

    C - 最大连续子序列 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u Submit ...

随机推荐

  1. springAOP实现原理

    spring AOP实现原理, spring 会在初始化的时候,创建一个BeanPostProcessor(AnnotationAwareAspectJAutoProxyCreator)用来为类注入切 ...

  2. 虚拟交换系统-VSS

    1.虚拟交换系统VSS技术概述 VSS的特点: VSS将两台Catalyst 6500/4500系列交换机组合为单一虚拟交换机,对外来看,只有一台交换机,管理冗余链路如同管理自己的一个单一接口. VS ...

  3. 软件版本 Alpha、Beta、Rc

    软件版本的周期 α.β.γ 表示软件测试中的三个阶段 α :第一阶段,内部测试使用 β: 第二阶段,消除了大部分不完善的地方,仍可能存在漏洞,一般提供给特定的用户使用 γ: 第三阶段,产品成熟,个别地 ...

  4. 通过aptitude降级包解决依赖问题(E:无法修正错误,因为您要求某些软件包保持现状)

    Linux下的依赖关系令人头疼,尤其是提示如下错误的时候: 下列软件包有未满足的依赖关系: xxx : 依赖: xxx 但是它将不会被安装 E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破 ...

  5. [原]NTP时钟同步服务设置

    服务器列表 192.168.0.2 ntp服务端 192.168.0.3 ntp客户端 192.168.0.4 ntp客户端 192.168.0.5 ntp客户端 注:以下操作均以root操作 一.N ...

  6. CSS各种小技巧

    /* *背景的透明度设置 */ -moz-opacity: 0.8; opacity:.80; filter: alpha(opacity=80); 待续...

  7. Python 中命令行参数解析工具 docopt 安装和应用

    什么是 docopt? 1.docopt 是一种 Python 编写的命令行执行脚本的交互语言. 它是一种语言! 它是一种语言! 它是一种语言! 2.使用这种语言可以在自己的脚本中,添加一些规则限制. ...

  8. GO 空白标识符 _

    空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃. _ 实际上是一个只写变量,你不能得到它的值.这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用 ...

  9. 进程fork

    fork用于父进程创建一个子进程 返回两次 返回-1表示错误 父进程中返回创建子进程的ID,大于0 返回0是表示进入子进程 创建的子进程会继承父进程的属性,比如打开的文件描述符.工作目录.根目录等等. ...

  10. NGINX学习积累(学习牛人)

    大牛:http://www.cnblogs.com/zengkefu/p/5563608.html 当请求来临的时候,NGINX会选择进入虚拟主机,匹配location后,进入请求处理阶段. 在请求处 ...