1. 题目数据 (Problem Metadata)

2. 题意简述 (Problem Summary)

给定长度为 nn 的数组 AA 和长度为 mm 的数组 BB1n,m,q1051 \le n, m, q \le 10^5ai,bi109|a_i|, |b_i| \le 10^9)。共 qq 轮游戏,每轮给定 l1,r1,l2,r2l_1, r_1, l_2, r_2:小 L 先从 A[l1..r1]A[l_1..r_1] 中选一个数 aa,小 Q 再从 B[l2..r2]B[l_2..r_2] 中选一个数 bb。小 L 要让 aba \cdot b 尽可能大,小 Q 要让 aba \cdot b 尽可能小。求每轮在双方最优策略下的乘积值:

ans=maxaA[l1..r1]  minbB[l2..r2]  ab\text{ans} = \max_{a \in A[l_1..r_1]}\; \min_{b \in B[l_2..r_2]}\; a \cdot b

3. 朴素解法 (Brute-Force)

最直接的想法是对每个查询枚举 A[l1..r1]A[l_1..r_1] 中每个 aa,对每个 aa 枚举 B[l2..r2]B[l_2..r_2] 中每个 bb 计算 aba \cdot b 取最小值,再对 aa 取最大值。

  • 每次查询需 O((r1l1+1)(r2l2+1))O\big((r_1-l_1+1)(r_2-l_2+1)\big),最坏 O(nm)O(nm)
  • qq 次查询总计 O(qnm)1015O(qnm) \approx 10^{15},完全不可行。

即使优化为「先扫描 BB 求得 bmin,bmaxb_{\min}, b_{\max},再枚举 AA 中每个 aa 根据 aa 的符号选 bminb_{\min}bmaxb_{\max}」,每次查询仍需 O(n+m)O(n+m),总计 O(q(n+m))2×1010O\big(q(n+m)\big) \approx 2 \times 10^{10},仍然超时。瓶颈在于每次查询都要重新扫描数组求极值。

4. 核心解法 (Main Solution)

特殊性质

这是一个 min-max 博弈:小 L 先手最大化,小 Q 后手最小化。关键观察在于,给定小 L 选定的 aa 后,小 Q 的最优选择只取决于 aa 的符号,而与 BB 区间的正负组成无关。

关键突破

固定 aa,小 Q 要选 bb 使 aba \cdot b 最小:

  • a>0a > 0aba \cdot bbb 单调递增,小 Q 选 bminb_{\min}
  • a<0a < 0aba \cdot bbb 单调递减,小 Q 选 bmaxb_{\max}
  • a=0a = 0:乘积恒为 00

因此无论 BB 区间是全正、全负还是混合,小 Q 的选择都统一为「aa 负选 bmaxb_{\max}aa 正选 bminb_{\min}」。这就是为什么代码只需要维护 BB 的区间最大值与最小值,而不必像后文分析 AA 那样细分 BB 的正负。

下面分析小 L 的选数策略。定义小 L 选定 aa 后的最终结果为:

