题解:洛谷 AT_abc470_d Inverse and Swap

【题目来源】

洛谷:AT_abc470_d [ABC470D] Inverse and Swap

【题目描述】

给定一个长度为 \(N\) 的排列 \(P = (P_1, \dots, P_N)\),其为 \(1\)\(N\) 的一个排列。

你需要按顺序处理 \(Q\) 个操作。操作有以下两种类型:

  • 1 x y:交换 \(P_x\)\(P_y\) 的值。
  • 2:构造排列 \(P' = (P'_1, \dots, P'_N)\),其为 \(1\)\(N\) 的一个排列,满足如下条件(可以证明这样的 \(P'\) 唯一存在):
    • 对于 \(1 \leq i \leq N\),都有 \(P_{P'_i} = i\)
      然后用 \(P'_1,\dots, P'_N\) 的值依次替换 \(P_1,\dots,P_N\)

输出经过所有操作后 \(P_1,\dots,P_N\) 的值,数值之间用空格分隔输出在同一行。

【输入】

输入按如下格式从标准输入读取:

\(N\) \(Q\)
\(P_1\) \(P_2\) \(\cdots\) \(P_N\)
\(\mathrm{query}_1\)

\(\mathrm{query}_Q\)

其中,\(\mathrm{query}_q\) 表示第 \(q\) 个操作,按以下两种格式之一给出:

\(1\) \(x\) \(y\)

\(2\)

【输出】

输出所有操作之后 \(P_1,\dots,P_N\) 的值。数之间用空格分隔输出在一行。

【输入样例】

5 5
2 1 3 5 4
1 2 4
2
1 2 3
1 3 4
2

【输出样例】

4 5 2 1 3

【核心思想】

  1. 问题分析:给定排列 \(P\)\(Q\) 个操作,操作 \(1\) 交换两个位置的值,操作 \(2\)\(P\) 替换为其逆排列 \(P'\)(满足 \(P_{P'_i} = i\))。若每次操作 \(2\) 都显式求逆,复杂度为 \(O(N)\),总复杂度 \(O(NQ)\) 无法接受。核心观察是:操作 \(2\) 等价于切换"当前视角"——在逆排列上执行交换操作,等价于在原排列的逆上执行对应位置的交换。

  2. 算法选择

    • 双数组维护 + 延迟求逆:同时维护原排列 \(a\) 和逆排列 \(b\),用布尔标记 \(flag\) 记录当前处于"原排列模式"还是"逆排列模式"
    • 操作 \(2\)\(O(1)\) 优化:不真正执行求逆,而是翻转 \(flag\),后续操作根据 \(flag\) 决定操作哪个数组
  3. 关键步骤

    • 初始化:读入 \(P\)\(a[i] = P_i\)\(b[a[i]] = i\)\(b\)\(a\) 的逆排列),\(flag = false\)
    • 操作 \(1\)(交换 \(x, y\)
      • \(!flag\)(操作原排列 \(a\)):
        • \(A = a[x], B = a[y]\),交换 \(a[x], a[y]\)
        • 更新逆排列:\(b[A] = y, b[B] = x\)
      • \(flag\)(操作逆排列 \(b\)):
        • \(A = b[x], B = b[y]\),交换 \(b[x], b[y]\)
        • 更新原排列:\(a[A] = y, a[B] = x\)
    • 操作 \(2\)(求逆)\(flag = !flag\)\(O(1)\) 切换模式)
    • 输出:若 \(!flag\) 输出 \(a\),否则输出 \(b\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(Q)\),每次操作 \(O(1)\)
    • 空间复杂度:\(O(N)\),两个排列数组
  5. 延迟求逆的核心思想

    • 对偶性利用:若当前处于逆排列模式,交换操作 \(1\) 的语义自然对应于逆排列上的交换,只需同步维护另一个数组
    • 奇偶性决定最终输出:操作 \(2\) 执行偶数次等于无操作,奇数次等于执行一次,用 \(flag\) 记录奇偶性
    • 避免显式求逆\(O(N)\) 的求逆被 \(O(1)\) 的模式切换替代,通过操作语义的对偶性保证正确性
    • 适用于频繁求逆/翻转、需要维护双向映射的数据结构问题

【算法标签】

普及 #模拟

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 500005; // 最大排列长度
int n, q; // n:排列长度, q:查询数量
int a[N], b[N]; // a[i]:排列P中位置i的值, b[i]:值i所在的位置(即b是a的逆排列)

int main()
{
    cin >> n >> q; // 读入排列长度和查询数量
    for (int i=1; i<=n; i++) // 读入排列P
    {
        cin >> a[i]; // a[i]表示位置i上的值
        b[a[i]] = i; // b[v]表示值v所在的位置,建立逆排列关系
    }
    bool flag = false; // flag标记当前是否处于"逆排列模式"
    // flag=false时操作的是原排列a,flag=true时操作的是逆排列b
    while (q--) // 循环处理每个查询
    {
        int op, x, y; // op:操作类型, x,y:操作参数
        cin >> op; // 读入操作类型
        if (op==1) // 操作1:交换两个位置的值
        {
            cin >> x >> y; // 读入要交换的两个位置
            if (!flag) // 当前操作原排列a
            {
                int A = a[x], B = a[y]; // 记录位置x和y上的值
                swap(a[x], a[y]); // 交换a中位置x和y的值
                // 更新逆排列b:值A现在到了位置y,值B现在到了位置x
                b[A] = y, b[B] = x;
            }
            else // 当前操作逆排列b(相当于对原排列求逆后再操作)
            {
                int A = b[x], B = b[y]; // 记录"值"x和y所在的位置
                swap(b[x], b[y]); // 交换b中"值"x和y的位置信息
                // 更新原排列a:位置A上的值变为y,位置B上的值变为x
                a[A] = y, a[B] = x;
            }

        }
        else // 操作2:对当前排列求逆(即构造满足P_{P'_i}=i的P')
        {
            flag = !flag; // 切换模式:原排列<->逆排列
            // 利用flag的切换来延迟执行求逆操作,避免O(N)的显式求逆
        }
    }
    if (!flag) // 如果最终flag为false,直接输出原排列a
    {
        for (int i=1; i<=n; i++)
            cout << a[i] << " ";
        cout << endl;
    }
    else // 如果最终flag为true,说明进行了奇数次求逆,输出逆排列b
    {
        for (int i=1; i<=n; i++)
            cout << b[i] << " ";
        cout << endl;
    }
    return 0;
}

【运行结果】

5 5
2 1 3 5 4
1 2 4
2
1 2 3
1 3 4
2
4 5 2 1 3
posted @ 2026-08-23 21:53  团爸讲算法  阅读(6)  评论(0)    收藏  举报