NC15553题解
Xilong Yang

NC15553 数学考试,对理解前缀和与枚举很有好处。 题目链接:NC15553 数学考试——牛客网

题目描述

今天qwb要参加一个数学考试,这套试卷一共有n道题,每道题qwb能获得的分数为ai,qwb并不打算把这些题全做完, 他想选总共2k道题来做,并且期望他能获得的分数尽可能的大,他准备选2个不连续的长度为k的区间, 即[L,L+1,L+2,....,L+k-1],[R,R+1,R+2,...,R+k-1](R >= L+k)。

输入描述:

1
2
3
第一行一个整数T(T<=10),代表有T组数据
接下来一行两个整数n,k,(1<=n<=200,000),(1<=k,2k <= n)
接下来一行n个整数a1,a2,...,an,(-100,000<=ai<=100,000)

输出描述:

1
输出一个整数,qwb能获得的最大分数

示例:

输入

1
2
3
4
5
2
6 3
1 1 1 1 1 1
8 2
-1 0 2 -1 -1 2 3 -1

输出

1
2
6
7

分析

首先需要解决的是如何确定一个区间。一个区间显然可以用[左端点, 右端点]这样的数对来确定,而本题中区间的长度是给定的,因此只要用左端点与右端点中的一个点就能确定一个区间,这里我们选用左端点。

然后考虑暴力解法,这题如果用暴力来做,需要O(n2)的复杂度遍历每种可能的区间组合。以及O(k)的复杂度计算每个区间的和。则总的时间复杂度为O(kn2),最坏情况下kn^2 = 4 x 10^15,显然无法在可接受的时间内完成运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//main
//暴力枚举区间组合,求解最大值
for(i : n - k)
{
for(j = i + k : n - k)
{
//Sum为求和函数
maxCount = max(maxCount, Sum(i) + Sum(j))
}
}
//Sum(i)
{
count = 0
for(i : i + k)
{
count += a[i]
}
return count
}

看见区间的和可以想到前缀和,通过维护一个前缀和来将每个区间的和的计算时间复杂度降为O(1),这样整个问题的时间复杂度就降为了枚举的O(n2),最坏情况下n2 = 4 x 10^10,仍然无法在可接受的时间内完成运算。

1
2
3
4
5
6
7
8
9
10
//main
//暴力枚举区间组合,求解最大值
for(i : n - k)
{
for(j = i + k : n - k)
{
//sum为维护的区间和
maxCount = max(maxCount, sum[i] + sum[j]
}
}

我们规定j一定大于i,由此观察遍历过程

image-20201011145807948

两次遍历中有很多重合部分,对于寻找最大值而言是没有必要的。因为若前进一步后的i' < i,则它们分别加上同一个数后大小关系不变。而变化后i仍在有效取值范围内,因此,维护一个maxi的值即可省略对灰色部分的遍历。

image-20201011151108260

对于j'以后的值无需判断,因为max'定不小于max且j'后的值max和max'都可取用,所以不可能存在max + jx > max' + jx(jx在j'之后)。

image-20201011152355021

因此我们仅需考虑这一部分,其中对于step2,max' + j'即是这一步的最大值,将其与上一步的最大值比较来更新maxCount,再将step2看做step1对其进行相同操作,最后即可求出maxCount的值。

1
2
3
4
5
6
7
maxi = sum[0];
maxCount = maxi + sum[k];
for(i : n - k)
{
maxi = max(maxi, sum[i]);
maxCount = max(maxCount, maxi + sum[i + k]);
}

时间复杂度O(n),满足题目要求。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
const ll infinity = 3e10;

int main()
{
ll t;
cin >> t;
while(t--)
{
ll n, k;
cin >> n >> k;
//输入时即维护前缀和
vector<ll> sum(n, 0);
cin >> sum[0];
for(ll i = 1; i < n; ++i)
{
cin >> sum[i];
sum[i] += sum[i - 1];
}

//跟据前缀和求出每个区间的和
vector<ll> kSum(n + 1 - k, 0);
kSum[0] = sum[k - 1];
for(ll i = 1; i < n + 1 - k; ++i)
{
kSum[i] = sum[i - 1 + k] - sum[i - 1];
}

//枚举求最大值
ll curMax = infinity * -1;
ll count = infinity * -1;
for(ll i = 0; i <= n - 2 * k; ++i)
{
curMax = max(curMax, kSum[i]);
count = max(count, curMax + kSum[i + k]);
}
cout << count << endl;
}
return 0;
}