[CSP-S 2022] 策略游戏

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

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

OJ: luogu

题目 ID: P8818

难度:提高+/省选-

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

创建: 2026-06-21 14:48

更新: 2026-10-01 22:20

形式化题目

给定长度 nn 的数组 AA 和长度 mm 的数组 BB,定义矩阵 Cij=Ai×BjC_{ij} = A_i \times B_j。

每次询问给出 l1,r1,l2,r2l_1, r_1, l_2, r_2。小 L 先从 [l1,r1][l_1, r_1] 选一个下标 xx,小 Q 再从 [l2,r2][l_2, r_2] 选一个下标 yy,得分是 CxyC_{xy}。小 L 想让得分尽量大,小 Q 想让得分尽量小,双方都采用最优策略。

求每次询问的得分。

暴力解法

思路

枚举小 L 选哪个 xx,再枚举小 Q 选哪个 yy,对每个 xx 求小 Q 能压到的最小得分,最后取最大值。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-06-21 14:48
 * update_at: 2026-10-01 22:20
 */
// brute.cpp:小数据暴力解,枚举小 L 选哪个 x,再枚举小 Q 选哪个 y,直接按题意做极大极小。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const ll INF64 = (1LL << 60); // 哨兵值,比任何真实答案都小/大

int n, m, q;
ll a[205]; // 数组 A,只服务小数据
ll b[205]; // 数组 B,只服务小数据

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;

        // 外层枚举小 L 的选择 x,内层求小 Q 能压到的最小得分,再取最大
        ll answer = -INF64;
        for (int x = l1; x <= r1; x++) {
            ll worst = INF64;
            for (int y = l2; y <= r2; y++) {
                worst = min(worst, a[x] * b[y]);
            }
            answer = max(answer, worst);
        }
        cout << answer << '\n';
    }

    return 0;
}

复杂度

  • 时间:O(∣A区间∣×∣B区间∣)O(|A区间| \times |B区间|) 每次询问
  • 空间:O(n+m)O(n + m)

瓶颈

单次询问就是区间长度的乘积级,n,m,q⩽105n, m, q \leqslant 10^5 时完全不可行。

正解

思路

先固定小 L 选到的一个数 aa,思考小 Q 会怎么选:

  • 如果 a>0a > 0,为了让乘积尽量小,小 Q 一定会选 BB 区间里的最小值;
  • 如果 a<0a < 0,小 Q 一定会选 BB 区间里的最大值;
  • 如果 a=0a = 0,结果恒为 00。

所以一轮查询里,BB 区间真正有用的信息只有最小值 min⁡B\min B 和最大值 max⁡B\max B。然后按 BB 区间的符号分三类讨论。

1. BB 区间全非负

  • 正数 aa 的得分是 a×min⁡Ba \times \min B
  • 负数 aa 的得分是 a×max⁡Ba \times \max B

为了让结果尽量大:

  • 如果 AA 区间里有非负数,选最大的 aa
  • 否则只能在负数里选最接近 00 的那个负数

2. BB 区间全非正

  • 正数 aa 的得分是 a×min⁡Ba \times \min B(非正数)
  • 负数 aa 的得分是 a×max⁡Ba \times \max B(非负数)

为了让结果尽量大:

  • 如果 AA 区间里有非正数,选最小的那个数(负数绝对值最大,乘积最大)
  • 否则只能选最小正数

3. BB 区间同时有正有负

  • 正数 aa 会被乘上 min⁡B\min B,结果是负数
  • 负数 aa 会被乘上 max⁡B\max B,结果也是负数
  • 如果 AA 区间有 00,那直接选 00 最优

否则只需要比较:

  • 最小正数 ×min⁡B\times \min B
  • 最大负数(最接近 00 的负数)×max⁡B\times \max B

于是 AA 区间里需要查询的量只有:最大值、最小值、最小正数、最大负数、是否存在 00。这些都可以用 ST 表和前缀和在 O(1)O(1) 查询出来;BB 区间只要查最小值和最大值即可。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-06-21 14:48
 * update_at: 2026-10-01 22:20
 */
// main.cpp:把极大极小乘积按 B 区间符号分三类讨论,
// 用 ST 表维护 A 区间的最值、最小正数、最大负数、是否有零。
#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]; // 数组 A
ll b[MAXN]; // 数组 B

int lg2_table[MAXN];  // lg2_table[i]:floor(log2(i)),ST 表查区间时用
int zero_prefix[MAXN]; // zero_prefix[i]:A[1..i] 中 0 的个数前缀和

ll st_a_max[18][MAXN];    // ST 表:A 区间最大值
ll st_a_min[18][MAXN];    // ST 表:A 区间最小值
ll st_a_posmin[18][MAXN]; // ST 表:A 区间最小正数,不存在时为 INF64
ll st_a_negmax[18][MAXN]; // ST 表:A 区间最大的负数(最接近 0),不存在时为 -INF64

ll st_b_max[18][MAXN]; // ST 表:B 区间最大值
ll st_b_min[18][MAXN]; // ST 表:B 区间最小值

// 查询 st 表区间 [l,r] 的最大值
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]);
}

// 查询 st 表区间 [l,r] 的最小值
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]);
}

// 预处理 1..limit 的 log2 下取整表
void build_logs(int limit) {
    lg2_table[1] = 0;
    for (int i = 2; i <= limit; i++) {
        lg2_table[i] = lg2_table[i >> 1] + 1;
    }
}

// 预处理 A 的四张 ST 表和零个数前缀和
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]);
        }
    }
}

// 预处理 B 的两张 ST 表
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;

        // B 区间只关心最小值和最大值
        ll b_min = query_min(st_b_min, l2, r2);
        ll b_max = query_max(st_b_max, l2, r2);

        // A 区间需要的五类信息
        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 {
                // A 有正有负:最小正数乘 b_min、最大负数乘 b_max 取较大者
                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) 查询。

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

图示解析

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

一图流解析