2025南昌CCPC 挑战图同构 D题解
D. 挑战图同构 题解
题意
给定两张点编号相同的带权无向图 G1, G2。
定义一条路径的权值为路径上所有边权的最大值。两点 u, v 的距离 dis(u, v) 是所有 u 到 v 路径权值的最小值;若不连通,则距离为 +inf。
要求判断对于所有不同点对 (u, v),两张图中的 dis(u, v) 是否完全相等。
注意这里不允许重新编号,所以并不是传统图同构,而是要求相同编号点之间的距离矩阵完全一致。
关键观察
对于一张带权无向图,若只保留边权 <= x 的边,那么:
- 若
u, v在这个子图中连通,说明存在一条路径,使得路径最大边权<= x; - 若不连通,说明不存在路径最大边权
<= x。
因此,dis(u, v) 等于在按边权从小到大加入边的过程中,u, v 第一次变为连通时的边权。
于是,两张图的所有 dis(u, v) 完全相等,当且仅当:
对于每一个可能的权值 x,加入所有边权 <= x 的边后,两张图得到的连通块划分完全相同。
也就是说,我们不需要真的计算所有点对距离,只需要模拟 Kruskal 加边过程,并在每个权值层结束后比较两张图当前的连通块划分。
做法
分别维护两张图的并查集。
把两张图的边都按权值从小到大处理。每次取当前两张图中最小的边权 w,然后:
- 在
G1中加入所有权值为w的边; - 在
G2中加入所有权值为w的边; - 比较这一次发生变化的点所在连通块,在两张图中是否完全相同。
如果某个权值处理完后,两张图的连通块划分不同,则一定存在某些点对在一张图中已经连通,而另一张图中还没有连通,答案为 NO。
若所有权值处理完后都一致,则答案为 YES。
如何比较连通块
朴素地比较两个连通块中包含的点集会很慢。
你的代码中使用了随机哈希来表示集合:
ull hsh[N], sum1[N], sum2[N];
给每个点 i 随机一个 unsigned long long 值 hsh[i]。
一个连通块的哈希值定义为这个连通块内所有点的随机值之和:
sum[root] = hsh[v1] + hsh[v2] + ... + hsh[vk]
并查集合并两个连通块时,也同步合并哈希值:
sum[l] += sum[r];
这样,若两个连通块包含的点集相同,它们的哈希和一定相同;若点集不同,它们的哈希和极大概率不同。
所以比较两个点 i 在两张图中所在连通块是否相同,只需要比较:
sum1[find(i, f1, sum1)] == sum2[find(i, f2, sum2)]
这是一种 Monte Carlo 做法,存在极小概率哈希冲突,但使用 64 位随机哈希后,实际比赛中通常可以接受。
如果想写完全确定性做法,可以用 set / map 维护连通块成员,或者给连通块做确定性编号,但实现会更复杂。
为什么只检查发生变化的点
代码中用全局集合 s 记录本轮合并涉及到的端点:
set<int> s;
每次成功合并时:
s.insert(x);
s.insert(y);
如果某个权值层结束后,两张图的连通块划分发生了变化,那么变化一定出现在本轮被合并影响到的连通块里。
没有参与本轮合并的点,它们所在的连通块没有变化;如果上一轮已经相同,这一轮仍然相同。
因此只检查 s 中的点即可。
代码实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int N=1e5+5;
// 随机哈希(空间O(n))
ull hsh[N], sum1[N], sum2[N];
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
int find(int x,vector<int>&f, ull sum[]){
if(f[x]<0) return x;
return f[x]=find(f[x],f,sum);
}
set<int>s;
struct node{
int w, u, v;
bool operator<(const node& other) const {
return w > other.w;
}
};
void Union(int x,int y,vector<int>&f, ull sum[]){
int l=find(x,f,sum);
int r=find(y,f,sum);
if(l==r) return;
// 按大小合并,累加哈希值
if(f[l]<f[r]) swap(l,r);
f[l]+=f[r];
f[r]=l;
sum[l]+=sum[r];
s.insert(x);
s.insert(y);
}
int main(){
// 预处理随机哈希值
for(int i=1;i<N;i++) hsh[i]=rnd();
int t;
cin>>t;
while(t--){
int n,m1,m2;
cin>>n>>m1>>m2;
// 初始化哈希和
for(int i=1;i<=n;i++){
sum1[i]=sum2[i]=hsh[i];
}
s.clear();
priority_queue<node>q1,q2;
vector<int>f1(n+1,-1),f2(n+1,-1);
int u,v,w;
for(int i=1;i<=m1;i++){
cin>>u>>v>>w;
q1.push({w,u,v});
}
for(int i=1;i<=m2;i++){
cin>>u>>v>>w;
q2.push({w,u,v});
}
int ok=1;
while(!q1.empty()&&!q2.empty()){
int ans;
s.clear();
ans=min(q1.top().w,q2.top().w);
while(!q1.empty()&&q1.top().w==ans){
auto it=q1.top();q1.pop();
Union(it.u,it.v,f1,sum1);
}
while(!q2.empty()&&q2.top().w==ans){
auto it=q2.top();q2.pop();
Union(it.u,it.v,f2,sum2);
}
for(int i:s){
int r1=find(i,f1,sum1);
int r2=find(i,f2,sum2);
// 比较哈希和
if(sum1[r1]!=sum2[r2]){
ok=0;break;
}
}
if(!ok) break;
}
if(ok){
s.clear();
while(!q1.empty()){
auto it=q1.top();q1.pop();
Union(it.u,it.v,f1,sum1);
}
while(!q2.empty()){
auto it=q2.top();q2.pop();
Union(it.u,it.v,f2,sum2);
}
for(int i:s){
int r1=find(i,f1,sum1);
int r2=find(i,f2,sum2);
if(sum1[r1]!=sum2[r2]) ok=0;
}
}
cout << (ok ? "YES" : "NO") << '\n';
}
}
正确性证明
引理 1
在一张图中,点对 u, v 的距离 dis(u, v) <= x,当且仅当在只保留边权 <= x 的子图中,u, v 连通。
证明:
若 dis(u, v) <= x,则存在一条从 u 到 v 的路径,其最大边权不超过 x,所以这条路径上的所有边都在边权 <= x 的子图中,u, v 连通。
反过来,若 u, v 在边权 <= x 的子图中连通,则存在一条路径,其所有边权都不超过 x,路径最大边权不超过 x,因此 dis(u, v) <= x。
引理成立。
引理 2
两张图所有点对距离完全相等,当且仅当对任意权值 x,两张图中边权 <= x 的子图具有完全相同的连通块划分。
证明:
由引理 1 可知,某个点对在阈值 x 下是否连通,完全由 dis(u, v) <= x 决定。
如果两张图所有点对距离相等,那么对任意 x,每个点对在两图中的连通性都相同,所以连通块划分相同。
如果对任意 x 连通块划分都相同,那么任意点对第一次变为连通的权值也相同,因此距离相同。
引理成立。
引理 3
算法在每个出现过的边权处理完后检查连通块划分,足以判断所有阈值 x 的连通块划分是否相同。
证明:
连通块划分只会在加入某条边时发生变化,也就是只可能在某个出现过的边权处发生变化。
若 x 位于两个相邻边权之间,两张图中边权 <= x 的子图与处理完前一个边权后的状态完全相同。
因此只检查出现过的边权即可。
引理成立。
定理
算法输出 YES 当且仅当两张图在题目定义下同构。
证明:
算法按边权从小到大同步处理两张图,并在每个权值层结束后比较两张图的连通块划分。
若算法发现某一层后划分不同,根据引理 2,存在点对距离不同,答案应为 NO。
若算法始终没有发现不同,则所有出现过的边权层后的划分都相同。根据引理 3,可知任意阈值下划分都相同。再由引理 2,两张图所有点对距离完全相等,答案应为 YES。
定理成立。
复杂度分析
设两张图边数分别为 m1, m2。
使用优先队列按边权取边:
- 建堆 / 入队复杂度为
O((m1 + m2) log(m1 + m2)); - 并查集操作近似
O(alpha(n)); - 每次合并只把端点加入检查集合,整体检查次数与成功合并相关。
总时间复杂度约为:
O((m1 + m2) log(m1 + m2))
空间复杂度:
O(n + m1 + m2)
其中随机哈希数组和并查集都是 O(n),优先队列存边为 O(m1 + m2)。
代码思路对应
你的代码中:
f1, f2:两张图的并查集;sum1, sum2:两张图每个连通块的随机哈希和;q1, q2:分别存储两张图的边,按边权从小到大弹出;Union:合并连通块,同时维护哈希和;s:记录本轮受影响的点,只检查这些点所在连通块是否一致;ok:标记当前是否仍可能同构。
整体逻辑就是:按权值同步做 Kruskal,并判断两张图在每个权值层后的连通块划分是否相同。

浙公网安备 33010602011771号