前言
"全球校园人工智能算法精英大赛"是江苏省人工智能学会举办的面向全球具有正式学籍的全日制高等院校及以上在校学生举办的算法竞赛。其中的算法巅峰赛属于产业命题赛道,这是第二赛季,对最后一道优化题,采用 虚拟边算法进行求解,这个算法属于真正意义上的 baseline,非常具有教学意义。

回顾
第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第二赛季--最后一题解读
大致题意如下
给定 N 个必选点,M 个可选点,请构建一个联通方案,使得包含 N 个必选点的图 G 其联通的代价(路径和)尽量小?
具体的题目描述和各种算法综述可具体参见上文,这边不再简单重复。
思路
题目有个限制条件,即两个中继器(可选点)不能直接相连。
那如何理解虚拟边这个概念呢?让我们专注于 边。
如果只有必选点P,排除所有可选点O(中继器),那么就退化为纯粹的最小生成树问题。此时生成树对应的边集合 S = { e 1 , . . . , e n − 1 } {S=\{e_1, ..., e_{n - 1}\}} S={e1,...,en−1},任意边 e i e_i ei其两个端点皆为必选点(好像说了句废话)。
但是对于任意边 e i e_i ei, 两端点为 p a p_a pa, p b p_b pb, 如果存在可选点 O c O_c Oc, 使得
d i s t ( p a , p b ) > d i s t ( p a , O c ) + d i s t ( O c , p b ) dist(p_a, p_b) \gt dist(p_a, O_c) + dist(O_c, p_b) dist(pa,pb)>dist(pa,Oc)+dist(Oc,pb)
那么我们把, p a , O c , p b p_a, O_c, p_b pa,Oc,pb 3 点2边,整体代替原先的 p a , p b p_a, p_b pa,pb 2 点1边,然后等价视为新 p a , p b 的 p_a, p_b的 pa,pb的"直连边",这类边称之为"虚拟边"。
因此该算法的核心思路及其简单而高效
-
预处理虚拟边,为任意 2 个必选点 p a , p b p_a, p_b pa,pb, 构建虚拟边 ( p a , o c , p b ) , o c (p_a, o_c, p_b), o_c (pa,oc,pb),oc可为空。
-
采用常规的 MST 算法,计算最小生成树
难点分析
来分析一下预处理的代价
对于任意 2点 p a , p b p_a, p_b pa,pb
v i r t u a l _ e d g e ( p a , p b ) = arg min c ∈ O ( d i s t ( p a , o c ) + d i s t ( o c , p b ) ) virtual\edge(p_a, p_b) ={ \arg\min{c \in O} \big( dist(p_a, o_c) + dist(o_c, p_b) \big) } virtual_edge(pa,pb)=argc∈Omin(dist(pa,oc)+dist(oc,pb))
采用最朴素的算法:
对每一对 ( p a , p b ) (p_a, p_b) (pa,pb), 线性遍历 m 个候选点 o c o_c oc,找出使距离和最小的 o c o_c oc。计算一对点的时间复杂度为 O ( m ) O(m) O(m)。由于点集 P 中共有 n 2 n^2 n2个点对,因此整个算法的时间复杂度为 O ( n 2 ∗ m ) O(n^2*m) O(n2∗m)。
而 n , m ≤ 1500 {n, m \le 1500} n,m≤1500, 因此预处理阶段最坏的结果是 O ( 3.375 ∗ 10 9 ) O(3.375*10^9) O(3.375∗109)。
在 10 秒限制内,遇到机器性能差/波动,c++未开编译开关优化,那有可能超时。
那这个预处理过程,可以优化不?
AI 给出的答案是:引入 KD 树,支持点的最近邻查找。
把所有的可选点,放入 KD 树,然后以 p a , p b p_a, p_b pa,pb两点构建的中点去查询最近邻点,尝试更新虚拟边值,这样就以 O ( l o g ( m ) ) O(log(m)) O(log(m))的代价实现计算。整体的时间复杂度为 O ( n 2 ∗ l o g ( m ) ) O(n^2*log(m)) O(n2∗log(m))。
kd树介绍
k-d树 (k-dimensional tree) 是一种用于分割k维数据空间的数据结构,本质上是一种特殊的二叉树。
它的核心原理是循环选择坐标轴(如先 x 轴,再 y 轴),取该维度上的中位数作为节点,将空间递归地划分为两个半空间,从而构建一棵平衡树。主要应用于多维空间的最近邻搜索和范围查询。
详细内容可参考 OI Wiki 的权威讲解:https://oi-wiki.org/ds/kdt/
空间上

树的结构

