把极大极小乘积按 B 区间符号分三类讨论,只需在 A 区间查询最值、最小正数、最大负数和是否有零。
OJ: luogu
题目 ID: P8818
难度:提高+/省选-
标签:ST表分类讨论极小化极大思维
日期: 2026-06-21 14:48
题意
有两个数组 A、B。
每次询问给出两个区间:
A[l1..r1]B[l2..r2]
小 L 先从 A 的区间里选一个数,小 Q 再从 B 的区间里选一个数。
得分是这两个数的乘积。
小 L 想让得分尽量大,小 Q 想让得分尽量小。
要求输出双方都最优时的得分。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, m, q;
ll a[205], b[205];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
while (q--) {
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
// brute.cpp:枚举小 L 选哪个 x,再枚举小 Q 选哪个 y,直接按题意做极大极小。
ll answer = -(1LL << 60);
for (int x = l1; x <= r1; x++) {
ll worst = (1LL << 60);
for (int y = l2; y <= r2; y++) {
worst = min(worst, a[x] * b[y]);
}
answer = max(answer, worst);
}
cout << answer << '\n';
}
return 0;
}暴力做法就是枚举小 L 选哪个 A_x,再枚举小 Q 选哪个 B_y,求出每个 x 对应的最坏结果,然后再取最大值。这个方法复杂度是
关键是先固定小 L 选到的一个数 a,思考小 Q 会怎么选:
- 如果
a > 0,为了让乘积尽量小,小 Q 一定会选B区间里的最小值 - 如果
a < 0,小 Q 一定会选B区间里的最大值 - 如果
a = 0,结果恒为0
所以一轮查询里,B 区间真正有用的信息只有:
- 最小值
minB - 最大值
maxB
然后按 B 区间的符号分三类讨论。
1. B 区间全非负
这时:
- 正数
a的得分是a * minB - 负数
a的得分是a * maxB
为了让结果尽量大:
- 如果
A区间里有非负数,显然选最大的a - 否则只能在负数里选最接近
0的那个负数
2. B 区间全非正
这时:
- 正数
a的得分是a * minB,这是负数 - 负数
a的得分是a * maxB,这是非负数
为了让结果尽量大:
- 如果
A区间里有负数,选最小的那个负数(绝对值最大) - 否则如果有
0,选0 - 否则只能选最小正数
3. B 区间同时有正有负
这时:
- 正数
a会被乘上minB,结果是负数 - 负数
a会被乘上maxB,结果也是负数 - 如果
A区间有0,那直接选0最优
否则只需要比较:
- 最小正数
* minB - 最大负数(最接近
0的负数)* maxB
于是我们在 A 区间里需要查询的量只有:
- 最大值
- 最小值
- 最小正数
- 最大负数
- 是否存在
0
这些都可以用 ST 表和前缀和在
B 区间只要查最小值和最大值即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
const ll INF64 = (1LL << 60);
int n, m, q;
ll a[MAXN], b[MAXN];
int lg2_table[MAXN];
int zero_prefix[MAXN];
ll st_a_max[18][MAXN];
ll st_a_min[18][MAXN];
ll st_a_posmin[18][MAXN]; // 区间内最小正数,不存在时为 INF64
ll st_a_negmax[18][MAXN]; // 区间内最大的负数(最接近 0),不存在时为 -INF64
ll st_b_max[18][MAXN];
ll st_b_min[18][MAXN];
ll query_max(ll st[18][MAXN], int l, int r) {
int k = lg2_table[r - l + 1];
return max(st[k][l], st[k][r - (1 << k) + 1]);
}
ll query_min(ll st[18][MAXN], int l, int r) {
int k = lg2_table[r - l + 1];
return min(st[k][l], st[k][r - (1 << k) + 1]);
}
void build_logs(int limit) {
lg2_table[1] = 0;
for (int i = 2; i <= limit; i++) {
lg2_table[i] = lg2_table[i >> 1] + 1;
}
}
void build_st_a() {
for (int i = 1; i <= n; i++) {
st_a_max[0][i] = a[i];
st_a_min[0][i] = a[i];
st_a_posmin[0][i] = (a[i] > 0 ? a[i] : INF64);
st_a_negmax[0][i] = (a[i] < 0 ? a[i] : -INF64);
zero_prefix[i] = zero_prefix[i - 1] + (a[i] == 0);
}
for (int k = 1; (1 << k) <= n; k++) {
int len = 1 << k;
int half = len >> 1;
for (int i = 1; i + len - 1 <= n; i++) {
st_a_max[k][i] = max(st_a_max[k - 1][i], st_a_max[k - 1][i + half]);
st_a_min[k][i] = min(st_a_min[k - 1][i], st_a_min[k - 1][i + half]);
st_a_posmin[k][i] = min(st_a_posmin[k - 1][i], st_a_posmin[k - 1][i + half]);
st_a_negmax[k][i] = max(st_a_negmax[k - 1][i], st_a_negmax[k - 1][i + half]);
}
}
}
void build_st_b() {
for (int i = 1; i <= m; i++) {
st_b_max[0][i] = b[i];
st_b_min[0][i] = b[i];
}
for (int k = 1; (1 << k) <= m; k++) {
int len = 1 << k;
int half = len >> 1;
for (int i = 1; i + len - 1 <= m; i++) {
st_b_max[k][i] = max(st_b_max[k - 1][i], st_b_max[k - 1][i + half]);
st_b_min[k][i] = min(st_b_min[k - 1][i], st_b_min[k - 1][i + half]);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
build_logs(max(n, m));
build_st_a();
build_st_b();
while (q--) {
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
ll b_min = query_min(st_b_min, l2, r2);
ll b_max = query_max(st_b_max, l2, r2);
ll a_max = query_max(st_a_max, l1, r1);
ll a_min = query_min(st_a_min, l1, r1);
ll a_posmin = query_min(st_a_posmin, l1, r1);
ll a_negmax = query_max(st_a_negmax, l1, r1);
bool has_zero = (zero_prefix[r1] - zero_prefix[l1 - 1] > 0);
ll answer;
if (b_min >= 0) {
// B 区间全非负,小 Q 会取最小的 B。
if (a_max < 0) {
answer = a_negmax * b_max;
}
else {
answer = a_max * b_min;
}
}
else if (b_max <= 0) {
// B 区间全非正,小 Q 会取最大的 B。
if (a_min > 0) {
answer = a_posmin * b_min;
}
else {
answer = a_min * b_max;
}
}
else {
// B 区间同时有正有负。
if (has_zero) {
answer = 0;
}
else if (a_max < 0) {
answer = a_negmax * b_max;
}
else if (a_min > 0) {
answer = a_posmin * b_min;
}
else {
ll cand1 = a_posmin * b_min;
ll cand2 = a_negmax * b_max;
answer = max(cand1, cand2);
}
}
cout << answer << '\n';
}
return 0;
}复杂度
- 预处理 ST 表:
- 每次查询:
总时间复杂度是
总结
这题本质不是矩阵博弈,而是:
- 先把“对手会怎么选”压缩成符号分类
- 再把每种情况需要的区间信息整理成固定几个最值
- 用 ST 表把这些区间最值做到
查询
最关键的是把极大极小问题拆成清楚的分类讨论。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
