GESP2026年6月认证C++八级( 第三部分编程题(1、线网建设))精讲


第三部分 第一题

《线网建设》

------通信王国里的修路大师(Kruskal最小生成树)


第一幕:通信王国

1、很久很久以前。

程序大陆上,有一个十分发达的国家------

通信王国。

王国里有很多座通信基站。

每座基站都有自己的坐标,例如:

复制代码
① (1,0)

② (3,2)

③ (5,1)

④ (2,4)

2、国王希望:

任意两座基站,都能够互相通信。

但是......

修建线路可是要花钱的!

线路越长,花的钱越多。

更麻烦的是:

超过 L 米的线路根本修不了。

于是国王说:

"请你帮我设计一套最省钱的建设方案。"

这就是今天这道题。


第二幕:读懂题目

1、输入:

复制代码
n

个点。


2、每个点:

复制代码
(x,y)

坐标。


3、两个点之间距离

就是:

复制代码
√((x1-x2)²+(y1-y2)²)

但是:

只有

复制代码
距离≤L

才能修。


4、最后要求:

所有点连通。

总代价最小。


5、如果不能连通:

输出

复制代码
Impossible

6、举一个最简单的例子

(1)例如:

复制代码
A

B

C

三座城市。


(2)距离:

复制代码
A-B 2

A-C 5

B-C 3

画出来:

复制代码
       A

   2 /   \5

    /     \

   B -- 3 --C

(3)有三条路。

如果全部修:

总代价:

复制代码
2+5+3=10

太浪费。


(4)其实修:

复制代码
A-B

B-C

总代价:

复制代码
5

所有城市仍然连通。

这就是:

最小生成树(MST)


第三幕:什么叫生成树?

1、有的同学会问:

为什么叫生成树?

(1)假设有:

4个城市。

全部修:

复制代码
A-----B
|\   /|
| \ / |
| / \ |
|/   \|
C-----D

里面有很多圈。


(2)其实:

有些路可以拆掉。


(3)最后:

复制代码
A

|

B

|

C

|

D

或者:

复制代码
A

/ \

B  C

    |

    D

(4)只要:

所有点连通。

没有环。

这就是:

生成树。


2、为什么要最小?

(1)因为:

生成树有很多种。


(2)我们要:

总代价

最小。


(3)所以:

最小生成树(Minimum Spanning Tree)


第四幕:怎样才能花的钱最少?

这就是整道题最关键的问题。

(1)假设现在有这些边:

复制代码
长度

8

2

5

1

9

3

(2)如果你是国王。

你会先修哪条?


(3)当然:

最便宜!


所以:

第一条原则:

先修最短的路。


(4)于是排序:

变成:

复制代码
1

2

3

5

8

9

是不是就结束了?

不是!


第五幕:为什么不能一直修?

(1)来看:

复制代码
     A

    / \

   2   3

  /      \

B --- 1 ---C

(2)已经修了:

复制代码
A-B

B-C

现在:

A和C

其实已经能到达。


(3)如果:

再修

复制代码
A-C

会怎样?


(4)就出现:

环。

复制代码
  A

 / \

B---C

这条边:

完全浪费。


(5)所以:

第二条原则:

形成环的边不能修。


(6)于是:

Kruskal算法

终于出现了。


第六幕:Kruskal算法

整个算法只有四句话。


1、第一步

把所有边算出来。

例如:

5个点。

两两之间:

复制代码
1-2

1-3

1-4

...

4-5

全部记录。

本题就是:

复制代码
for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)

枚举所有点对。


2、第二步

距离超过L。

不能修。

直接不要。

复制代码
if(dx*dx+dy*dy>l*l)
    continue;

为什么不用:

复制代码
sqrt(...)

因为:

开方慢。

比较平方即可。


3、第三步

剩下边。

按照长度排序。

例如:

复制代码
1

2

3

5

8

越来越长。


4、第四步

从小到大加入。

如果:

不会形成环。

修。

否则:

跳过。

直到:

修了:

复制代码
n-1

条边。

结束。


5、Kruskal流程图

复制代码
所有边

↓

排序

↓

最短边

↓

形成环?

↓

否

↓

加入

↓

继续

↓

已经加入 n-1 条?

↓

结束

是不是很简单?


第七幕:怎样判断有没有形成环?

1、这是八级最重要的数据结构:

并查集(Union Find)


2、假设:

(1)开始:

复制代码
A

B

C

D

四个人。

互相不认识。


(2)编号:

复制代码
A

B

C

D

(3)现在修:

复制代码
A-B

于是:

A和B

成为一家人。


(4)再修:

复制代码
B-C

现在:

复制代码
ABC

是一家。


(5)最后:

