16 KiB
title | description | date | lastmod | slug | categories | tags | math | |||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
南软/智软2025年开放日机试第1题 | 南京大学软件学院/智能软件与工程学院开放日机试第1题,图论+并查集问题 | 2025-07-27T11:59:00+08:00 | 2025-07-28T22:19:00+08:00 | nju01 |
|
|
true |
题目
有一个圆上均匀分布着L个点(编号按逆时针顺序依次为1~L)。在这些点中还存在m条弦。如果在圆弧上从一个点走到另一个相邻的点,需要支付1元的费用;但如果通过弦来走(包括交点),则无需支付费用。 例如,如图所示,如果存在弦(1,3)和(2,4),则从点1到点2可以先从1走到两条弦的交点,再从交点走到2,这样就无需收费。请你设计算法,找出某两个点之间最少的交通费。
- 程序的第一行输入三个整数:L、m、q,用空格分隔。
- 接下来输入m行,每行两个整数,表示一条弦。
- 接下来输入q行,每行两个整数,表示q个问题。如“1 2”则表示一个问题,表示点1和2之间的最少交通费。
- 程序的输出为q行,每行为一个问题的答案。
注:题中 L 最大为 3 × 10^8

示例输入
5 2 1
1 3
2 4
1 2
示例输出
0
机试情况
南软/智软的夏令营机试是4小时4道题,每题100分,满分400分。4小时内排行榜上此题无人AC,不过有人拿到60-70分。我自己在考场也是只拿到部分分数(没想到通法,只打表了一些少数点的情况),事后思考后才成功解决。
解题思路
我们可以自然地把这个问题抽象为一个图,其中包含两种不同代价的边:
- 圆弧边:连接圆上相邻的两个点,例如点 i 和点 i+1(以及点 L 和点 1)。走这些边需要花费1元,因此它们的边权为 1。
- 免费边:所有通过弦和弦的交点构成的路径。走这些边无需花费,因此它们的边权为 0。
问题的关键在于,所有通过弦和交点能够互相到达的点,实际上构成了一个“免费交通网络”。网络内的任意两点之间都可以零费用到达。我们可以把这样一个网络视为一个连通分量。
所以我们可以用并查集 来高效地处理和合并这些连通分量。
- 初始化:将圆上的
L
个点每一个都看作一个独立的集合。 - 合并弦端点:对于给定的
m
条弦,每条弦(u, v)
都意味着u
和v
是零费用连通的。我们将u
和v
所在的集合合并。 - 合并相交弦:接下来,我们需要找出所有相交的弦。
- 如何判断两条弦是否相交? 假设有两条弦
(a, b)
和(c, d)
。为了方便判断,我们先将每条弦的端点按编号从小到大排序,即u1 = min(a, b), v1 = max(a, b)
和u2 = min(c, d), v2 = max(c, d)
。 这两条弦在圆内相交的充要条件是,它们的端点在圆上是交错排列的。也就是说,必须满足u1 < u2 < v1 < v2
或者u2 < u1 < v2 < v1
。 - 对于每一对相交的弦,例如
(a, b)
和(c, d)
,它们的所有四个端点a, b, c, d
都应该在同一个零费用连通分量中。我们只需将其中任意一个点(如a
)与另一条弦的任意一个点(如c
)所在的集合合并即可。
- 如何判断两条弦是否相交? 假设有两条弦
完成以上步骤后,并查集就完整地记录了所有的零费用连通分量。
第二步:计算两个点之间的最短交通费
对于每一个查询 (s, t)
:
- 首先,我们使用并查集的
find
操作检查s
和t
是否在同一个连通分量中。- 如果
find(s) == find(t)
,说明它们在同一个免费交通网络内,可以直接到达,费用为 0。
- 如果
- 如果它们不在同一个连通分量中,费用就来自于在圆弧上从一个连通分量“跳”到另一个连通分量的次数。这可以转化为一个在 “分量图” 上的最短路问题。
- 图中的每个节点代表一个连通分量。
- 如果圆弧上相邻的两个点
i
和i+1
属于不同的连通分量(即find(i) != find(i+1)
),我们就在这两个分量对应的节点之间连一条边,权重为 1。 - 问题就变成了,在分量图上,从
s
所在的分量走到t
所在的分量,最少需要经过几条边。这是一个典型的无权图最短路问题,可以使用BFS 来解决。
C++ 代码
#include <iostream>
#include <vector>
#include <numeric> // iota
#include <algorithm> // swap, min, max
#include <utility> // pair
#include <map>
#include <queue>
// -------- DSU 模板 --------
class DSU
{
public:
vector<int> parent;
vector<int> sz; // 按大小合并的依据 (避免与C++的size()函数重名,改为sz)
int count; // 联通分量
DSU(int n)
{
count = n;
parent.resize(n);
sz.resize(n);
iota(parent.begin(), parent.end(), 0); // 从0开始连续填充
sz.assign(n, 1);
}
int find(int i)
{
if (parent[i] == i)
return i;
return parent[i] = find(parent[i]);
}
void unite(int a, int b)
{
int root_a = find(a), root_b = find(b);
if (root_a != root_b)
{
if (sz[root_a] < sz[root_b])
std::swap(root_a, root_b);
// a是大树,b合并到a
parent[root_b] = root_a;
sz[root_a] += sz[root_b];
count--;
}
}
bool is_connected(int a, int b)
{
return find(a) == find(b);
}
// 获取联通分量
int get_count() const
{
return count;
}
// 获取i所在集合的大小
int get_size(int i)
{
return sz[find(i)];
}
};
// -------- DSU 模板结束 --------
int main()
{
// C++ 标准输入输出加速
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int L, m, q;
cin >> L >> m >> q;
// --- 步骤 1: 预处理,构建零费用连通分量 ---
// DSU对象,大小为L+1以方便使用1-based索引
DSU dsu(L + 1);
vector<pair<int, int>> chords;
for (int i = 0; i < m; ++i)
{
int u, v;
cin >> u >> v;
// 存储弦,并保证端点有序,方便后续判断
chords.push_back({std::min(u, v), std::max(u, v)});
// 合并弦的两个端点
dsu.unite(u, v);
}
// 检查所有弦的配对,看它们是否相交 (O(m^2))
for (int i = 0; i < m; ++i)
{
for (int j = i + 1; j < m; ++j)
{
int u1 = chords[i].first;
int v1 = chords[i].second;
int u2 = chords[j].first;
int v2 = chords[j].second;
// 判断相交:端点是否交错排列
// (u1 < u2 < v1 < v2) 或 (u2 < u1 < v2 < v1)
if ((u1 < u2 && u2 < v1 && v1 < v2) || (u2 < u1 && u1 < v2 && v2 < v1))
{
// 如果相交,合并它们所在的集合
// 只需要合并任意两个不同弦上的点即可
dsu.unite(u1, u2);
}
}
}
// --- 步骤 2: 构建“分量图” ---
// 使用 map 将 DSU 的根节点映射到从 0 开始的连续索引
map<int, int> comp_map;
int comp_idx_counter = 0;
for (int i = 1; i <= L; ++i) {
int root = dsu.find(i);
if (comp_map.find(root) == comp_map.end()) {
comp_map[root] = comp_idx_counter++;
}
}
int num_components = comp_map.size();
vector<vector<int>> comp_adj(num_components);
// 遍历圆周上的所有相邻点对,构建分量图的邻接表
for (int i = 1; i <= L; ++i)
{
int p1 = i;
int p2 = (i == L) ? 1 : i + 1; // p2是p1在圆上的下一个点
// 如果相邻点属于不同分量,则在分量图上添加一条边
if (!dsu.is_connected(p1, p2))
{
int root1 = dsu.find(p1);
int root2 = dsu.find(p2);
int idx1 = comp_map[root1];
int idx2 = comp_map[root2];
comp_adj[idx1].push_back(idx2);
comp_adj[idx2].push_back(idx1);
}
}
// --- 步骤 3: 处理查询 ---
for (int i = 0; i < q; ++i)
{
int s, t;
cin >> s >> t;
// 如果起点和终点在同一个分量,费用为0
if (dsu.is_connected(s, t))
{
cout << 0 << "\n";
continue;
}
// 否则,在分量图上运行BFS
int start_comp_idx = comp_map[dsu.find(s)];
int end_comp_idx = comp_map[dsu.find(t)];
queue<pair<int, int>> bfs_q; // 存储 {当前分量索引, 距离}
vector<int> dist(num_components, -1); // -1表示未访问
bfs_q.push({start_comp_idx, 0});
dist[start_comp_idx] = 0;
while (!bfs_q.empty())
{
auto [curr_comp, d] = bfs_q.front();
bfs_q.pop();
if (curr_comp == end_comp_idx)
{
cout << d << "\n";
break;
}
for (int neighbor_comp : comp_adj[curr_comp])
{
if (dist[neighbor_comp] == -1) // 如果邻居未被访问
{
dist[neighbor_comp] = d + 1;
bfs_q.push({neighbor_comp, d + 1});
}
}
}
}
return 0;
}
补充:优化思路
由于题目给的 L 最大达到了 3 × 10^8,DSU dsu(L + 1)
创建了两个大小为 L+1 的 vector,当 L = 3*10^8 时,需要 2 * 3*10^8 * 4 bytes ≈ 2.4 GB 的内存,毫无疑问会爆内存。
我们解决这个问题的核心思想是,不需要关心圆上所有的 L 个点。真正影响“免费交通网络”结构和连接性的点,只有那些被明确提到的“关键点”。
这些“关键点”包括:
- 所有 m 条弦的 2*m 个端点。
- 所有 q 次查询的 2*q 个起点和终点。
除此之外的所有其他点,我们都可以看作是“空白”的弧。只要处理这些数量级很小(最多 2m + 2q 个)的关键点,然后计算它们之间的关系就行了。
对原算法进行两个关键的改造:
- 把 DSU 类从基于 std::vector 的实现改为基于 std::map 的实现。map 只会为我们实际接触到的“关键点”分配内存,而不会预先分配一个大小为 L 的巨大数组。
- 不遍历 1 到 L 的 for 循环,转而只遍历我们收集到的“关键点”,并检查这些关键点与其在圆弧上的前一个点之间的“边界”,看这些边界是否跨越了不同的连通分量。
C++ 代码
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <utility>
#include <map>
#include <queue>
#include <set>
using namespace std;
// -------- 改造后的 DSU 模板 (基于 map) --------
class MapDSU
{
public:
map<int, int> parent;
map<int, int> sz;
// find 操作自动为新遇到的点创建,并返回根节点
int find(int i) {
if (parent.find(i) == parent.end()) {
parent[i] = i;
sz[i] = 1;
return i;
}
if (parent[i] == i) {
return i;
}
return parent[i] = find(parent[i]);
}
void unite(int a, int b) {
int root_a = find(a);
int root_b = find(b);
if (root_a != root_b) {
if (sz[root_a] < sz[root_b]) {
swap(root_a, root_b);
}
parent[root_b] = root_a;
sz[root_a] += sz[root_b];
}
}
bool is_connected(int a, int b) {
// 如果点不存在于map中,说明是孤立点,肯定不连通
if (parent.find(a) == parent.end() || parent.find(b) == parent.end()) {
return find(a) == find(b); // 让find去初始化
}
return find(a) == find(b);
}
};
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int L, m, q;
cin >> L >> m >> q;
// 收集所有关键点然后并查集
MapDSU dsu;
vector<pair<int, int>> chords(m);
set<int> key_points_set; // 使用 set 自动排序和去重
for (int i = 0; i < m; ++i) {
cin >> chords[i].first >> chords[i].second;
if (chords[i].first > chords[i].second) {
swap(chords[i].first, chords[i].second);
}
dsu.unite(chords[i].first, chords[i].second);
key_points_set.insert(chords[i].first);
key_points_set.insert(chords[i].second);
}
// 交叉判断逻辑不变,作用于 MapDSU
for (int i = 0; i < m; ++i) {
for (int j = i + 1; j < m; ++j) {
int u1 = chords[i].first, v1 = chords[i].second;
int u2 = chords[j].first, v2 = chords[j].second;
if ((u1 < u2 && u2 < v1 && v1 < v2) || (u2 < u1 && u1 < v2 && v2 < v1)) {
dsu.unite(u1, u2);
}
}
}
// 将查询点也加入关键点集合
vector<pair<int, int>> queries(q);
for (int i = 0; i < q; ++i) {
cin >> queries[i].first >> queries[i].second;
key_points_set.insert(queries[i].first);
key_points_set.insert(queries[i].second);
}
// 将 set 转换为 vector,方便索引
vector<int> key_points(key_points_set.begin(), key_points_set.end());
// 建优化后的分量图
map<int, int> comp_map;
int comp_idx_counter = 0;
// 只要遍历关键点来找所有连通分量
for (int point : key_points) {
int root = dsu.find(point);
if (comp_map.find(root) == comp_map.end()) {
comp_map[root] = comp_idx_counter++;
}
}
int num_components = comp_map.size();
vector<vector<int>> comp_adj(num_components);
// 关键!!!只检查关键点形成的“边界”
for (int p : key_points) {
// 检查 p 和它在圆上的前一个点 p_prev
int p_prev = (p == 1) ? L : p - 1;
}
// 遍历所有排序后的关键点,检查相邻关键点之间的连接
for (size_t i = 0; i < key_points.size(); ++i) {
int p1 = key_points[i];
int p2 = key_points[(i + 1) % key_points.size()];
if (!dsu.is_connected(p1, p2)) {
// 这两个关键点之间的弧,连了两个不同的分量
int root1 = dsu.find(p1), root2 = dsu.find(p2);
if (comp_map.count(root1) && comp_map.count(root2)) {
int idx1 = comp_map[root1], idx2 = comp_map[root2];
if(idx1 != idx2){
comp_adj[idx1].push_back(idx2);
comp_adj[idx2].push_back(idx1);
}
}
}
}
for (int i = 0; i < q; ++i) {
int s = queries[i].first;
int t = queries[i].second;
if (dsu.is_connected(s, t)) {
cout << 0 << "\n";
continue;
}
// 检查查询点是否在我们的DSU中,如果不在说明它们是孤立点
if(dsu.parent.find(s) == dsu.parent.end() || dsu.parent.find(t) == dsu.parent.end()){
}
int start_comp_idx = comp_map[dsu.find(s)];
int end_comp_idx = comp_map[dsu.find(t)];
queue<pair<int, int>> bfs_q;
vector<int> dist(num_components, -1);
bfs_q.push({start_comp_idx, 0});
dist[start_comp_idx] = 0;
bool found = false;
while (!bfs_q.empty()) {
auto [curr_comp, d] = bfs_q.front();
bfs_q.pop();
if (curr_comp == end_comp_idx) {
cout << d << "\n";
found = true;
break;
}
for (int neighbor_comp : comp_adj[curr_comp]) {
if (dist[neighbor_comp] == -1) {
dist[neighbor_comp] = d + 1;
bfs_q.push({neighbor_comp, d + 1});
}
}
}
if(!found) {
// 如果BFS没找到,说明分量图不连通
// 此时距离就是它们在圆弧上的最短距离
// 备用,以防分量图构建不完全
int dist_arc = abs(s - t);
cout << min(dist_arc, L - dist_arc) << "\n";
}
}
return 0;
}