P2129 L 国的战斗续之多路出击 解题报告
P2129 L 国的战斗续之多路出击 解题报告
1. 题目大意
我们有 \(n\) 支军队,每支军队都有一个初始坐标 \((x, y)\)。接下来有 \(m\) 个命令,我们需要从后往前依次处理这些命令。命令有三种:
- 移动 (
m p q
): 所有军队的坐标从 \((x, y)\) 变为 \((x+p, y+q)\)。 - x轴翻转 (
x
): 所有军队的坐标从 \((x, y)\) 变为 \((-x, y)\)。 - y轴翻转 (
y
): 所有军队的坐标从 \((x, y)\) 变为 \((x, -y)\)。
最后,我们需要输出每支军队经过所有变换后的最终坐标。
2. 思路分析
2.1 朴素想法与挑战
最直接的想法是,我们有 \(n\) 支军队,那就对每支军队都执行一遍所有 \(m\) 个命令。
具体来说,就是开一个循环处理 \(m\) 个命令(注意是倒序),在循环里面再开一个循环,遍历 \(n\) 支军队,对它们的坐标进行相应的变换。
这种方法的时间复杂度是多少呢?对于 \(m\) 个命令中的每一个,我们都操作了 \(n\) 个点,所以总时间复杂度是 \(O(n \times m)\)。
我们看看数据范围:\(n\) 和 \(m\) 最大都可以达到 \(5 \times 10^5\)。如果用 \(O(n \times m)\) 的算法,计算量级会达到 \((5 \times 10^5) \times (5 \times 10^5) = 2.5 \times 10^{11}\),这远远超过了计算机在一秒内能处理的计算量(通常是 \(10^8\) 级别),所以这种朴素的模拟方法一定会超时。我们需要找到一个更高效的办法。
2.2 核心思想:相对运动与坐标系变换
既然移动所有的点太慢了,我们不妨换个角度思考。想象一下,所有的军队都在一张巨大的坐标纸上。
- 当命令要求所有军队向右移动 5 个单位时,这和整个坐标纸(也就是坐标系)向左移动 5 个单位,效果是一样的。
- 当命令要求所有军队关于 y 轴对称时,这和整个坐标纸关于 y 轴翻转,效果也是一样的。
这个思想就是“相对运动”。我们不去记录 \(n\) 个点各自的变化,而是去记录整个坐标系发生了怎样的变化。这样,无论有多少支军队,我们都只需要维护一组坐标系的状态。在所有命令处理完后,我们得到了一个最终的“扭曲”了的坐标系。最后,我们再把每个点的初始坐标代入这个“扭曲”的坐标系,一次性计算出它们的最终位置。
这样,处理 \(m\) 个命令的复杂度就是 \(O(m)\),最后计算 \(n\) 个点的最终位置的复杂度是 \(O(n)\),总时间复杂度就降到了 \(O(n+m)\),完全可以接受。
2.3 具体实现
我们要如何记录坐标系的变化呢?
-
逆序处理:题目明确要求从后往前处理命令,所以我们先读入并存储所有命令,然后从第 \(m\) 个命令开始,倒着处理到第 1 个。
-
维护状态:我们需要几个变量来描述当前坐标系的状态:
deltax
,deltay
:记录坐标系整体的平移量。初始时都为 0。flag1
,flag2
:记录坐标系是否被翻转。flag1
代表 x 轴方向是否被翻转,flag2
代表 y 轴方向是否被翻转。我们可以用 0 表示“未翻转”,1 表示“已翻转”。
-
处理命令(从
m
到1
):-
遇到翻转命令 (
x
或y
):- 如果是
x
命令,就切换flag1
的状态(0 变 1,1 变 0)。 - 如果是
y
命令,就切换flag2
的状态。 - 这表示从这一刻起,后续的所有操作都是在一个翻转了的坐标系上进行的。
- 如果是
-
遇到移动命令 (
m p q
):- 我们需要把这次移动累加到
deltax
和deltay
上。 - 但要注意!如果当前坐标系的 x 轴是翻转的(
flag1
为 1),那么原本向 x 轴正方向移动p
,在翻转的坐标系看来,就等价于向 x 轴负方向移动p
。所以,我们累加到deltax
上的应该是-p
而不是p
。 - 同理,如果
flag2
为 1,我们累加到deltay
上的应该是-q
。 - 所以,实际累加的值是:
deltax += p * (flag1 ? -1 : 1)
和deltay += q * (flag2 ? -1 : 1)
。
- 我们需要把这次移动累加到
-
-
计算最终坐标:
- 当所有 \(m\) 个命令都倒序处理完后,我们得到了最终的平移量
deltax
,deltay
和最终的翻转状态flag1
,flag2
。 - 对于任何一个初始坐标为 \((x_i, y_i)\) 的军队,它相对于最终坐标系原点的位置应该是 \((x_i + deltax, y_i + deltay)\)。
- 但是,这个坐标是在“扭曲”后的坐标系里的。我们还要把它变回标准的、没有翻转的坐标系里。
- 如果最终 x 轴是翻转的(
flag1
为 1),那么最终的 x 坐标就要取反。 - 如果最终 y 轴是翻转的(
flag2
为 1),那么最终的 y 坐标就要取反。 - 所以,第 \(i\) 支军队的最终坐标 \((x_i', y_i')\) 为:
- \(x_i' = (x_i + deltax) \times (flag1 ? -1 : 1)\)
- \(y_i' = (y_i + deltay) \times (flag2 ? -1 : 1)\)
- 当所有 \(m\) 个命令都倒序处理完后,我们得到了最终的平移量
3. 代码解析
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int Maxn = 501010; // 定义数组大小
typedef long long ll; // 坐标和可能超出 int 范围,使用 long long
char ch[Maxn]; // 存储 m 个命令的类型
int n, m, flag1, flag2; // flag1: x轴翻转标记, flag2: y轴翻转标记
ll x[Maxn], y[Maxn]; // 存储 n 个军队的初始坐标
ll a[Maxn], b[Maxn]; // 存储 m 个移动命令的参数 p 和 q
// 一个辅助函数,方便地将翻转标记 (0或1) 转换成乘数 (1或-1)
int Get(int p) {
return (p ? -1 : 1); // p为1(翻转)时返回-1,p为0(未翻转)时返回1
}
int main() {
scanf("%d%d", &n, &m);
// 读入n个军队的初始坐标
for(int i = 1; i <= n; ++i) cin >> x[i] >> y[i];
// 读入并存储所有m个命令,因为要倒序处理
for(int i = 1; i <= m; ++i) {
cin >> ch[i];
if(ch[i] == 'm') cin >> a[i] >> b[i];
}
// 初始化总平移量
ll deltax = 0, deltay = 0;
// 倒序处理所有命令
for(int i = m; i >= 1; --i) {
if(ch[i] == 'x') {
flag1 ^= 1; // 切换x轴翻转状态 (0->1, 1->0)
} else if(ch[i] == 'y') {
flag2 ^= 1; // 切换y轴翻转状态
} else { // 如果是移动命令 'm'
// 根据当前的翻转状态,累加平移量
deltax += Get(flag1) * a[i];
deltay += Get(flag2) * b[i];
}
}
// 所有命令处理完毕,计算并输出每个点的最终坐标
for(int i = 1; i <= n; ++i) {
// 1. 先应用总平移量: (x[i] + deltax, y[i] + deltay)
// 2. 再根据最终的翻转状态,进行最后的翻转
cout << Get(flag1) * (deltax + x[i]) << ' ' << Get(flag2) * (deltay + y[i]) << endl;
}
return 0;
}
4. 总结
本题的核心在于巧妙地运用了相对运动的思想,将对 \(n\) 个点的 \(m\) 次操作,转化为对单个坐标系的 \(m\) 次操作,最后再将变换结果统一应用到 \(n\) 个点上。这使得算法复杂度从无法接受的 \(O(n \times m)\) 优化到了高效的 \(O(n+m)\),是解决此类“对全体进行统一操作”问题的经典思路。