蓝桥杯 -拔河题解
题目
问题描述
小明是学校里的一名老师,他带的班级共有 nn 名同学,第 ii 名同学力量值为 aia**i。在闲暇之余,小明决定在班级里组织一场拔河比赛。
为了保证比赛的双方实力尽可能相近,需要在这 nn 名同学中挑选出两个队伍,队伍内的同学编号连续:{al1,al1+1,...,ar1−1,ar1}{a**l 1,a**l 1+1,...,a**r 1−1,a**r 1} 和 {al2,al2+1,...,ar2−1,ar2}{a**l 2,a**l 2+1,...,a**r 2−1,a**r 2},其中 l1≤r1<l2≤r2l 1≤r 1<l 2≤r2。
两个队伍的人数不必相同,但是需要让队伍内的同学们的力量值之和尽可能相近。请计算出力量值之和差距最小的挑选队伍的方式。
输入格式
输入共两行。
第一行为一个正整数 nn。
第二行为 nn 个正整数 aia**i。
输出格式
输出共一行,一个非负整数,表示两个队伍力量值之和的最小差距。
样例输入
text5 10 9 8 12 14
样例输出
text1
样例说明
其中一种最优选择方式:
队伍 1: {a1,a2,a3}{a 1,a 2,a 3},队伍 2:{a4,a5}{a 4,a5},力量值和分别为 10+9+8=2710+9+8=27 , 12+14=2612+14=26,差距为 ∣27−26∣=1∣27−26∣=1 。
评测用例规模与约定
对于 20%20% 的评测用例,保证 n≤50n≤50 。
对于 100%100% 的评测用例,保证 n≤103,ai≤109n ≤103,a**i≤109 。
思路
计算一个区间内的力量之和
计算一个区间内的力量之和需要使用前缀和。前缀和就是通过构造一个前缀和数组,构造方式为第 i i i个元素值为元素组中前 i i i个元素(包括第 i i i个元素)

这样的好处是计算区间 [ i , j ] [i,j] [i,j]值可以变成计算前缀和数组 a a a中 a [ j ] − a [ i − 1 ] a[j]-a[i-1] a[j]−a[i−1]的值。这样时间复杂度就变成 O ( 1 ) O(1) O(1)了
暴力求解
暴力求解的思路大概是定义四个指针,分别表示左区间左右端点 l 1 , r 1 l1,r1 l1,r1和右区间左右端点 l 2 , r 2 l2,r2 l2,r2。同时需要满足以下条件 l 1 < = r 1 , r 1 < l 2 , l 2 < = r 1 l1<=r1,r1<l2,l2<=r1 l1<=r1,r1<l2,l2<=r1,同时为了避免重复计算,可以用两个指针先遍历出每个区间的值。基于此可以写出下列代码
python
import bisect
n = int(input())
a = list(map(int, input().split()))
# 计算前缀和
prefix_sum = [0] * (n + 1)
for i in range(1, n + 1):
prefix_sum[i] = prefix_sum[i - 1] + a[i - 1]
value=[[0 for _ in range(n+1)] for _ in range(n+1)]
##先计算 降低时间复杂度
for i in range(1,n+1):
for j in range(i,n+1):
value[i][j]=prefix_sum[j]-prefix_sum[i-1]
ef=[]
# 降序排序。
for i in value:
ef.append(sorted(i))
min_diff = float('inf')
# 遍历
for l1 in range(1,n):
for r1 in range(l1,n):
for l2 in range(r1+1,n+1):
left=value[l1][r1]
index=bisect.bisect_left(ef[l2],left)
if index<len(ef[l2]):
min_diff=min(abs(ef[l2][index]-left),min_diff)
if index>1:
min_diff=min(abs(ef[l2][index-1]-left),min_diff)
print(min_diff)
在上述代码中如果使用4个指针那么时间复杂度就为 O ( n 4 ) O(n^4) O(n4),为了降低时间复杂度。这里先进行预处理。使用 e f ef ef存储计算出以i为该区间左端点的数据值。这里的 e f [ i ] [ j ] ef[i][j] ef[i][j]是指以 i i i为该区间左端点以j为右端点的区间的区间和 。实际使用的时候j的值不会影响结果。因此对每行排序从而方便二分查找。
接着遍历的时候时间复杂度就可以降低一个数量级为 O ( n 3 ) O(n^3) O(n3)。因为右区间的右端点不需要遍历。这是因为当确定了左区间值和右区间左端点的时候。只需要找到在使用该左端点的右区间中和左区间值最接近的值。而我们不关心这个右端点的值。在求出df数组下就可以直接使用二分查找这个值。
但是使用该思路时间复杂度还是达到 O ( n 3 ) O(n^3) O(n3),此外还有预处理数组ef需要 O ( n 2 ) O(n^2) O(n2)以及计算前缀和需要 O ( n ) O(n) O(n)可以忽略。时间复杂度过高。使用该代码只能通过 75 75 75% 的测试用例。

正解思路
正解代码极其简单。思路是先计算前缀和,然后通过前缀和得到所有区间的值。找到这些值里面相差最小的值。时间复杂度为 O ( n 2 ) O(n^2) O(n2)代码如下:
python
input()
data=list(map(int,input().split()))
predata=[data[0]]
for i in data:
predata.append(predata[-1]+i)
value=[]
# 遍历所有区间的值
for i in range(1,len(predata)):
for j in range(i,len(predata)):
value.append(predata[j]-predata[i-1])
value=sorted(value)
ans=1e11
# 无所谓重复,因为重复相当于不去中间重复部分
for i in range(len(value)-1):
ans=min(abs(value[i]-value[i+1]),ans)
print(ans)
可能你会有疑问:为什么这样可以通过,不考虑重复吗。
实际上,重复不影响结果。因为如果选中的两个区间重复了那么必定还有两个区间是这两个区间由该重复区间非重复段组成。具体来说就是,例如下图中两个重复区间:

那么取这两个区间中的非重复段得到的结果必定和两个非重复部分结果相同。换句话说就是可以去上述区间,那么也可以取下图这两个区间。那么这两个区间结果值和上面这两个重复区间的结果是相同的

因此可以不用考虑重复段。