[CSP-S 2022] 策略游戏

GitHub跳转原题关系图返回列表

把极大极小乘积按 B 区间符号分三类讨论,只需在 A 区间查询最值、最小正数、最大负数和是否有零。

OJ: luogu

题目 ID: P8818

难度:提高+/省选-

标签:ST表分类讨论极小化极大思维

日期: 2026-06-21 14:48

题意

有两个数组 AB

每次询问给出两个区间:

  • 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 对应的最坏结果,然后再取最大值。这个方法复杂度是 O(A区间B区间)O(|A区间| * |B区间|),完全不够。

关键是先固定小 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 表和前缀和在 O(1)O(1) 查询出来。
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 表:O((n+m)log(n+m))O((n+m)\log(n+m))
  • 每次查询:O(1)O(1)

总时间复杂度是 O((n+m+q)log(n+m))O((n+m+q)\log(n+m)) 级别,空间复杂度是 O((n+m)log(n+m))O((n+m)\log(n+m))

总结

这题本质不是矩阵博弈,而是:

  1. 先把“对手会怎么选”压缩成符号分类
  2. 再把每种情况需要的区间信息整理成固定几个最值
  3. 用 ST 表把这些区间最值做到 O(1)O(1) 查询

最关键的是把极大极小问题拆成清楚的分类讨论。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析