题解:洛谷 P1444 虫洞
【题目来源】
洛谷:P1444 [USACO1.3] 虫洞 wormhole - 洛谷
【题目描述】
Farmer John 周末进行高能物理实验的结果却适得其反,导致 \(n\) 个虫洞出现在农场上,农场是一个二维平面,没有两个虫洞处于同一位置。
根据他的计算,FJ 知道他的虫洞两两配对,形成 \(\dfrac{n}{2}\) 对配对。例如,如果 \(A\) 和 \(B\) 的虫洞连接成一对,进入虫洞 \(A\) 的任何物体将从虫洞 \(B\) 出去,方向不变;反之亦然。
然而这可能发生相当令人不快的后果。例如,假设有两个成对的虫洞 \(A(1,1)\) 和 \(B(3,1)\),Bessie 从 \((2,1)\) 开始朝着 \(x\) 正方向移动。Bessie 将进入虫洞 \(B(3,1)\),从 \(A(1,1)\) 出去,然后再次进入 \(B\),困在一个无限循环中!
FJ 知道他的农场里每个虫洞的确切位置。他知道 Bessie 总是向 \(x\) 正方向走进来,虽然他不记得贝茜的当前位置。
请帮助 FJ 计算有多少种虫洞配对方案,使得存在一个位置,使得 Bessie 从该位置出发,会被困在一个无限循环中。
【输入】
第一行一个正整数 \(n\),表示虫洞数量。
接下来 \(n\) 行,每行两个整数 \(x,y\),表示一个虫洞的坐标。
【输出】
输出一行一个整数表示答案。
【输入样例】
4
0 0
1 0
1 1
0 1
【输出样例】
2
【核心思想】
-
问题分析:给定 \(n\) 个虫洞的二维坐标,要求将它们两两配对形成 \(n/2\) 对。Bessie 从任意位置向 \(x\) 正方向移动,会进入遇到的第一个虫洞(同 \(y\) 坐标且 \(x\) 更大),从配对虫洞穿出后继续向 \(x\) 正方向移动。求存在无限循环的配对方案数。这是一个DFS 枚举配对 + 环检测问题。
-
算法选择:
- 预处理同层后继:按 \(x\) 坐标排序后,对每个虫洞 \(i\) 预处理
to[i],表示同 \(y\) 坐标下右侧最近的虫洞(即向 \(x\) 正方向走会进入的虫洞) - DFS 枚举所有配对:用
v[i]记录虫洞 \(i\) 的配对对象,递归枚举所有可能的完美匹配 - 环检测验证:对每种配对方案,从每个虫洞出发模拟 Bessie 的行走过程,若访问到已访问节点则存在循环
- 预处理同层后继:按 \(x\) 坐标排序后,对每个虫洞 \(i\) 预处理
-
关键步骤:
- 读入与排序:读入 \(n\) 个虫洞坐标,按 \(x\) 升序、\(y\) 升序排序
- 预处理
to数组:对每个虫洞 \(i\),找到同 \(y\) 坐标且 \(x\) 更大的最近虫洞 \(j\),to[i] = j(若不存在则为 0) - DFS 枚举配对
dfs(x):- 若 \(x > n\),所有虫洞已配对,进入验证阶段
- 若
v[x]已配对,直接dfs(x+1) - 否则枚举 \(i \in [x+1, n]\) 中未配对的虫洞,令
v[x] = i, v[i] = x,递归后回溯
- 验证循环(配对完成时):
- 对每个起始虫洞 \(i\),初始化
vis数组 - 模拟行走:
x = to[v[x]],即从当前虫洞穿出后进入同层下一个虫洞 - 若
vis[to[v[x]]]已为 1,说明进入循环,ans++并返回
- 对每个起始虫洞 \(i\),初始化
- 输出结果:存在循环的配对方案数 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O((n-1)!! \cdot n^2)\),\((n-1)!!\) 为完美匹配数,每种匹配验证 \(O(n^2)\)
- 空间复杂度:\(O(n)\),存储坐标、配对关系和访问标记
-
DFS 配对 + 环检测的核心思想:
- 运动规则建模:Bessie 向 \(x\) 正方向移动,只会进入同 \(y\) 坐标右侧最近的虫洞,用
to数组预处理后行走过程变为确定性跳转 - 配对即图论边:虫洞配对形成无向边,行走过程为
当前虫洞 → 配对虫洞 → 同层后继虫洞 → 配对虫洞 → ...的交替路径 - 循环判定条件:若某虫洞在模拟中被第二次访问,说明路径形成环,Bessie 被困
- 完美匹配枚举:\(n\) 个点的两两配对数为 \((n-1)!!\),DFS 按序枚举保证不重复不遗漏
- 适用于图论配对、状态空间枚举、环检测类问题
- 运动规则建模:Bessie 向 \(x\) 正方向移动,只会进入同 \(y\) 坐标右侧最近的虫洞,用
【解题思路】

【算法标签】
普及 #DFS-图
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int n,to[15],v[15],ans,vis[15];
struct node{
int x,y,id;
}a[15];
bool cmp(node a,node b){ // 按照x坐标从小到大,y坐标从小到大排序
if (a.x!=b.x) return a.x<b.x;
return a.y < b.y;
}
void dfs(int x){
if(x>n){ // 搜索退出条件
for(int i=1;i<=n;i++){//枚举每个节点进去
x=i;
memset(vis,0,sizeof(vis)); // 每轮都需初始化vis数组,用来标记那些节点已经访问过
while(x){ // 还能走
vis[x]=1; // 标记已经走过
if(vis[to[v[x]]]){ // 下一个要进去的洞进去过,v表示相连,to表示最终会到达,如从1出发->走到2->虫洞连到3->走到4->虫洞连到1(循环往复)
ans++; // 配对数自增1
return; // 返回
}
x=to[v[x]]; // 如果没有访问过,修改x,继续往下走
}
}
}
if(v[x]){ // 对于已经相连的节点,直接进行下一轮搜索,如dfs(4),此时v[4]=3
dfs(x+1);
return;
}
for(int i=x+1;i<=n;i++) {
if(!v[i]){ // 如果某个点没有与其他点连接
v[i]=x; // 枚举2个节点,并相连
v[x]=i;
dfs(x+1); // 进行下一个节点的搜索,如1与2,2与3,3与4
v[i]=v[x]=0; // 还原现场
}
}
}
int main(){
cin >> n; // 输入n
for(int i=1;i<=n;i++) { // 遍历n个虫洞
cin >> a[i].x >> a[i].y; // 输入每个虫洞的坐标
}
sort(a+1,a+1+n,cmp); // 按照x坐标和y坐标的从小到大进行排序
for(int i=1;i<=n;i++) { // 双重for循环枚举所有点
for(int j=i+1;j<=n;j++) { // 每个点和后面的点进行配对
if(a[i].y==a[j].y){ // 如果y轴相同(即向右走一定与走到)
to[i]=j; // 将这两个点连起来。
break;
}
}
}
dfs(1); // 进行dfs深搜
cout << ans << endl; // 输出方案数
}
【运行结果】
4
0 0
1 0
1 1
0 1
2
浙公网安备 33010602011771号