如果:

修:

复制代码
A-C

会怎样?


(6)并查集一查:

发现:

他们已经是一家。

说明:

形成环。

不能修。


(7)这就是:

并查集最大的作用:

快速判断两个点是否已经连通。


第八幕:为什么最后要判断

复制代码
t==n-1

(1)生成树有一个性质:

n个点,一定只有n-1条边。

例如:

5个点。

一定:

复制代码
4

条边。


(2)否则:

要么:

不连通。

要么:

有环。


(3)因此:

最后:

如果:

复制代码
t<n-1

说明:

还有点没连上。

输出:

复制代码
Impossible

这就是题目的要求。


第九幕:完整算法流程

整道题其实就是下面这一张图。

复制代码
读入坐标

        │

        ▼

枚举所有点对

        │

        ▼

距离≤L?

        │

 否│继续

        ▼

加入边集

        │

        ▼

按照长度排序

        │

        ▼

依次枚举边

        │

        ▼

并查集判断

是否形成环?

        │

 是│跳过

        ▼

加入生成树

        │

        ▼

统计总代价

        │

        ▼

加入边==n-1?

        │

 是│输出答案

 否│Impossible

第十幕:为什么这题选择Kruskal,而不是Prim?

1、有的同学都会问:

Prim也能求最小生成树呀?

答案是:

当然能。


2、但是本题:

(1)先要判断:

复制代码
距离≤L

还要:

枚举所有点对。

天然就生成了一张:

边表(Edge List)


(2)而Kruskal最喜欢:

边表。

所以:

写起来,最自然。


第十一幕:参考程序:

1、官方参考程序:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;

// 最多500个点
const int N = 510;

// 最多边数
// 两两连边,最多约 N*N 条
const int E = N * N;

// n:基站数量
// l:允许修建线路的最大长度
int n, l;

// 每个基站的坐标
int x[N], y[N];

// p:保存边的编号
// u:边的起点
// v:边的终点
int p[E], u[E], v[E];

// 当前共有多少条边
int cnt;

// 并查集数组
// f[i]表示i的父节点
// 开始全部为0,表示自己就是集合代表
int f[N];

// t表示已经加入最小生成树的边数
int t = 0;

// d保存每条边的长度
double d[E];

// 最终答案(最小生成树总长度)
double ans = 0;

//////////////////////////////////////////////////////
// 并查集------寻找祖先(带路径压缩)
//////////////////////////////////////////////////////
int getf(int u)
{
    // 如果没有父亲
    // 自己就是祖先
    if (f[u] == 0)
        return u;

    // 路径压缩
    return f[u] = getf(f[u]);
}

//////////////////////////////////////////////////////
// 排序函数
// 按照边长从小到大排序
//////////////////////////////////////////////////////
bool cmp(int a, int b)
{
    return d[a] < d[b];
}

int main()
{
    //////////////////////////////////////////////////
    // 输入
    //////////////////////////////////////////////////

    cin >> n >> l;

    // 输入每个点坐标
    for (int i = 1; i <= n; i++)
        cin >> x[i] >> y[i];

    //////////////////////////////////////////////////
    // 枚举所有边
    //////////////////////////////////////////////////

    for (int i = 1; i <= n; i++)
    {
        for (int j = i + 1; j <= n; j++)
        {
            // 两点横坐标差
            int dx = x[i] - x[j];

            // 两点纵坐标差
            int dy = y[i] - y[j];

            // 如果距离超过L
            // 这条边不能修
            //
            // 注意:
            // 比较平方即可
            // 不需要sqrt()
            if (dx * dx + dy * dy > l * l)
                continue;

            // 新增一条边
            cnt++;

            // 第cnt条边
            p[cnt] = cnt;

            // 起点
            u[cnt] = i;

            // 终点
            v[cnt] = j;

            // 保存真正距离
            d[cnt] = sqrt(dx * dx + dy * dy);
        }
    }

    //////////////////////////////////////////////////
    // 所有边按长度排序
    //////////////////////////////////////////////////

    sort(p + 1, p + cnt + 1, cmp);

    //////////////////////////////////////////////////
    // Kruskal
    //////////////////////////////////////////////////

    for (int i = 1; i <= cnt; i++)
    {
        // 当前边编号
        int id = p[i];

        // 当前边两个端点
        int pu = u[id];
        int pv = v[id];

        //////////////////////////////////////////////////
        // 判断是否已经连通
        //////////////////////////////////////////////////

        if (getf(pu) == getf(pv))
            continue;

        //////////////////////////////////////////////////
        // 加入最小生成树
        //////////////////////////////////////////////////

        // 边数+1
        t++;

        // 累加长度
        ans += d[id];

        //////////////////////////////////////////////////
        // 合并两个集合
        //////////////////////////////////////////////////

        f[getf(pu)] = getf(pv);
    }

    //////////////////////////////////////////////////
    // 判断是否成功生成树
    //////////////////////////////////////////////////

    // n个点
    // 最小生成树必须有n-1条边

    if (t == n - 1)
        printf("%.2lf\n", ans);
    else
        printf("Impossible\n");

    return 0;
}

