题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(n\) 个虫洞的二维坐标,要求将它们两两配对形成 \(n/2\) 对。Bessie 从任意位置向 \(x\) 正方向移动,会进入遇到的第一个虫洞(同 \(y\) 坐标且 \(x\) 更大),从配对虫洞穿出后继续向 \(x\) 正方向移动。求存在无限循环的配对方案数。这是一个DFS 枚举配对 + 环检测问题。

  2. 算法选择

    • 预处理同层后继:按 \(x\) 坐标排序后,对每个虫洞 \(i\) 预处理 to[i],表示同 \(y\) 坐标下右侧最近的虫洞(即向 \(x\) 正方向走会进入的虫洞)
    • DFS 枚举所有配对:用 v[i] 记录虫洞 \(i\) 的配对对象,递归枚举所有可能的完美匹配
    • 环检测验证:对每种配对方案,从每个虫洞出发模拟 Bessie 的行走过程,若访问到已访问节点则存在循环
  3. 关键步骤

    • 读入与排序:读入 \(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++ 并返回
    • 输出结果:存在循环的配对方案数 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O((n-1)!! \cdot n^2)\)\((n-1)!!\) 为完美匹配数,每种匹配验证 \(O(n^2)\)
    • 空间复杂度:\(O(n)\),存储坐标、配对关系和访问标记
  5. DFS 配对 + 环检测的核心思想

    • 运动规则建模:Bessie 向 \(x\) 正方向移动,只会进入同 \(y\) 坐标右侧最近的虫洞,用 to 数组预处理后行走过程变为确定性跳转
    • 配对即图论边:虫洞配对形成无向边,行走过程为 当前虫洞 → 配对虫洞 → 同层后继虫洞 → 配对虫洞 → ... 的交替路径
    • 循环判定条件:若某虫洞在模拟中被第二次访问,说明路径形成环,Bessie 被困
    • 完美匹配枚举\(n\) 个点的两两配对数为 \((n-1)!!\),DFS 按序枚举保证不重复不遗漏
    • 适用于图论配对、状态空间枚举、环检测类问题

【解题思路】

【算法标签】

普及 #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
posted @ 2026-08-03 08:47  团爸讲算法  阅读(13)  评论(0)    收藏  举报