Invisible Edges

Invisible Edges

DFS序,栈,括号匹配,通信

一、题意简述

这是一个多阶段构造题,程序需要运行两次:

  1. 第一阶段(VegiCat):读入一个森林,给每个顶点 (u) 赋一个整数 (a_u)((1 \le a_u \le 2n)),输出该序列。
  2. 隐藏操作:评测机生成一个隐藏的排列 (p),将原顶点 (u) 重编号为 (p_u),顶点的值跟着顶点走。原来的边 ((u,v)) 变成新边 ((p_u,p_v))。
  3. 第二阶段(VegiDog):只读入 (n) 和重编号后的值序列 (b_1,b_2,\dots,b_n)。在不知道原图和排列 (p) 的情况下,输出重编号后的森林的所有边。

核心限制:两人不能直接传递图结构,只能通过顶点上的值来传递信息。


二、思路推导:为什么是括号匹配?

面对这种“无法传图,只能传值,且顶点编号会被彻底打乱”的问题,我们需要一个与顶点绝对编号无关的图结构编码方式。

1. 观察数据范围与图的性质

  • 图是森林,无环,(m<n)。
  • 值域是 (1\sim 2n),而不是 (n)。这强烈暗示:每个顶点需要“两个状态”或“两个时刻”。
  • 森林最自然的两个时刻就是:进入顶点(左括号) 和 离开顶点(右括号)。

2. DFS 欧拉序与括号序列

对森林进行 DFS 遍历,进入一个点时记录一次,离开时记录一次,一共 (2n) 个事件。

如果进入记作 (,离开记作 ),这就完美构成了一个合法的括号序列。

在括号序列中,只要用栈做匹配,就能唯一确定父子关系,即森林的边。

3. 如何传递信息?

VegiCat 只需要把每个点的 “进入时刻” 作为值赋给该顶点即可。

  • 位置范围正好是 (1\sim 2n)。
  • 因为每个点只进入一次,所以这 (n) 个值互不相同。
  • 剩下的 (n) 个位置自然就是“离开时刻”。

重编号只是把顶点的名字换了,但“第几个时刻进入”这个相对顺序是不变的。Dog 拿到值之后,补全右括号,做一次栈匹配,就能还原出重编号后的边。


三、具体操作

1. VegiCat 的策略

  1. 对给定的原森林进行 DFS。
  2. 维护一个计时器 timer,初始为 1。
  3. 每进入一个顶点 u,令 a[u] = timer++。
  4. 每离开一个顶点,让 timer++ 占位,不输出。
  5. 最终输出数组 a 即可。

注意坑点:在 DFS 中遍历邻接表时,无论邻居是否已被访问,都必须推进当前节点的访问索引 idx[u],否则遇到已访问的父节点会导致死循环。

2. VegiDog 的策略

  1. 读入值序列 (b)。建立映射 pos,使得 pos[b[i]] = i,表示位置 b[i] 是顶点 i 的进入时刻。
  2. 扫描时刻 (t) 从 (1) 到 (2n):
    • 如果 pos[t] != 0,说明这是左括号,将顶点 pos[t] 压入栈。
    • 如果 pos[t] == 0,说明是右括号,弹出栈顶元素 u。
    • 如果弹出后栈不为空,当前栈顶 v 就是 u 的父节点,记录边 (u,v)。
  3. 输出所有记录的边。

注意坑点:压栈时一定要压顶点编号 pos[t],而不是时刻 t,否则输出边的端点会超过 (n)。


四、正确性证明

  1. 合法性:DFS 遍历产生的进入/离开序列必定是一个合法的括号序列。
  2. 唯一性:给定所有左括号的位置,右括号的位置被唯一确定。合法的括号序列经过标准的栈匹配,只能恢复出唯一的森林结构。
  3. 抗重编号干扰:重编号只是修改了顶点的标签,DFS 遍历的结构和括号的匹配关系没有变。Dog 通过值的大小重建了时间线,从而完美复原了重编号后的父子关系。

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e14;

void solvecat() {
    int n, m; cin >> n >> m;
    vector<vector<int>> g(n + 1, vector<int>());
    for(int i = 0; i < m; i ++) {
        int u, v; cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    vector<int> vis(n + 1, 0);
    vector<int> a(n + 1, 0);
    vector<int> stk;
    vector<int> idx(n + 1, 0); // 第n个点,访问到第几层的节点
    int timer = 1;
    // DFS序,用栈 括号匹配模拟 ((())) 1 - 2 - 3的形式, (()()()) 1-2  1-3  1-4的形式,每个括号弹出的时候,与前面一个不完整括号存在边
    // 如此,只要对左括号的位置,对点进行编号,就可以确定入栈顺序
    for(int root = 1; root <= n; root ++) {
        if(vis[root]) continue;
        vis[root] = 1;
        a[root] = timer ++;
        stk.push_back(root);

        while(!stk.empty()) {
            int u = stk.back();
            if(idx[u] < g[u].size()) {
                int v = g[u][idx[u] ++];
                if(!vis[v]) {
                    vis[v] = 1;
                    a[v] = timer ++;
                    stk.push_back(v);
                }
            }
            else {
                timer ++;
                stk.pop_back();
            }
        }
    }

    for(int i = 1; i <= n; i ++) cout << a[i] << " \n"[i == n];
}
void solvedog() {
    int n; cin >> n;
    vector<int> b(n + 1, 0);
    vector<int> pos(n * 2 + 1, 0); //每个编号对应的节点
    for(int i = 1; i <= n; i ++) {
        cin >> b[i];
        pos[b[i]] = i;
    }
    vector<pair<int, int>> edge;
    vector<int> stk;
    for(int i = 1; i <= n * 2; i ++) {
        if(pos[i] != 0) stk.push_back(pos[i]);
        else { // pos[i] = 0,说明存在右括号,弹出点
            int u = stk.back();
            stk.pop_back();
            if(!stk.empty()) { //没空说明存在父节点
                //跟外面的括号存在一条边
                int v = stk.back();
                edge.push_back({u, v});
            }
        }
    }
    cout << edge.size() << '\n';
    for(auto [u, v] : edge) cout << u << " " << v << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    string mode; cin >> mode;
    int _ = 1; cin >> _;
    if(mode == "cat") {
        while(_--) solvecat();
    }
    else {
        while(_--) solvedog();
    }
    
    return 0;
}
posted @ 2026-10-27 15:00  thenrify  阅读(37)  评论(0)    收藏  举报