这边引用了 oiwiki 上的图。
代码解析
这是 2025 年的 AI 生成版本
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <math.h>
#include <map>
#include <set>
#include <queue>
using namespace std;
typedef long long ll;
struct Device {
int id, px, py;
char type;
};
struct Point {
int px, py;
int dev_id;
};
struct KDNode {
Point point;
KDNode *left, *right;
};
ll dist_sq(int x1, int y1, int x2, int y2) {
ll dx = (ll)x1 - x2;
ll dy = (ll)y1 - y2;
return dx * dx + dy * dy;
}
KDNode* build_kd(vector<Point>& pts, int depth = 0) {
if (pts.empty()) return nullptr;
int axis = depth % 2;
sort(pts.begin(), pts.end(), [axis](const Point& a, const Point& b) {
return axis == 0 ? a.px < b.px : a.py < b.py;
});
size_t med = pts.size() / 2;
KDNode* node = new KDNode();
node->point = pts[med];
vector<Point> left_pts(pts.begin(), pts.begin() + med);
vector<Point> right_pts(pts.begin() + med + 1, pts.end());
node->left = build_kd(left_pts, depth + 1);
node->right = build_kd(right_pts, depth + 1);
return node;
}
void find_nearest(KDNode* node, double tx, double ty, int depth, Point& best, double& best_d) {
if (node == nullptr) return;
double d = pow(node->point.px - tx, 2) + pow(node->point.py - ty, 2);
if (d < best_d) {
best_d = d;
best = node->point;
}
int axis = depth % 2;
double t_coord = axis == 0 ? tx : ty;
double n_coord = axis == 0 ? node->point.px : node->point.py;
KDNode *first = (t_coord < n_coord) ? node->left : node->right;
KDNode *second = (t_coord < n_coord) ? node->right : node->left;
find_nearest(first, tx, ty, depth + 1, best, best_d);
double diff = t_coord - n_coord;
if (diff * diff < best_d) {
find_nearest(second, tx, ty, depth + 1, best, best_d);
}
}
struct VirtualEdge {
int r1, r2;
ll cost;
bool is_direct;
int c_id;
bool operator<(const VirtualEdge& other) const {
return cost < other.cost;
}
};
int find(vector<int>& parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]);
return parent[x];
}
void union_sets(vector<int>& parent, vector<int>& rank, int x, int y) {
int px = find(parent, x), py = find(parent, y);
if (px == py) return;
if (rank[px] < rank[py]) swap(px, py);
parent[py] = px;
if (rank[px] == rank[py]) rank[px]++;
}
int main() {
int N, K;
cin >> N >> K;
vector<Device> devices(N + K);
vector<int> robot_list;
vector<Point> relay_list;
for (int i = 0; i < N + K; i++) {
cin >> devices[i].id >> devices[i].px >> devices[i].py >> devices[i].type;
if (devices[i].type != 'C') {
robot_list.push_back(i);
} else {
relay_list.push_back({devices[i].px, devices[i].py, devices[i].id});
}
}
int M = robot_list.size();
KDNode* kd_root = nullptr;
if (!relay_list.empty()) {
vector<Point> pts = relay_list;
kd_root = build_kd(pts);
}
vector<VirtualEdge> all_vedges;
for (int ri = 0; ri < M; ++ri) {
for (int rj = ri + 1; rj < M; ++rj) {
int i = robot_list[ri], j = robot_list[rj];
Device& d1 = devices[i], &d2 = devices[j];
ll d_sq = dist_sq(d1.px, d1.py, d2.px, d2.py);
bool both_R = (d1.type == 'R' && d2.type == 'R');
ll direct_scaled = (both_R ? 5LL : 4LL) * d_sq;
ll via_scaled = LLONG_MAX / 2;
int used_c = -1;
if (kd_root) {
double mx = (double(d1.px) + d2.px) / 2.0;
double my = (double(d1.py) + d2.py) / 2.0;
Point best;
double best_d = numeric_limits<double>::infinity();
find_nearest(kd_root, mx, my, 0, best, best_d);
ll via = dist_sq(d1.px, d1.py, best.px, best.py) + dist_sq(d2.px, d2.py, best.px, best.py);
via_scaled = 5LL * via;
used_c = best.dev_id;
}
ll eff_scaled = min(direct_scaled, via_scaled);
bool is_dir = (direct_scaled <= via_scaled);
all_vedges.push_back({ri, rj, eff_scaled, is_dir, used_c});
}
}
sort(all_vedges.begin(), all_vedges.end());
vector<int> parent(M), rank(M, 0);
for (int i = 0; i < M; ++i) parent[i] = i;
vector<VirtualEdge> used_vedges;
for (auto& ve : all_vedges) {
if (find(parent, ve.r1) != find(parent, ve.r2)) {
union_sets(parent, rank, ve.r1, ve.r2);
used_vedges.push_back(ve);
}
}
set<int> used_relays;
set<pair<int, int>> links;
for (auto& ve : used_vedges) {
int id1 = devices[robot_list[ve.r1]].id;
int id2 = devices[robot_list[ve.r2]].id;
if (ve.is_direct) {
int a = min(id1, id2), b = max(id1, id2);
links.insert({a, b});
} else {
int c = ve.c_id;
used_relays.insert(c);
int a = min(id1, c), b = max(id1, c);
links.insert({a, b});
a = min(id2, c), b = max(id2, c);
links.insert({a, b});
}
}
if (used_relays.empty()) {
cout << "#" << endl;
} else {
auto it = used_relays.begin();
cout << *it;
++it;
for (; it != used_relays.end(); ++it) {
cout << "#" << *it;
}
cout << endl;
}
if (links.empty()) {
cout << "#" << endl;
} else {
auto it = links.begin();
cout << it->first << "-" << it->second;
++it;
for (; it != links.end(); ++it) {
cout << "#" << it->first << "-" << it->second;
}
cout << endl;
}
}
补充
写在最后