2、教学版

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <iomanip>
using namespace std;

const int MAXN = 505;

//=======================
// 一条边
//=======================
struct Edge
{
    int u;          // 起点
    int v;          // 终点
    double w;       // 长度
};

//=======================
// 所有边
//=======================
vector<Edge> edge;

//=======================
// 并查集
//=======================
int parent[MAXN];

//=======================
// 查找祖先(路径压缩)
//=======================
int Find(int x)
{
    if(parent[x]==x)
        return x;

    return parent[x]=Find(parent[x]);
}

//=======================
// 合并两个集合
//=======================
void Union(int x,int y)
{
    x=Find(x);
    y=Find(y);

    parent[x]=y;
}

//=======================
// 排序规则
//=======================
bool cmp(Edge a,Edge b)
{
    return a.w<b.w;
}

int main()
{
    int n,L;
    cin>>n>>L;

    vector<pair<int,int>> point(n+1);

    // 输入坐标
    for(int i=1;i<=n;i++)
    {
        cin>>point[i].first>>point[i].second;
        parent[i]=i;              // 初始化并查集
    }

    // 建边
    for(int i=1;i<=n;i++)
    {
        for(int j=i+1;j<=n;j++)
        {
            int dx=point[i].first-point[j].first;
            int dy=point[i].second-point[j].second;

            if(dx*dx+dy*dy>L*L)
                continue;

            Edge e;
            e.u=i;
            e.v=j;
            e.w=sqrt(dx*dx+dy*dy);

            edge.push_back(e);
        }
    }

    // 排序
    sort(edge.begin(),edge.end(),cmp);

    double ans=0;
    int cnt=0;

    // Kruskal
    for(auto e:edge)
    {
        if(Find(e.u)==Find(e.v))
            continue;

        Union(e.u,e.v);

        ans+=e.w;

        cnt++;

        // 已经形成生成树,可以提前结束
        if(cnt==n-1)
            break;
    }

    if(cnt==n-1)
        cout<<fixed<<setprecision(2)<<ans;
    else
        cout<<"Impossible";
}

第十二幕:程序详细解析步骤

1、我们先不要写程序

而是先画流程图。

这道题应该先画:

复制代码
             输入坐标
                 │
                 ▼
        枚举所有点之间距离
                 │
                 ▼
         超过L?
      是─────────┐
      │          │
      │跳过      │保留
      ▼          ▼
            加入边集
                 │
                 ▼
             所有边排序
                 │
                 ▼
            Kruskal算法
                 │
                 ▼
             输出答案

2、 第一件事情------设计一条边

(1)官方程序用了四个数组:

复制代码
u[]
v[]
d[]
p[]

对于初学者不太好理解。


(2)我们可以把它们合成一个结构体。

复制代码
struct Edge
{
    int u;          // 起点编号
    int v;          // 终点编号
    double w;       // 边长
};

是不是舒服多了?


(3)一条边就是:

复制代码
Edge

里面放:

起点

终点

长度

(4)例如:

复制代码
Edge

1

3

2.36

(5)就是:

复制代码
1 -------- 3

长度2.36

3、我们需要很多很多边

(1)当然不能只存一条。

于是:

复制代码
vector<Edge> edge;

(2)就是建立:

复制代码
边仓库

以后:

算出来一条边。

就放进去。


(3)例如:

复制代码
1------2

↓

push_back()

↓

仓库

(4)再来一条:

复制代码
2------5

↓

push_back()

↓

仓库

以后:

所有边都在这里。


4、怎样比较两条边?

(1)排序的时候。

sort不知道:

什么叫

"短"。


(2)所以我们告诉它。

复制代码
bool cmp(Edge a,Edge b)
{
    return a.w<b.w;
}

(3)意思就是:

谁短。

谁排前面。


(4)例如:

复制代码
4.3

2.1

8.5

1.6

排序以后:

复制代码
1.6

2.1

4.3

8.5

这就是Kruskal第一步。


5、 并查集重新写

(1)官方程序使用:

复制代码
f[]

我们改个名字。

复制代码
int parent[505];

是不是一下就知道:

复制代码
父亲

什么意思。


(2)初始化:

复制代码
1

2

3

4

5

每个人:

父亲都是自己。

