寻找段落
原题目:
luogu-P1419
简要描述:
给定一个长度为n的序列,求出在所有长度在[S,T]之间的连续子序列中,平均值最大的那个子序列的平均值。
题目描述
给定一个长度为
解题思路
问题分析
显然可以暴力,你可以先写一个暴力代码
n = 1e5,暗示我们这个题目是
显然这里用到了平均数的性质,所有我们需要看看这个性质是什么,需要去算.
二分答案的可行性
可以想到
- 答案是上下界的.o
现在思考,答案是否具有二分性
---------------- Real_ANS ---------------------
^
|
|
估算的值est --+
Estimate: 估算
如果估算的值 est 在Real_ANS的左边,则必然存在段区间 est。
如果我们能在est 是在 Real_ANS的左侧,还是右侧,那么这个题目就可以是用二分。
数学变换
因为我们不知道 Real_ANS是多少,
那么我们在 不知道Real_ANS的值的情况下判断 est 的左侧还是右侧吗?
那么关键就在
然后创建一个数组 b[i]:
通过构造这个新数组
如果存在这样的段落,说明这个 est 是可行的(甚至还可以更大);如果不存在,说明 est 太大了。为了快速判断是否存在这样的段落,我们需要频繁计算一段区间
显然是前缀和
结合前缀和公式,这个条件就变成了:
即:
可以想到固定i,然后取枚举
显然j-1的范围是
那么我们可以枚举i,然后取
直觉理解
- 直觉: 把a数组的每个数值想象成一个柱子,柱子的高度代表
a[i]的值.- 如果所有的柱子都削去
est后, 还存在区间和>=0, 说明est在Real_ANS的左边- 问题变成: 最大区间和, 朴素方法: 枚举
- 区间和显然使用 : 前缀和
- 枚举优化: 固定 结尾i,求
S[i] - S[j-1]是否存在>=0的- 是否存在 转成 找到
S[j-1]的最小值- 转成求对应区间
[i-T,i-S]最小值
区间最值问题
问题变成的区间最值问题
- 显然我们可以用单调队列来维护这个最小值
- 也可以是用线段树来维护这个最小值
- 也可以是用ST表来维护这个最小值
- 也可以是用分块来维护这个最小值
- 也可以是用树状数组来维护这个最小值
单调队列优化
这里只能用 单调队列 因为时间.
算法步骤
- 二分答案,设定上下界
- 对于每个猜测的平均值
: - 构造数组
- 计算前缀和数组
- 使用单调队列维护长度为
的滑动窗口最小值 - 检查是否存在合法区间使得区间和
- 构造数组
- 根据检查结果调整二分边界
时间复杂度
- 二分答案:
- 每次检查:
- 总体时间复杂度:
这真是一个好问题, 最难的部分就是思维转换, 把问题变成求区间最值问题
代码实现
cpp
copy
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* Author by Rainboy blog: https://rainboylv.com github : https://github.com/rainboylvx
* date: 2025-12-24 19:40:34
* * 算法核心:二分答案 (Binary Search on Answer) + 单调队列优化 (Monotonic Queue Optimization)
* 复杂度:O(N * log(Answer_Range / EPS))
*/
#include <bits/stdc++.h>
#include <iomanip>
#include <ios>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 2e6+5;
int n,s,t;
int a[maxn];
double b[maxn], sum[maxn]; // b: 减去二分值后的数组, sum: b数组的前缀和
const double EPS = 1e-5; // 精度控制,用于实数二分结束条件
// 构造辅助数组 b 和前缀和 sum
// 核心思想:原式 (sum[i]-sum[j])/(i-j) >= est
// 变形为:(sum[i]-sum[j]) >= est * (i-j)
// 移项得:(sum[i] - i*est) - (sum[j] - j*est) >= 0
// 这里的 b[k] = a[k] - est 其实就是把每个元素先减去平均值
// 只要找到一段区间和 >= 0,就说明平均值 >= est
void build_arr_b(double est) {
for(int i = 1; i <= n; ++i)
{
b[i] = a[i] - est; // 每个元素减去猜测的平均值
sum[i] = sum[i-1] + b[i]; // 计算新数组的前缀和
}
}
// 辅助函数:计算区间和 (当前代码未直接使用,保留作为逻辑参考)
double sum_range(int l,int r) { return sum[r] - sum[l-1]; }
void init(){
std::cin >> n;
std::cin >> s >> t;
for(int i = 1; i <= n; ++i)
{
std::cin >> a[i];
}
}
// 检查是否存在一个长度在 [S, T] 之间的子段,其平均值 >= est
bool check(double est) {
build_arr_b(est); // 根据当前的猜测值 est 重构前缀和
// 单调队列 q,存放的是下标 k
// 队列性质:队列内的 sum[k] 单调递增
// 队首 q.front() 始终是当前合法范围内 sum 值最小的那个下标
deque<int> q;
// i 代表子段的【结束位置】
// 子段范围是 [j+1, i],其中 j 是我们在单调队列里维护的【前驱位置】(即 sum[j] 中的 j)
// 长度限制:S <= i - j <= T ==> i - T <= j <= i - S
for(int i = s; i <= n; ++i)
{
// 1. 入队操作:尝试将新的候选前驱 (i-s) 加入队列
// 只有当 i >= s 时,(i-s) 才能构成至少长度为 s 的区间,是一个合法的起点前驱
int candidate = i - s;
// 维护单调性:如果新来的 sum[candidate] 比队尾更小,
// 那么队尾那些 sum 值大的元素永远不可能成为“最优解”(减数越小,结果越大),直接踢出
while( !q.empty() && sum[q.back()] >= sum[candidate] )
q.pop_back();
q.push_back(candidate);
// 2. 出队操作:移除“过期”的元素
// 如果队首的下标 < i-t,说明以该下标为起点的段落长度 > t,超出了最大长度限制
while( !q.empty() && q.front() < i-t )
q.pop_front();
// 3. 判定操作
if( !q.empty() ) {
// sum[i] - sum[q.front()] 表示以 i 结尾,以 q.front()+1 开始的区间和
// 如果这个区间和 >= 0,说明平均值 >= est,check 成功
if( sum[i] - sum[q.front()] >= 0 )
return true;
}
}
return false; // 遍历完所有可能并未找到,说明 est 偏大
}
// 二分查找答案
double bs_find(double l, double r) {
while (r - l > EPS) { // 当区间足够小时停止
double mid = (l + r) / 2;
if( check(mid) )
l = mid; // mid 可行,尝试更大的平均值 (答案在右边)
else
r = mid; // mid 不可行,答案在左边
}
return l;
}
signed main () {
ios::sync_with_stdio(false); cin.tie(0);
init();
// 确定二分范围:题目数据范围 -1e4 到 1e4
double max_val = 1e4 + 5;
double min_val = -1e4 - 5;
double ans = bs_find(min_val, max_val);
cout << fixed << setprecision(3) << ans << "\n";
return 0;
}