From 8a02550dec07890a4021e3adb85d8529919cd12a Mon Sep 17 00:00:00 2001 From: ember <1279347317@qq.com> Date: Thu, 28 Aug 2025 20:43:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/post/算法题/nju01.md | 173 +++++++++++++++++------------------ 1 file changed, 84 insertions(+), 89 deletions(-) diff --git a/content/post/算法题/nju01.md b/content/post/算法题/nju01.md index 7d0d20e..d18770d 100644 --- a/content/post/算法题/nju01.md +++ b/content/post/算法题/nju01.md @@ -2,7 +2,7 @@ title: 南软/智软2025年开放日机试第1题 description: 南京大学软件学院/智能软件与工程学院开放日机试第1题,图论+并查集问题 date: 2025-07-27T11:59:00+08:00 -lastmod: 2025-07-28T22:19:00+08:00 +lastmod: 2025-08-28T20:20:00+08:00 slug: nju01 # image: helena-hertz-wWZzXlDpMog-unsplash.jpg categories: @@ -88,6 +88,8 @@ math: true #include #include +using namespace std; + // -------- DSU 模板 -------- class DSU { @@ -270,28 +272,34 @@ int main() # 补充:优化思路 -由于题目给的 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 最大可达 3 * 10^8,上面代码会导致MLE。DSU dsu(L + 1) 至少需要约 2.4 GB 内存,显然是不可接受的。 -我们解决这个问题的核心思想是,不需要关心圆上所有的 L 个点。真正影响“免费交通网络”结构和连接性的点,只有那些被明确提到的“关键点”。 +解决这个问题的核心思想是**离散化**,也称**关键点法**。我们无需关心圆上所有的 `L` 个点,真正影响“免费交通网络”结构和连接性的,只有那些被明确提到的“关键点”。 这些“关键点”包括: -- 所有 m 条弦的 2*m 个端点。 -- 所有 q 次查询的 2*q 个起点和终点。 +- 所有 `m` 条弦的 `2*m` 个端点。 +- 所有 `q` 次查询的 `2*q` 个起点和终点。 -除此之外的所有其他点,我们都可以看作是“空白”的弧。只要处理这些数量级很小(最多 2m + 2q 个)的关键点,然后计算它们之间的关系就行了。 +除此之外的所有其他点,我们都可以看作是“空白”的弧。我们只需处理这数量级很小(最多 2m + 2q 个)的关键点,并计算它们之间的关系即可。 -对原算法进行两个关键的改造: +对原算法进行如下两个关键的改造: -- 把 DSU 类从基于 std::vector 的实现改为基于 std::map 的实现。map 只会为我们实际接触到的“关键点”分配内存,而不会预先分配一个大小为 L 的巨大数组。 -- 不遍历 1 到 L 的 for 循环,转而只遍历我们收集到的“关键点”,并检查这些关键点与其在圆弧上的前一个点之间的“边界”,看这些边界是否跨越了不同的连通分量。 +1. **使用基于 `std::map` 的并查集(DSU)。** 我们将 DSU 的底层实现从 `std::vector` 改为 `std::map`。`map` 只会为我们实际接触到的“关键点”动态分配内存,而不会预先分配一个大小为 L 的巨大数组。这直接将空间复杂度从 `O(L)` 降至 `O(m+q)`。 +2. **高效构建带权的“分量图”并使用Dijkstra算法。**我们不再遍历 `1` 到 `L` 来建图,而是只关注由所有**关键点**分割出的**关键弧**。 + - 首先,我们将所有关键点收集起来,并进行排序和去重。 + - 然后,我们遍历这个排好序的关键点列表。对于每一对**在圆弧上相邻**的关键点(例如列表中的 `p_i` 和 `p_{i+1}`,以及最后一个点和第一个点形成的环形弧),它们之间就构成了一段关键弧。 + - 如果这段弧两端的关键点 `p_i` 和 `p_{i+1}` 属于**不同的连通分量**(即 `find(p_i) != find(p_{i+1})`),那么这段弧就是连接两个免费区的“付费桥梁”。 + - **关键修正一:** 我们就在这两个分量对应的节点之间连一条边。这条边的**权重**并非固定的1,而是这段弧的**实际长度**(例如 `p_{i+1} - p_i`)。 + - **关键修正二:** 因为边的权重不同,使“分量图”成为一个**带权图**。因此,在求解两个分量间的最短路时,我们必须使用 **Dijkstra 算法**,而非原思路中的 BFS。 + +这样,我们就在时间和空间上都高效地解决了这个问题。算法复杂度只与 `m` 和 `q` 的大小相关,而与巨大的 `L` 无关。 ## C++ 代码 ```cpp #include #include -#include #include #include #include @@ -300,19 +308,16 @@ int main() using namespace std; -// -------- 改造后的 DSU 模板 (基于 map) -------- -class MapDSU -{ +// -------- 基于 map 的 DSU 模板 -------- +class MapDSU { public: map parent; map 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; @@ -333,27 +338,26 @@ public: } 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去初始化 - } + // find会自动初始化不存在的点 return find(a) == find(b); } }; -int main() -{ +const long long INF = 1e18; + +int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); - int L, m, q; + long long L; + int m, q; cin >> L >> m >> q; - // 收集所有关键点然后并查集 MapDSU dsu; vector> chords(m); - set key_points_set; // 使用 set 自动排序和去重 + set key_points_set; + // --- 步骤1: 合并弦和相交弦 --- for (int i = 0; i < m; ++i) { cin >> chords[i].first >> chords[i].second; if (chords[i].first > chords[i].second) { @@ -364,18 +368,20 @@ int main() 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)) { + long long u1 = chords[i].first, v1 = chords[i].second; + long long u2 = chords[j].first, v2 = chords[j].second; + if (u1 > u2) { + swap(u1, u2); swap(v1, v2); + } + if (u1 < u2 && u2 < v1 && v1 < v2) { dsu.unite(u1, u2); } } } - - // 将查询点也加入关键点集合 + + // --- 步骤2: 收集所有关键点 --- vector> queries(q); for (int i = 0; i < q; ++i) { cin >> queries[i].first >> queries[i].second; @@ -383,95 +389,84 @@ int main() key_points_set.insert(queries[i].second); } - // 将 set 转换为 vector,方便索引 vector key_points(key_points_set.begin(), key_points_set.end()); - // 建优化后的分量图 - map comp_map; + // --- 步骤3: 构建带权的“分量图” --- + map root_to_idx; // DSU根节点到分量图新索引的映射 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++; + if (root_to_idx.find(root) == root_to_idx.end()) { + root_to_idx[root] = comp_idx_counter++; } } - int num_components = comp_map.size(); - vector> comp_adj(num_components); + int num_components = root_to_idx.size(); + // 邻接表存储 {邻居分量索引, 权重} + vector>> 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]; + // p2 是 p1 在关键点列表中的下一个点(包括环形) 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); - } + int root1 = dsu.find(p1); + int root2 = dsu.find(p2); + + if (root1 != root2) { + long long dist; + if (i == key_points.size() - 1) { // 最后一个点到第一个点的环形距离 + dist = (L - p1) + p2; + } else { + dist = p2 - p1; } + int idx1 = root_to_idx[root1]; + int idx2 = root_to_idx[root2]; + comp_adj[idx1].push_back({idx2, dist}); + comp_adj[idx2].push_back({idx1, dist}); } } - - for (int i = 0; i < q; ++i) { - int s = queries[i].first; - int t = queries[i].second; + // --- 步骤4: 处理查询 --- + for (const auto& query : queries) { + int s = query.first; + int t = query.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)]; + int start_root = dsu.find(s); + int end_root = dsu.find(t); + int start_idx = root_to_idx[start_root]; + int end_idx = root_to_idx[end_root]; - queue> bfs_q; - vector dist(num_components, -1); + // Dijkstra 算法 + priority_queue, vector>, greater>> pq; + vector dist(num_components, INF); - 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(); + dist[start_idx] = 0; + pq.push({0, start_idx}); - if (curr_comp == end_comp_idx) { - cout << d << "\n"; - found = true; - break; - } + while (!pq.empty()) { + auto [d, u] = pq.top(); + pq.pop(); - 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 (d > dist[u]) continue; + if (u == end_idx) break; + + for (const auto& edge : comp_adj[u]) { + int v = edge.first; + long long weight = edge.second; + if (dist[u] + weight < dist[v]) { + dist[v] = dist[u] + weight; + pq.push({dist[v], v}); } } } - if(!found) { - // 如果BFS没找到,说明分量图不连通 - // 此时距离就是它们在圆弧上的最短距离 - // 备用,以防分量图构建不完全 - int dist_arc = abs(s - t); - cout << min(dist_arc, L - dist_arc) << "\n"; - } + cout << dist[end_idx] << "\n"; } return 0;