Invisible Edges
Invisible Edges
DFS序,栈,括号匹配,通信
一、题意简述
这是一个多阶段构造题,程序需要运行两次:
- 第一阶段(VegiCat):读入一个森林,给每个顶点 (u) 赋一个整数 (a_u)((1 \le a_u \le 2n)),输出该序列。
- 隐藏操作:评测机生成一个隐藏的排列 (p),将原顶点 (u) 重编号为 (p_u),顶点的值跟着顶点走。原来的边 ((u,v)) 变成新边 ((p_u,p_v))。
- 第二阶段(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 的策略
- 对给定的原森林进行 DFS。
- 维护一个计时器
timer,初始为 1。 - 每进入一个顶点
u,令a[u] = timer++。 - 每离开一个顶点,让
timer++占位,不输出。 - 最终输出数组
a即可。
注意坑点:在 DFS 中遍历邻接表时,无论邻居是否已被访问,都必须推进当前节点的访问索引 idx[u],否则遇到已访问的父节点会导致死循环。
2. VegiDog 的策略
- 读入值序列 (b)。建立映射
pos,使得pos[b[i]] = i,表示位置b[i]是顶点i的进入时刻。 - 扫描时刻 (t) 从 (1) 到 (2n):
- 如果
pos[t] != 0,说明这是左括号,将顶点pos[t]压入栈。 - 如果
pos[t] == 0,说明是右括号,弹出栈顶元素u。 - 如果弹出后栈不为空,当前栈顶
v就是u的父节点,记录边(u,v)。
- 如果
- 输出所有记录的边。
注意坑点:压栈时一定要压顶点编号 pos[t],而不是时刻 t,否则输出边的端点会超过 (n)。
四、正确性证明
- 合法性:DFS 遍历产生的进入/离开序列必定是一个合法的括号序列。
- 唯一性:给定所有左括号的位置,右括号的位置被唯一确定。合法的括号序列经过标准的栈匹配,只能恢复出唯一的森林结构。
- 抗重编号干扰:重编号只是修改了顶点的标签,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;
}

浙公网安备 33010602011771号