P2129 L 国的战斗续之多路出击 解题报告


P2129 L 国的战斗续之多路出击 解题报告

1. 题目大意

我们有 \(n\) 支军队,每支军队都有一个初始坐标 \((x, y)\)。接下来有 \(m\) 个命令,我们需要从后往前依次处理这些命令。命令有三种:

  1. 移动 (m p q): 所有军队的坐标从 \((x, y)\) 变为 \((x+p, y+q)\)
  2. x轴翻转 (x): 所有军队的坐标从 \((x, y)\) 变为 \((-x, y)\)
  3. 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 具体实现

我们要如何记录坐标系的变化呢?

  1. 逆序处理:题目明确要求从后往前处理命令,所以我们先读入并存储所有命令,然后从第 \(m\) 个命令开始,倒着处理到第 1 个。

  2. 维护状态:我们需要几个变量来描述当前坐标系的状态:

    • deltax, deltay:记录坐标系整体的平移量。初始时都为 0。
    • flag1, flag2:记录坐标系是否被翻转flag1 代表 x 轴方向是否被翻转,flag2 代表 y 轴方向是否被翻转。我们可以用 0 表示“未翻转”,1 表示“已翻转”。
  3. 处理命令(从 m1):

    • 遇到翻转命令 (xy):

      • 如果是 x 命令,就切换 flag1 的状态(0 变 1,1 变 0)。
      • 如果是 y 命令,就切换 flag2 的状态。
      • 这表示从这一刻起,后续的所有操作都是在一个翻转了的坐标系上进行的。
    • 遇到移动命令 (m p q):

      • 我们需要把这次移动累加到 deltaxdeltay 上。
      • 但要注意!如果当前坐标系的 x 轴是翻转的(flag1 为 1),那么原本向 x 轴正方向移动 p,在翻转的坐标系看来,就等价于向 x 轴负方向移动 p。所以,我们累加到 deltax 上的应该是 -p 而不是 p
      • 同理,如果 flag2 为 1,我们累加到 deltay 上的应该是 -q
      • 所以,实际累加的值是:deltax += p * (flag1 ? -1 : 1)deltay += q * (flag2 ? -1 : 1)
  4. 计算最终坐标

    • 当所有 \(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)\)

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)\),是解决此类“对全体进行统一操作”问题的经典思路。

posted @ 2025-07-23 17:07  surprise_ying  阅读(7)  评论(0)    收藏  举报