洛谷P1404 平均數
題目描述
給一個長度為 \(n\) 的數列,我們需要找出該數列的一個子串,使得子串平均數最大化,並且子串長度 \(\geq m\) 。
輸入格式
第一行兩個整數 \(n\) 和 \(m\)。
接下來 \(n\) 行,每行一個整數 \(a_i\),表示序列第 \(i\) 個數字。
輸出格式
一個整數,表示最大平均數的 \(1000\) 倍,如果末尾有小數,直接捨去,不要用四捨五入求整。
資料規模
對於 \(60\%\) 的資料,保證 \(m \leq n \leq 10^4\);
對於 \(100\%\) 的資料,保證 \(1 \leq a_i \leq 2000\)。
題解
樸素的思想是列舉所有的子串,找到最大平均數,時間複雜度 \(O(n^2)\),無法透過此題。
考慮最佳化,本題的特殊限制條件是子串長度不得小於 \(m\) 。如何保證這一條件滿足呢?只需要先找到所有長度為 \(m\) 的子串,再在這些子串的基礎上向前或向後擴充套件即可得到所有符合要求的子串。透過列舉子串的第一個元素的位置 \(i\) 易得子串的最後一個元素位置為 \(i + m - 1\),故可方便的在 \(O(n)\) 的複雜度內列舉長度為 \(m\) 的子串。
考慮擴充套件子串,如果向後擴充套件的同時向前擴充套件,則向後擴充套件的子串會和後續子串向前擴充套件的子串重合,形成列舉的重複,故只向前或向後擴充套件。此處採用向前擴充套件。最佳化的關鍵是在 \(O(\log n)\) 的複雜度內求出最優的擴充套件方法。顯然列舉的複雜度是 \(O(n)\) 不符合要求。筆者的最初想法是使用字首的最大平均值進行擴充套件,只需要 \(O(1)\) 的時間複雜度。但對於單個最高的數來說,可能選擇多個較小的數的情況更優,因為兩組數的並的平均值不等於兩組數平均值的平均值,故作罷。那麼還有什麼方法可以過濾沒有必要的擴充套件呢?
考慮二分答案。對於一個選定的基準數作為平均數的情況考慮擴充套件,易得以下擴充套件策略:
- 字首的平均數小於基準數,捨棄字首;
- 字首的平均數大於等於基準數,則保留字首。
因此,只需要在列舉長度為 \(m\) 的子串的同時用以上策略擴充套件即可得到基準數確定時的最優情況。二分的時間複雜度為 \(O(\log a)\),驗證的時間複雜度為 \(O(n)\), 總時間複雜度為 \(O(n \log a)\) ,可以解決此題。
AC程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int MAXN = 1e6 + 3;
int n, m;
double a[MAXN];
double sum = 0.0; //維護只取m個數的和
bool check(double x) {
sum = 0.0;
for (int i = 1; i <= m; i++) {
sum += a[i];
}
double ans = 0.0;
double preSum = 0.0; //維護往前延伸的最大平均數的和(分子)
double preNum = 0.0; //維護往前延伸的最大平均數的數量(分母)
for (int i = 1; i <= n - m + 1; i++) { //從m開始列舉子串的第一個數的位置
double preAvg = 0.0; //往前延伸的最大平均數
if (preNum != 0) preAvg = preSum / preNum;
if (preAvg < x) preSum = a[i - 1], preNum = 1.0; //之前的平均數達不到x,則只保留新出現的數
else preSum += a[i - 1], preNum += 1.0; //否則在保留之前的字首的基礎上加上新出現的數
ans = max(ans, max((sum + preSum) / ((double)m + preNum), sum / (double)m)); //更新答案
sum -= a[i]; //減去子串的第一個數
sum += a[i + m]; //加上新出現的一個數
}
return ans >= x;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
double l = 0, r = 2000, mid;
while (l + 1e-5 < r) {
mid = (l + r) / 2.0;
if (check (mid)) l = mid;
else r = mid;
}
cout << (int)(r * 1000.0) << '\n';
return 0;
}