洛谷 P5745 【深基附B例】区间最大和 题解 与 算法时间复杂度分析
洛谷 P5745 【深基附B例】区间最大和 题解 与 算法时间复杂度分析
在算法竞赛中,我们时常因为 TLE 而丢分,所以时间复杂度是算法竞赛中十分重要的一部分。本文将通过洛谷 P5745 【深基附B例】区间最大和来介绍时间复杂度的计算。
题目描述
给定 \(n\) 个正整数组成的数列 \(a_1, a_2, \cdots, a_n\) 和一个整数 \(m\)。求出这个数列中的一个子区间 \([i, j]\),也就是在这个数列中连续的数字 \(a_i, a_{i + 1}, \cdots, a_{j - 1}, a_j\),使得这个子区间的和在不超过 \(m\) 的情况下最大。如果有多个区间符合要求,输出 \(i\) 最小的那一个。
说明/提示
本题采用捆绑测试
子任务 1(10分):\(n\le 200\) ;
子任务 2(20分):\(n\le 3000\) ;
子任务 3(30分):\(n\le 10^5\) ;
子任务 4(40分):\(n\le 4\times 10^6\) 。
对于 \(100\%\) 的数据,\(1 \leq n \leq 4 \times 10^6\),\(1 \leq m \leq 10^9\),\(0 \leq a_1, a_2, \cdots, a_n \leq 10^5\)。
思路 1
枚举所有子区间的头尾 \(i\) 和 \(j\),然后对子区间里面所有的数字累加求和,再判断是否在不大于 \(m\) 的基础上最大。
代码实现
#include <iostream>
using namespace std;
int a[4000010];
int n, m;
int ansmax = 0, ansi, ansj;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++) {
int sum = 0;
for (int k = i; k <= j; k++) sum += a[k];
if (sum <= m && sum > ansmax)
ansmax = sum, ansi = i, ansj = j;
}
cout << ansi << " " << ansj << " " << ansmax << endl;
return 0;
}
时间复杂度分析
程序的运行时间与代码的运行次数有关。其中,运行次数最多的代码是 sum += a[k];。下面,我们来计算这条语句的运行次数:
其中,对运行次数影响最大的是 \(n^3\),所以这个算法的时间复杂度是 \(\mathcal O(n^3)\)。
但是,数学计算太复杂了(其实是蒟蒻数学太菜),有什么更简单的方法吗?可以估计程序的时间复杂度。这个代码有三重循环:
- 第一重循环 \(i\) 要执行 \(n\) 次;
- 第二重循环 \(j\) 要执行 \(n-i+1\) 次;
- 第三重循环 \(k\) 要执行 \(j-i+1\) 次。
其中,\(i,j,k\) 均与 \(n\) 同阶(与 \(n\) 增长速度相同),所以时间复杂度为 \(\mathcal O(n^3)\)。
一台家用计算机每秒运算次数约为 \(10^8\)。当 \(n=200\) 时,\(n^3=8\times 10^6\);当 \(n=3000\) 时,\(n^3=2.7\times 10^{10}>10^8\),无法胜任子任务2。
评测结果:10pts,与预测的相同。
思路 2
每次都计算子区间和太麻烦,可以计算前缀和来减少运算次数。
代码实现
#include <iostream>
using namespace std;
int a[4000010], s[4000010];
int n, m;
int ansmax = 0, ansi, ansj;
int main() {
cin >> n >> m;
s[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++) {
int sum = s[j] - s[i - 1];
if (sum <= m && sum > ansmax)
ansmax = sum, ansi = i, ansj = j;
}
cout << ansi << " " << ansj << " " << ansmax << endl;
return 0;
}
时间复杂度分析
很明显的双重循环,每次循环次数均与 \(n\) 同阶,所以时间复杂度为 \(\mathcal O(n^2)\)。
当 \(n=3000\) 时,\(n^2=9\times 10^6<10^8\);当 \(n=10^5\) 时,\(n^2=10^{10}>10^8\),无法胜任子任务3。
评测结果:30pts
思路 3
因为 s 数组是单调不下降的,所以可以使用二分来解决这道题。我们可以固定区间左端点 \(i\),二分查找满足条件的右端点 \(j\)。具体代码实现如下:
#include <iostream>
using namespace std;
int a[4000010];
long long s[4000010];
int n, m;
long long ansmax = 0;
int ansi, ansj;
int find(long long x) {
int l = 1, r = n + 1;
while (l < r) {
int mid = l + (r - l) / 2;
if (s[mid] >= x) r = mid;
else l = mid + 1;
}
if (s[l] == x) return l;
else return l - 1;
}
int main() {
cin >> n >> m;
s[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
for (int i = 1; i <= n; i++) {
long long x = s[i - 1] + m;
int j = find(x);
long long sum = s[j] - s[i - 1];
if (sum <= m && sum > ansmax)
ansmax = sum, ansi = i, ansj = j;
}
cout << ansi << " " << ansj << " " << ansmax << endl;
return 0;
}
时间复杂度分析
这个代码看似只有一重循环,但别忘了还有 find 函数。外层循环执行 \(n\) 次,find 函数使用二分查找,需要 \(\log_2n\) 次比较,所以时间复杂度为 \(\mathcal O(n\log n)\)(无需在意底数是多少,通过换底公式变换之后只是常数倍的差别)
当 \(n=10^5\) 时,\(n\log_2 n\approx 1.66\times 10^6\);当 \(n=4\times 10^6\) 时,\(n\log_2 n\approx 8.77\times 10^7\),接近运算的极限(但洛谷评测机性能好,可以AC)。
评测结果:cin 100pts,快读getchar 100pts
思路 4
可以维护一个队列,依次将 a 数组中的元素入队,直到加入下一个数字和会超过 \(m\) 为止。计算子区间和并弹出队首。
代码实现
#include <iostream>
using namespace std;
int a[4000010];
int n, m;
long long ansmax = 0, sum = 0;
int l = 1, r = 1, ansi, ansj; // l 和 r 分别是队首和队尾指针
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
while (l <= n) {
while (r <= n && sum + a[r] <= m)
sum += a[r], r++; // 入队
if (sum <= m && sum > ansmax)
ansmax = sum, ansi = l, ansj = r - 1;
sum -= a[l], l++; // 出队
}
cout << ansi << " " << ansj << " " << ansmax << endl;
return 0;
}
时间复杂度分析
这里看似是双重循环,但每个元素只入队一次,所以时间复杂度为 \(\mathcal O(n)\),完全可以在 1 秒内输出结果。
评测结果:cin 100pts,快读getchar 100pts
这里可以发现用 cin 实现 \(\mathcal O(n)\) 算法的运行时间甚至比使用快读实现的 \(\mathcal O(n\log n)\) 还要慢。类似快读这样在不改变算法时间复杂度的前提下提高程序运行效率的优化方法叫做常数优化。常数优化是进阶选手的必备技能,在这里不再赘述,感兴趣的可以上网查找相关内容学习。

浙公网安备 33010602011771号