复制代码
for(int i=1;i<=n;i++)
    parent[i]=i;

(3)画出来:

复制代码
1

↓

1

2

↓

2

......

谁也不认识谁。


6、Find函数

(1)这里也是我们教学的重点。

复制代码
int Find(int x)
{
    if(parent[x]==x)
        return x;

    return parent[x]=Find(parent[x]);
}

(2)为什么这样写?

举个例子。


(3)原来:

复制代码
1

↓

2

↓

5

(4)以后:

再找:

1。

程序:

一路找:

复制代码
1

↓

2

↓

5

找到:

5。


(5)然后:

顺便把:

复制代码
1

↓

5

以后:

不用绕路了。

这就叫:

路径压缩。


7、 Union函数

(1)官方写了一句:

复制代码
f[getf(x)]=getf(y);

有的同学看起来有点累。


(2)我们写成函数。

复制代码
void Union(int x,int y)
{
    x=Find(x);

    y=Find(y);

    parent[x]=y;
}

一下就清楚了。


(3)第一步:

找到两个家长。


(4)第二步:

让:

复制代码
x家

搬到:

复制代码
y家

结束。


8、 建边

(1)这里是整个程序最好理解的一部分。

复制代码
for

枚举:

所有点。

复制代码
①

②

③

④

(2)两两之间:

计算距离。

如果:

复制代码
≤L

说明:

可以修。

加入:

复制代码
edge

(3)代码写成:

复制代码
Edge e;

e.u=i;

e.v=j;

e.w=sqrt(...);

edge.push_back(e);

(4)是不是比:

复制代码
u[]

v[]

d[]

好理解?


9、 Kruskal真正开始

(1)排序:

复制代码
sort(edge.begin(),edge.end(),cmp);

(2)这一句。

就是:

复制代码
最短

↓

最长

(3)然后:

开始修路。

复制代码
for(auto e:edge)

意思:

一条一条拿出来。


(4)例如:

第一条:

复制代码
1------3

第二条:

复制代码
2------5

......


(5)然后判断:

复制代码
if(Find(e.u)==Find(e.v))

什么意思?

就是:

看看:

是不是已经认识。


(6)进行判断:

认识:

跳过。

不认识:

修路。

复制代码
Union()

ans+=e.w;

10、 判断是否提前结束


(1)提前结束条件:

复制代码
已经修了

n-1

条边。

(2)生成树永远:

复制代码
n个点

↓

n-1条边

这是数学性质。


(3)所以:

复制代码
if(cnt==n-1)

立即结束。

还能快一点。


第十三幕:这道题考察了哪些知识?

知识点 是否重点 本题作用
二维坐标距离 ⭐⭐⭐ 建立边
不开平方比较平方 ⭐⭐⭐⭐ 判断是否超过L
最小生成树(Kruskal) ⭐⭐⭐⭐⭐ 核心算法
并查集(Union-Find) ⭐⭐⭐⭐⭐ 判断是否形成环
排序(sort) ⭐⭐⭐⭐ 按边权从小到大处理

这是一道典型的图论综合题 。它把几何建图 + 边集构造 + 排序 + 并查集 + Kruskal 最小生成树完整串联了起来,是八级具有代表性的编程题之一。


最后,送给同学们一句 Kruskal 的口诀

先建边,再排序;

边最短,优先取;

并查集,判成环;

不是一家就合并;

修满 n-1 条边,最小生成树就完成!

只要记住这五句话,今后遇到绝大多数 Kruskal 最小生成树 的题目,都能迅速建立正确的解题思路。


相关推荐
胖大和尚1 小时前
Linux 内核工程师、HPC工程师、C++工程师
c++·kernel·hpc
chh5632 小时前
C++--list
开发语言·数据结构·c++·学习·算法·list
qq_416409132 小时前
性价比高的openclaw选哪家
c++
汉克老师3 小时前
GESP2026年6月认证C++八级( 第三部分编程题(2、堆石子))精讲
c++·排列组合·快速幂·费马小定理·gesp8级·逆元·组合数计算
信竞星球_少儿编程题库3 小时前
2026年全国青少年信息素养大赛算法应用主题赛(星火征途C++小学组复赛真题)
c++
盐焗鹌鹑蛋3 小时前
【C++】AVL树
c++
杜子不疼.4 小时前
【C++】继承—C++的秘密武器,get父类的智慧
开发语言·c++
ShineWinsu4 小时前
对于Linux:基于UDP实现简单聊天室功能
linux·c++·面试·udp·笔试·进程·简单聊天室
chase_my_dream4 小时前
2D-SLAM 真实数据处理与多传感器工程落地:时间同步、异常过滤、标定对齐和系统调试
c++·人工智能·2d-slam