g(a)={abmina>0abmaxa<00a=0g(a) = \begin{cases} a \cdot b_{\min} & a > 0 \\ a \cdot b_{\max} & a < 0 \\ 0 & a = 0 \end{cases}

小 L 要求 maxg(a)\max g(a)。注意到 g(a)g(a)a>0a > 0a<0a < 0 两段上分别是关于 aa 的线性函数(系数 bminb_{\min}bmaxb_{\max} 固定),而线性函数在区间上的最大值只在端点取得。因此小 L 只需考虑 AA 区间中的几个极值候选。接下来我们推导各种选择时的结果,以及选择策略。

推导过程

小 L 选 前提 小 Q 选 结果
最小负数 anmina_n^{\min}(绝对值最大) AA 含负数 bmaxb_{\max} anminbmaxa_n^{\min} \cdot b_{\max}
最大负数 anmaxa_n^{\max}(绝对值最小) AA 含负数 bmaxb_{\max} anmaxbmaxa_n^{\max} \cdot b_{\max}
最小正数 apmina_p^{\min} AA 含正数 bminb_{\min} apminbmina_p^{\min} \cdot b_{\min}
最大正数 apmaxa_p^{\max} AA 含正数 bminb_{\min} apmaxbmina_p^{\max} \cdot b_{\min}
00 AA00 任意 00

之所以正数和负数各取两个端点,是因为 bminb_{\min}bmaxb_{\max} 的符号事先未知:当 bmin>0b_{\min} > 0 时正数段越大越好(选 apmaxa_p^{\max}),当 bmin<0b_{\min} < 0 时正数段越小越好(选 apmina_p^{\min});负数段同理。把所有存在的候选算一遍取 max\max,即可覆盖所有情况。

至此问题归结为 O(1)O(1) 查询 AA 的上述极值与 BBbmin,bmaxb_{\min}, b_{\max},用 ST 表 预处理即可。小 L 会在这些所有候选结果中选出一个最大的作为答案。

5. 正确性证明 (Proof of Correctness)

需证两点:小 Q 的最优策略,以及小 L 的候选集充分性。

引理(小 Q 的最优选择):设小 L 已选 aa,小 Q 在 BB 中选 bb 使 aba \cdot b 最小。当 a>0a > 0aba \cdot b 关于 bb 单调递增,最小值在 b=bminb = b_{\min} 取得;当 a<0a < 0 时关于 bb 单调递减,最小值在 b=bmaxb = b_{\max} 取得;当 a=0a = 0 时乘积恒为 00。这与 bmin,bmaxb_{\min}, b_{\max} 本身的正负无关,因为 bminb_{\min} 始终是 BB 中最小的、bmaxb_{\max} 始终是最大的。

定理(小 L 候选集充分性):小 L 的最优 aa 一定在 {apmax,apmin,anmax,anmin,0}\{a_p^{\max}, a_p^{\min}, a_n^{\max}, a_n^{\min}, 0\}(若存在)之中。对于 a>0a > 0 的部分,g(a)=abming(a) = a \cdot b_{\min} 是关于 aa 的一次函数,bminb_{\min} 为常数,一次函数在区间上的最大值在端点取得,故只需检查最小正数与最大正数。a<0a < 0 的部分,g(a)=abmaxg(a) = a \cdot b_{\max} 同理只需检查最小负数与最大负数。a=0a = 0g(0)=0g(0) = 0。三类候选的并集不重不漏地覆盖了 aa 的所有取值,取 max\max 即得全局最优。

综上所述,算法正确。

6. 复杂度分析 (Complexity)

  • 时间复杂度O((n+m)logn+q)O\big((n+m) \log n + q\big)。ST 表建表 55AA 表加 22BB 表,每个 O(nlogn)O(n \log n),约 7×105×171.2×1077 \times 10^5 \times 17 \approx 1.2 \times 10^7 次运算;每次查询 O(1)O(1)qq 次共 10510^5。1s 时限内轻松通过。
  • 空间复杂度O(nlogn)O(n \log n)77 个 ST 表,每个表 105×20×810^5 \times 20 \times 8 字节 16\approx 16 MB,共约 112112 MB,低于 512512 MB 限制。

7. 实现细节与避坑指南 (Implementation Details)

坑点 说明
整数溢出 ai,bi109|a_i|, |b_i| \le 10^9,乘积最大 101810^{18},必须用 long long。代码用 #define int long long 统一处理,是 OI 常用习惯。
哨兵值设计 inf = 2e9 大于所有合法 aia_i-inf 小于所有合法 aia_i,用作「不存在」标记。乘积不会触及 ±2×1018\pm 2 \times 10^{18}ans 初始化为 2×1018-2 \times 10^{18} 安全。
正/负数表的哨兵混用 a_max_pmax(a[i], 0)(无正数时返回 00,配合 L > 0 判定);a_min_pinf 哨兵(无正数时返回 inf,配合 L != inf 判定)。两种风格混用但逻辑自洽:夹 00 的表靠符号判定,inf 哨兵的表靠显式比较。
判定候选有效性 每个候选取出后必须检查是否「真的存在」:负数候选查 L < 0 && L != -inf,正数候选查 L > 0 && L != inf,避免用哨兵值参与乘法。
log2\log_2 预处理 代码预处理 lg2 数组到 10510^5,查询时 O(1)O(1)kk,避免每次调用库函数。

8. 参考代码 (Reference Code)

下面是我按「先分析小 L 的选数策略,再推导小 Q 的最优反应」这一直觉顺序写出的 ST 表解法。维护 AA 的五个区间量(最大/最小正数、最大/最小负数、是否含 00)与 BB 的最大/最小值,每次查询枚举四个极值候选加 00max\max

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
#include <bits/stdc++.h>
using namespace std;
#define int long long

const int N = 1e5 + 5, inf = 2e9;

int n, m, q;

int a[N], b[N];

int a_max_p[N][20], a_min_p[N][20], a_min_n[N][20], a_max_n[N][20];
int b_max[N][20], b_min[N][20];
int a_0[N][20];

int lg2[N];

#define pow2(x) (1<<(x))

void ST_init() {
for (int i = 1;i <= n;i++) {
a_max_p[i][0] = max(a[i], 0LL);
a_min_p[i][0] = ((a[i] > 0) ? a[i] : inf);
a_max_n[i][0] = ((a[i] < 0) ? a[i] : -inf);
a_min_n[i][0] = min(a[i], 0LL);
a_0[i][0] = (a[i] == 0);
}
for (int j = 1;j <= lg2[n];j++) {
for (int i = 1;i + pow2(j) - 1 <= n;i++) {
a_max_p[i][j] = max(a_max_p[i][j - 1], a_max_p[i + pow2(j - 1)][j - 1]);
a_min_p[i][j] = min(a_min_p[i][j - 1], a_min_p[i + pow2(j - 1)][j - 1]);
a_max_n[i][j] = max(a_max_n[i][j - 1], a_max_n[i + pow2(j - 1)][j - 1]);
a_min_n[i][j] = min(a_min_n[i][j - 1], a_min_n[i + pow2(j - 1)][j - 1]);
a_0[i][j] = a_0[i][j - 1] | a_0[i + pow2(j - 1)][j - 1];
}
}
for (int i = 1;i <= m;i++) {
b_max[i][0] = b_min[i][0] = b[i];
}
for (int j = 1;j <= lg2[m];j++) {
for (int i = 1;i + pow2(j) - 1 <= m;i++) {
b_max[i][j] = max(b_max[i][j - 1], b_max[i + pow2(j - 1)][j - 1]);
b_min[i][j] = min(b_min[i][j - 1], b_min[i + pow2(j - 1)][j - 1]);
}
}
}

int ST_query_max(int st[][20], int l, int r) {
int k = lg2[r - l + 1];
return max(st[l][k], st[r - pow2(k) + 1][k]);
}

int ST_query_min(int st[][20], int l, int r) {
int k = lg2[r - l + 1];
return min(st[l][k], st[r - pow2(k) + 1][k]);
}

signed main() {
// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.

scanf("%lld %lld %lld\n", &n, &m, &q);
for (int i = 2;i <= 100000;i++) {
lg2[i] = lg2[i / 2] + 1;
}

for (int i = 1;i <= n;i++) scanf("%lld ", &a[i]);
for (int i = 1;i <= m;i++) scanf("%lld ", &b[i]);

ST_init();

for (int i = 1;i <= q;i++) {
int l1, r1, l2, r2;
scanf("%lld %lld %lld %lld\n", &l1, &r1, &l2, &r2);
int ans = -2e18, L, Q;
if (ST_query_max(a_0, l1, r1)) ans = 0;
// L 选一个最小的负数
L = ST_query_min(a_min_n, l1, r1);
if (L < 0 && L != -inf) {
Q = ST_query_max(b_max, l2, r2);
ans = max(ans, L * Q);
}
// L 选一个最大的正数
L = ST_query_max(a_max_p, l1, r1);
if (L > 0 && L != inf) {
Q = ST_query_min(b_min, l2, r2);
ans = max(ans, L * Q);
}
// L 选一个最大的负数
L = ST_query_max(a_max_n, l1, r1);
if (L < 0 && L != -inf) {
Q = ST_query_max(b_max, l2, r2);
ans = max(ans, L * Q);
}
// L 选一个最小的正数
L = ST_query_min(a_min_p, l1, r1);
if (L > 0 && L != inf) {
Q = ST_query_min(b_min, l2, r2);
ans = max(ans, L * Q);
}
printf("%lld\n", ans);
}
return 0;
}

9. 补充说明 (Additional Notes)

  • 题目渊源:本题出自 CSP-S 2022 第二轮 第二题,是「min-max 博弈 + ST 表」的经典结合。这类「先手最大化、后手最小化」的零和博弈在竞赛中很常见,核心识别信号是双层 max-min\max\text{-}\min 嵌套——出现时先固定先手选择,分析后手的最优反应函数,往往能将后手的连续选择域压缩到少数极值上。
  • 思路与代码的简化:我在初步分析时曾想细分 BB 区间的正负组成(只含负数、只含正数、含 00、正负混合),对应维护 BB 的最大正数、最大非正数、最小负数、最小非负数。但推导后发现,小 Q 的最优 bb 选择只取决于 aa 的符号——aa 负选 bmaxb_{\max}aa 正选 bminb_{\min}——与 BB 的正负组成无关。因此代码只需要 bmaxb_{\max}bminb_{\min} 两个量,初步分析中的复杂分类是推导过程的中间产物,最终被统一掉了。这种「分析时多想几步、实现时只保留必要部分」是常见的解题节奏。ST 表作为静态区间极值查询的经典工具,在这类需要 O(1)O(1) 查询多个极值的问题中几乎是首选,方法经典,有学习的价值。
  • 其他方法:线段树等区间数据结构也可以解决 RMQ 问题,但在此类场景中 ST 表更优。

本站由 zaochen 使用 Stellar 1.33.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
全站访问量 - 次 · 访客数 - 人 · 本页面浏览 -