2022信奥赛C++提高组csp-s复赛真题及题解:策略游戏
题目描述
小 L 和小 Q 在玩一个策略游戏。
有一个长度为 n n n 的数组 A A A 和一个长度为 m m m 的数组 B B B,在此基础上定义一个大小为 n × m n \times m n×m 的矩阵 C C C,满足 C i j = A i × B j C_{i j} = A_i \times B_j Cij=Ai×Bj。所有下标均从 1 1 1 开始。
游戏一共会进行 q q q 轮,在每一轮游戏中,会事先给出 4 4 4 个参数 l 1 , r 1 , l 2 , r 2 l_1, r_1, l_2, r_2 l1,r1,l2,r2,满足 1 ≤ l 1 ≤ r 1 ≤ n 1 \le l_1 \le r_1 \le n 1≤l1≤r1≤n、 1 ≤ l 2 ≤ r 2 ≤ m 1 \le l_2 \le r_2 \le m 1≤l2≤r2≤m。
游戏中,小 L 先选择一个 l 1 ∼ r 1 l_1 \sim r_1 l1∼r1 之间的下标 x x x,然后小 Q 选择一个 l 2 ∼ r 2 l_2 \sim r_2 l2∼r2 之间的下标 y y y。定义这一轮游戏中二人的得分是 C x y C_{x y} Cxy。
小 L 的目标是使得这个得分尽可能大,小 Q 的目标是使得这个得分尽可能小。同时两人都是足够聪明的玩家,每次都会采用最优的策略。
请问:按照二人的最优策略,每轮游戏的得分分别是多少?
输入格式
第一行输入三个正整数 n , m , q n, m, q n,m,q,分别表示数组 A A A,数组 B B B 的长度和游戏轮数。
第二行: n n n 个整数,表示 A i A_i Ai,分别表示数组 A A A 的元素。
第三行: m m m 个整数,表示 B i B_i Bi,分别表示数组 B B B 的元素。
接下来 q q q 行,每行四个正整数,表示这一次游戏的 l 1 , r 1 , l 2 , r 2 l_1, r_1, l_2, r_2 l1,r1,l2,r2。
输出格式
输出共 q q q 行,每行一个整数,分别表示每一轮游戏中,小 L 和小 Q 在最优策略下的得分。
输入输出样例 1
输入 1
复制代码
3 2 2
0 1 -2
-3 4
1 3 1 2
2 3 2 2
输出 1
复制代码
0
4
输入输出样例 2
输入 2
复制代码
6 4 5
3 -1 -2 1 2 0
1 2 -1 -3
1 6 1 4
1 5 1 4
1 4 1 2
2 6 3 4
2 5 2 3
输出 2
复制代码
0
-2
3
2
-1
说明/提示
【样例解释 #1】
这组数据中,矩阵 C C C 如下:
0 0 − 3 4 6 − 8 \] \\begin{bmatrix} 0 \& 0 \\\\ -3 \& 4 \\\\ 6 \& -8 \\end{bmatrix} 0−3604−8
在第一轮游戏中,无论小 L 选取的是 x = 2 x = 2 x=2 还是 x = 3 x = 3 x=3,小 Q 都有办法选择某个 y y y 使得最终的得分为负数。因此小 L 选择 x = 1 x = 1 x=1 是最优的,因为这样得分一定为 0 0 0。
而在第二轮游戏中,由于小 L 可以选 x = 2 x = 2 x=2,小 Q 只能选 y = 2 y = 2 y=2,如此得分为 4 4 4。
**【数据范围】**
对于所有数据, 1 ≤ n , m , q ≤ 10 5 1 \\le n, m, q \\le {10}\^5 1≤n,m,q≤105, − 10 9 ≤ A i , B i ≤ 10 9 -{10}\^9 \\le A_i, B_i \\le {10}\^9 −109≤Ai,Bi≤109。对于每轮游戏而言, 1 ≤ l 1 ≤ r 1 ≤ n 1 \\le l_1 \\le r_1 \\le n 1≤l1≤r1≤n, 1 ≤ l 2 ≤ r 2 ≤ m 1 \\le l_2 \\le r_2 \\le m 1≤l2≤r2≤m。
| 测试点编号 | n , m , q ≤ n, m, q \\le n,m,q≤ | 特殊条件 |
|:-------------------------:|:-------------------------------:|:----:|
| 1 1 1 | 200 200 200 | 1, 2 |
| 2 2 2 | 200 200 200 | 1 |
| 3 3 3 | 200 200 200 | 2 |
| 4 ∼ 5 4 \\sim 5 4∼5 | 200 200 200 | 无 |
| 6 6 6 | 1000 1000 1000 | 1, 2 |
| 7 ∼ 8 7 \\sim 8 7∼8 | 1000 1000 1000 | 1 |
| 9 ∼ 10 9 \\sim 10 9∼10 | 1000 1000 1000 | 2 |
| 11 ∼ 12 11 \\sim 12 11∼12 | 1000 1000 1000 | 无 |
| 13 13 13 | 10 5 {10}\^5 105 | 1, 2 |
| 14 ∼ 15 14 \\sim 15 14∼15 | 10 5 {10}\^5 105 | 1 |
| 16 ∼ 17 16 \\sim 17 16∼17 | 10 5 {10}\^5 105 | 2 |
| 18 ∼ 20 18 \\sim 20 18∼20 | 10 5 {10}\^5 105 | 无 |
其中,特殊性质 1 为:保证 A i , B i \> 0 A_i, B_i \> 0 Ai,Bi\>0。
特殊性质 2 为:保证对于每轮游戏而言,要么 l 1 = r 1 l_1 = r_1 l1=r1,要么 l 2 = r 2 l_2 = r_2 l2=r2。
这是一道博弈论+区间查询的题目。我们需要理解两个玩家的策略,并高效处理多个区间查询。
### 思路分析
##### 1. 博弈策略分析
游戏规则:
1. 小L先选择 `x ∈ [l1, r1]`
2. 小Q后选择 `y ∈ [l2, r2]`
3. 得分 = `A[x] * B[y]`
4. 小L希望得分最大,小Q希望得分最小
这是一个零和博弈,我们可以倒推分析:
* 对于小Q(后手):给定小L选择的 `x`,小Q会选择使 `A[x] * B[y]` 最小的 `y`
* 如果 `A[x] ≥ 0`:小Q会选择 `B[y]` 最小的值
* 如果 `A[x] < 0`:小Q会选择 `B[y]` 最大的值
* 如果 `A[x] = 0`:无论小Q选什么,得分都是0
* 对于小L(先手):知道小Q的应对策略,小L会选择使最终得分最大的 `x`
##### 2. 数学表达式
对于给定的区间 `[l1, r1]` 和 `[l2, r2]`:
设:
* `minB` = B在 `[l2, r2]` 中的最小值
* `maxB` = B在 `[l2, r2]` 中的最大值
* 对于A在 `[l1, r1]` 中的每个元素 `a`,小Q选择的 `B[y]` 为:
B_choice(a) = {
minB, if a ≥ 0
maxB, if a < 0
}
* 最终得分为:`a * B_choice(a)`
小L的任务是选择 `a` 使得 `a * B_choice(a)` 最大。
##### 3. 小L的最优选择策略
考虑A在 `[l1, r1]` 中的元素:
1. **非负数(a ≥ 0)** :得分为 `a * minB`
* 如果 `minB ≥ 0`:得分非负,应选择最大的 `a`(即 `maxA_nonneg`)
* 如果 `minB < 0`:得分为非正数,应选择最小的非负数(即 `minA_nonneg`),因为负得最少(或正得最多)
2. **负数(a \< 0)** :得分为 `a * maxB`
* 如果 `maxB > 0`:得分为负数,应选择最大的负数(即 `maxA_neg`),因为负得最少
* 如果 `maxB ≤ 0`:得分为非负数,应选择最小的负数(即 `minA_neg`),因为正得最多
3. **零(a = 0)**:得分总是0
**小L的最终选择**:从以上候选值中选择得分最大的。
##### 4. 数据结构设计
我们需要快速查询以下信息:
* A区间:最大值、最小值、最大非负数、最小非负数、最大负数、最小负数、是否有零
* B区间:最小值、最大值
由于查询次数多(q ≤ 10\^5),需要O(log n)的查询:
* **线段树**:每个节点维护上述信息
* **ST表**:也可以,但需要维护的信息较多
##### 5. 算法步骤
对于每个查询 `(l1, r1, l2, r2)`:
1. 查询B区间:`minB`, `maxB`
2. 查询A区间:获取所有候选值
3. 根据 `minB` 和 `maxB` 的符号,计算候选得分
4. 选择最大得分
### 代码实现
```cpp
#include
using namespace std;
typedef long long ll;
const ll INF = 4e18;
const int N = 1e5 + 5;
int n, m, q;
ll a[N], b[N];
// A数组的线段树节点
struct NodeA {
ll mx, mn; // 最大值,最小值
ll mx_pos, mn_pos; // 最大非负数,最小非负数
ll mx_neg, mn_neg; // 最大负数,最小负数
bool has_zero; // 是否有0
};
// B数组的线段树节点(只需要最大值和最小值)
struct NodeB {
ll mx, mn;
};
NodeA ta[N << 2];
NodeB tb[N << 2];
// 合并两个A节点
NodeA mergeA(const NodeA &l, const NodeA &r) {
NodeA res;
// 最大值和最小值
res.mx = max(l.mx, r.mx);
res.mn = min(l.mn, r.mn);
// 非负数
res.mx_pos = max(l.mx_pos, r.mx_pos);
res.mn_pos = min(l.mn_pos, r.mn_pos);
// 负数
res.mx_neg = max(l.mx_neg, r.mx_neg);
res.mn_neg = min(l.mn_neg, r.mn_neg);
// 是否有0
res.has_zero = l.has_zero || r.has_zero;
return res;
}
// 合并两个B节点
NodeB mergeB(const NodeB &l, const NodeB &r) {
return {max(l.mx, r.mx), min(l.mn, r.mn)};
}
// 构建A线段树
void buildA(int p, int l, int r) {
if (l == r) {
ta[p].mx = ta[p].mn = a[l];
if (a[l] > 0) {
ta[p].mx_pos = ta[p].mn_pos = a[l];
ta[p].mx_neg = -INF; // 用-INF表示不存在
ta[p].mn_neg = INF; // 用INF表示不存在
ta[p].has_zero = false;
} else if (a[l] < 0) {
ta[p].mx_pos = -INF;
ta[p].mn_pos = INF;
ta[p].mx_neg = ta[p].mn_neg = a[l];
ta[p].has_zero = false;
} else { // a[l] == 0
ta[p].mx_pos = -INF;
ta[p].mn_pos = INF;
ta[p].mx_neg = -INF;
ta[p].mn_neg = INF;
ta[p].has_zero = true;
}
return;
}
int mid = (l + r) >> 1;
buildA(p << 1, l, mid);
buildA(p << 1 | 1, mid + 1, r);
ta[p] = mergeA(ta[p << 1], ta[p << 1 | 1]);
}
// 构建B线段树
void buildB(int p, int l, int r) {
if (l == r) {
tb[p] = {b[l], b[l]};
return;
}
int mid = (l + r) >> 1;
buildB(p << 1, l, mid);
buildB(p << 1 | 1, mid + 1, r);
tb[p] = mergeB(tb[p << 1], tb[p << 1 | 1]);
}
// 查询A区间
NodeA queryA(int p, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return ta[p];
int mid = (l + r) >> 1;
if (qr <= mid) return queryA(p << 1, l, mid, ql, qr);
if (ql > mid) return queryA(p << 1 | 1, mid + 1, r, ql, qr);
return mergeA(queryA(p << 1, l, mid, ql, qr),
queryA(p << 1 | 1, mid + 1, r, ql, qr));
}
// 查询B区间
NodeB queryB(int p, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return tb[p];
int mid = (l + r) >> 1;
if (qr <= mid) return queryB(p << 1, l, mid, ql, qr);
if (ql > mid) return queryB(p << 1 | 1, mid + 1, r, ql, qr);
return mergeB(queryB(p << 1, l, mid, ql, qr),
queryB(p << 1 | 1, mid + 1, r, ql, qr));
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= m; i++) cin >> b[i];
buildA(1, 1, n);
buildB(1, 1, m);
while (q--) {
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
// 查询B区间的最小值和最大值
NodeB nb = queryB(1, 1, m, l2, r2);
ll minB = nb.mn, maxB = nb.mx;
// 查询A区间的信息
NodeA na = queryA(1, 1, n, l1, r1);
// 计算候选得分
ll ans = -INF;
// 1. 考虑非负数的情况
if (na.mx_pos != -INF) { // 存在非负数
// 根据minB的符号选择
if (minB >= 0) {
// 选择最大的非负数
ans = max(ans, na.mx_pos * minB);
} else {
// 选择最小的非负数
ans = max(ans, na.mn_pos * minB);
}
}
// 2. 考虑负数的情况
if (na.mx_neg != -INF) { // 存在负数
// 根据maxB的符号选择
if (maxB > 0) {
// 选择最大的负数(绝对值最小)
ans = max(ans, na.mx_neg * maxB);
} else {
// 选择最小的负数(绝对值最大)
ans = max(ans, na.mn_neg * maxB);
}
}
// 3. 考虑0的情况
if (na.has_zero) {
ans = max(ans, 0LL);
}
cout << ans << '\n';
}
return 0;
}
```
### 功能分析
##### 核心数据结构
1. **NodeA**:维护A区间的关键信息
* `mx`, `mn`:最大值和最小值
* `mx_pos`, `mn_pos`:最大非负数和最小非负数
* `mx_neg`, `mn_neg`:最大负数和最小负数
* `has_zero`:是否有零
2. **NodeB**:维护B区间的关键信息
* `mx`, `mn`:最大值和最小值
##### 主要函数
1. **mergeA()**:合并两个A节点,用于线段树的区间合并
2. **mergeB()**:合并两个B节点
3. **buildA()** / **buildB()**:构建线段树
4. **queryA()** / **queryB()**:查询区间信息
##### 算法流程
对于每个查询:
1. 查询B区间:获取 `minB` 和 `maxB`
2. 查询A区间:获取所有需要的极值信息
3. 根据博弈策略计算候选得分:
* 非负数候选:根据 `minB` 符号选择最大或最小非负数
* 负数候选:根据 `maxB` 符号选择最大或最小负数
* 零候选:得分为0
4. 取所有候选得分的最大值
##### 时间复杂度分析
* 建树:O(n + m)
* 每次查询:O(log n + log m)
* 总复杂度:O(q log(n+m)),满足1e5数据量要求
##### 空间复杂度分析
* 线段树:O(4\*(n+m))
* 总空间:O(n+m)
##### 关键点总结
1. **博弈策略理解**:倒推分析,小Q总是选择使乘积最小的B值
2. **分类讨论**:根据A的符号和B的极值符号分情况处理
3. **数据结构优化**:使用线段树快速查询区间极值信息
4. **边界处理**:正确处理不存在的极值情况(用INF标记)
*** ** * ** ***
专栏推荐:**信奥赛C++提高组csp-s初赛\&复赛真题题解(持续更新)**
*** ** * ** ***
> **各种学习资料,助力大家一站式学习和提升**!!!
```cpp
#include
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
```
> ***1、csp信奥赛高频考点知识详解及案例实践:***
CSP信奥赛C++动态规划:
[https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转](https://blog.csdn.net/weixin_66461496/category_13096895.html)
CSP信奥赛C++标准模板库STL:
[https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转](https://blog.csdn.net/weixin_66461496/category_13108077.html)
`信奥赛C++提高组csp-s知识详解及案例实践:`
> ***2、csp信奥赛冲刺一等奖有效刷题题解:***
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):[https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转](https://blog.csdn.net/weixin_66461496/category_12808781.html)
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):[https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转](https://blog.csdn.net/weixin_66461496/category_12673810.html)
> ***3、GESP C++考级真题题解:***

GESP(C++ 一级+二级+三级)真题题解(持续更新):[https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转](https://blog.csdn.net/weixin_66461496/category_12858102.html)

GESP(C++ 四级+五级+六级)真题题解(持续更新):[https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转](https://blog.csdn.net/weixin_66461496/category_12869848.html)

GESP(C++ 七级+八级)真题题解(持续更新):
> ***4、CSP信奥赛C++竞赛拿奖视频课:***
[https://edu.csdn.net/course/detail/40437 点击跳转](https://edu.csdn.net/course/detail/40437)

### · 文末祝福 ·
```cpp
#include
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}
```