题目描述
从一个长度为N的正数数组numbers中找出长度至少为L且几何平均值最大子数组,并输出其位置和大小。(K个数的几何平均值为K个数的乘积的K次方根)
若有多个子数组的几何平均值均为最大值,则输出长度最小的子数组。
若有多个长度相同的子数组的几何平均值均为最大值,则输出最前面的子数组。
输入描述
第一行输入为N、L
N表示numbers的大小(1 ≤ N ≤ 100000)
L表示子数组的最小长度(1 ≤ L ≤ N)
之后N行表示numbers中的N个数,每个一行(10^-9 ≤ numbers[i] ≤ 10^9)
输出描述
输出子数组的位置(从0开始计数)和大小,中间用一个空格隔开。
备注
用例保证除几何平均值为最大值的子数组外,其他子数组的几何平均值至少比最大值小10^-10倍
示例1
输入
3 2
2
2
3
输出
1 2
说明
长度至少为2的子数组共三个,分别是{2,2}、{2,3}、{2,2,3},其中{2,3}的几何平均值最大,故输出其位置1和长度2
示例2
输入
10 2
0.2
0.1
0.2
0.2
0.2
0.1
0.2
0.2
0.2
0.2
输出
2 2
说明
有多个长度至少为2的子数组的几何平均值为0.2,其中长度最短的为2,也有多个,长度为2且几何平均值为0.2的子数组最前面的那个为从第二个数开始的两个0.2组成的子数组
思路
核心逻辑
- 数学转化:将几何平均值最大化问题转化为对数和平均值最大化问题(
几何平均值=(x1*x2*...*xk)^(1/k)→ 取对数得(lnx1+lnx2+...+lnxk)/k),避免大数乘积溢出; - 二分答案:二分最大几何平均值的对数(上下限为数组元素对数的最小/最大值),通过验证函数判断是否存在长度≥L的子数组满足几何平均值≥当前二分值;
- 验证优化:构造转换数组
b[i]=ln(num[i])-目标对数,利用前缀和+维护最小前缀和,O(n)验证是否存在符合条件的子数组; - 结果筛选:找到最大几何平均值后,遍历所有可能子数组,筛选出「长度最短、起始位置最靠前」的最优子数组。
浮点数二分技巧
- 迭代固定次数(80次)保证精度(满足题目1e-10的差值要求);
- 验证时引入极小误差容忍(1e-12),避免浮点精度误差导致误判。
时空复杂度
- 时间复杂度:O(N×log(max_gm/eps)),其中 N 为数组长度,log(max_gm/eps) 为二分迭代次数(约80次);验证环节每次O(N),结果筛选环节最坏O(N×L)(实际找到最短长度后立即break,远低于该上界);
- 空间复杂度:O(N),主要用于存储对数数组、前缀和数组等。
补充说明
- 二分直接针对「几何平均值的对数」而非原值,减少浮点运算误差;
- 验证环节通过维护「前j-L个前缀和的最小值」,将暴力验证的O(N×L)优化为O(N),适配N=1e5的规模要求;
- 结果筛选优先遍历短长度子数组,找到第一个符合条件的即终止,保证「最短长度+最靠前位置」的规则。
##代码
function solve(lines) {
try {
let ptr = 0;
const [N, L] = lines[ptr++].split(' ').map(Number);
const numbers = [];
for (let i = 0; i < N; i++) {
const num = parseFloat(lines[ptr++]);
if (num <= 0) {
throw new Error("Non-positive number");
}
numbers.push(num);
}
// 对数数组
const logA = numbers.map(x => Math.log(x));
const logMin = Math.min(...logA);
const logMax = Math.max(...logA);
const eps = 1e-12;
// 判断是否存在长度 >= L 的子数组,其几何平均值 >= exp(targetLog)
function existsValidSubarray(targetLog) {
// 构造 b[i] = logA[i] - targetLog
// 前缀和 prefix[0] = 0, prefix[i] = sum_{0}^{i-1} b[t]
let prefix = 0;
let minPrefix = 0; // prefix[0]
let minIndex = 0; // 对应 prefix[0] 的位置(虚拟位置 -1)
// 我们维护一个滑动的最小前缀,只考虑位置 <= current - L
const prefixHistory = [0]; // prefixHistory[i] = prefix at position i (i from 0 to N)
for (let i = 0; i < N; i++) {
prefix += logA[i] - targetLog;
prefixHistory.push(prefix);
// 当前右端点是 i(0-based),子数组结束于 i,长度 >= L ⇒ 起始 <= i - L + 1
// 所以前缀起始点 j 满足 j <= i - L + 1 ⇒ j <= (i + 1) - L
// prefixHistory[j] 对应子数组 [j, i]
if (i + 1 >= L) {
// 更新 minPrefix 为 min(prefixHistory[0..(i+1)-L])
const candidateIdx = (i + 1) - L;
if (prefixHistory[candidateIdx] < minPrefix) {
minPrefix = prefixHistory[candidateIdx];
minIndex = candidateIdx;
}
if (prefix - minPrefix >= -eps) {
return true;
}
}
}
return false;
}
// 二分最大可能的几何平均值的对数
let low = logMin;
let high = logMax;
for (let iter = 0; iter < 80; iter++) {
const mid = (low + high) / 2;
if (existsValidSubarray(mid)) {
low = mid;
} else {
high = mid;
}
}
// 现在 low 是最大几何平均值的对数(近似)
// 再次扫描,找出所有满足几何平均 >= exp(low) 的子数组中:
// - 长度最短
// - 若长度相同,起始下标最小(0-based)
const bestLog = low;
let bestLen = N + 1;
let bestStart = -1;
// 重新计算 b[i] = logA[i] - bestLog
const b = logA.map(x => x - bestLog);
const prefix = new Array(N + 1).fill(0);
for (let i = 0; i < N; i++) {
prefix[i + 1] = prefix[i] + b[i];
}
// 对每个起始点 i,找最小的 j >= i + L 使得 prefix[j] - prefix[i] >= 0
for (let i = 0; i <= N - L; i++) {
for (let j = i + L; j <= N; j++) {
if (prefix[j] - prefix[i] >= -eps) {
const len = j - i;
if (len < bestLen || (len === bestLen && i < bestStart)) {
bestLen = len;
bestStart = i;
}
break; // 找到最短即可
}
}
}
if (bestStart === -1) {
// 理论上不会发生
console.log("[]");
} else {
// 输出:起始下标(0-based)和长度
console.log(`${bestStart} ${bestLen}`);
}
} catch (e) {
console.log("[]");
}
}
// ===== 测试 =====
let inputs = [
`3 2
2
2
3`,
`10 2
0.2
0.1
0.2
0.2
0.2
0.1
0.2
0.2
0.2
0.2`
];
inputs.forEach(input => {
solve(input.split('\n'));
});
585

被折叠的 条评论
为什么被折叠?



