图论筑基——最小生成树及单源/全源最短路径的贪心本质
一、引入
给定\(n\)个点,\(m\)条边的无向图,求最小生成树使得树的边之和最小。
给定\(n\)个点,\(m\)条边的有向非负图,求从\(s\)点出发到任意点的最短距离。
这两问分别对应 最小生成树 和 单源最短路径。其中含有类似的思维和代码结构,这也是本篇博客试图将它们放在一起理解记忆的原因。
二、核心思想
两者解法的共同结构在于:
维护一个“已经正确”的点集\(S\),每一步从边界上选择一个当前最优的对象加入\(S\),并证明这个选择是安全的,于是\(S\)不断扩张直到覆盖全部点。
这是一种很典型的贪心扩张法。这类算法将点集分为两部分:
- \(S\):已经确定的不会再更改的集合
- \(T\):还没有确定的点
过程可以概括为:
- 先给定一个很小的初始正确集合\(S\)
- 定义每个外部点相对于\(S\)的代价
- 每次从外部选取一个当前最优的点加入\(S\)
- 由新加入的点重新更新\(S\),并更新剩余外部点的代价
- 重复直到结束。
它们实质有着贪心的本质:
从一个局部正确的解出发,将局部正确逐步扩展为全局正确
三、证明
下面尝试分别证明这种思想在最小生成树 \(prim\) 和单源最短路径 \(dijkstra\) 的正确性。
\(prim\)
表述
在 \(prim\) 中:
- \(S\):已经并入当前生成树的点集
- \(dis[v]\):由 \(S\) 连接到 \(v\) 所需的最短路径
每一次扩展 \(S\) :
证明
为什么下一步扩展是安全的?
当前 \(S\) 和外部点 \(T\) 构成一个割 \(S|T\),\(prim\) 选择的是跨过这个割的最小边 \(e\) 。
假设某颗最小生成树 \(X\) 不包含 \(e\),因为 \(X\) 联通了 \(S\) 和 \(T\),所以必然包含另一条跨越这个割的边 \(e'\)。
又因为 \(e\) 是割上的最小边,所以必然有:
把 \(X\) 中的 \(e\) 换成 \(e'\),图仍然联通,且边之和不会变大。
于是得到了一颗更优,且包含 \(e\) 的最小生成树。所以 \(e\) 是可取的。
\(dijkstra\)
表述
在 \(dijkstra\) 中:
- \(S\):已经确定从源点出发最短路的点集
- \(dis[v]\):当前从源点经过 \(S\) 到达 \(v\) 的最短距离估计
每一次扩展 \(S\):
即在所有尚未加入 \(S\) 的点中,选择当前 \(dis\) 最小的点 \(u\) 加入 \(S\)。
证明
设当前选出的点为 \(u\),我们要证明:\(dis[u]\) 已经等于从源点到 \(u\) 的真实最短路长度。
还是考虑反证法。假设不是这样,即存在一条从源点到 \(u\) 的更短路径 \(p\)。
那我们可以考虑这条路径 \(p\) 从 \(S\) 内部到 \(S\) 外部的那条边。设这条边为 \((x,y)\),其中:
- \(x \in S\)
- \(y \notin S\)
由于 \(x\) 已经在正确的点集 \(S\) 里了,根据假设,\(dis[x]\) 已经是真实的最短路长度。
而在添加 \(x\) 时,算法一定尝试过用边 \((x,y)\) 更新 \(dis[y]\),但是由于没有最终采用 \((x,y)\) 这条边,因此一定有:
而后者正是我们假设的更短路径 \(p\) 从\(x\) 到 \(y\) 的路径。
到达 \(u\) 包含 \(x\) 到 \(y\) 的路径,所以 \(p\) 是一定大于 \(dis[u]\) 的,这与 \(p\) 是更短的真实最短路径矛盾。
因此假设不成立,\(dis[u]\) 必然已经是从源点到 \(u\) 的最短路长度。
所以扩张的过程中把 \(u\) 加入 \(S\) 是安全的。
四、实现
边的存储
用vector
struct Node{
int v,d;
};
vector <Node> Q[MAXN];
Q[u].push_back((Node){v,w});
Q[v].push_back((Node){u,w});
\(prim\)
每一次取出跨越正确集合 \(S\) 和剩余点集 \(T\) 的最短边,并根据最短边 \(T\) 一侧的点 \(v\) 重新更新 新的点集 \(S\) 到剩余点的最短距离。
inline ll prim(){
memset(dis,INF,sizeof(dis));
pq.push((Node){1,0});
dis[1]=0;//初始化,随机以1为已知的正确集合S
ll ans = 0;
int cnt = 0;
while(!pq.empty()){
auto [u,d] = pq.top();pq.pop();
if(mem[u]) continue;//确认u是 S 之外的剩余点集 T
mem[u] = true;
ans += d;cnt++;
for(Node p : Q[u]){//遍历点u,更新新的dis数组
int v = p.v,w = p.d;
if(!mem[v] && dis[v]>w){
pq.push((Node){v,w});
dis[v] = w;
}
}
}
if(cnt<n) return -1;//没有成功联通n个点
return ans;
}
\(dijkstra\)
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int w=1,q=0;char ch=' ';
while(ch!='-' && ch<'0' || ch>'9') ch=getchar();
if(ch=='-') w=-1,ch=getchar();
while(ch<='9' && ch>='0') q=q*10+ch-'0',ch=getchar();
return w*q;
}
const int MAXN = 2e5+25;
int n,m,s,cnt,head[MAXN];
struct Edge{
int nxt,to,w;
}E[MAXN<<2];
inline void addedge(int u,int v,int w){
E[++cnt].to=v,E[cnt].w=w;
E[cnt].nxt=head[u],head[u]=cnt;return;
}
struct Node{
int u,d;
friend bool operator < (Node a,Node b){
return a.d>b.d;
}
};
priority_queue <Node> Q;
int dis[MAXN];bool vis[MAXN];
inline void dijkstra(int s){
memset(dis,0x3f3f3f3f,sizeof(dis));
memset(vis,false,sizeof(vis));
Q.push((Node){s,0});dis[s]=0;
while(!Q.empty()){
Node temp=Q.top();Q.pop();
int u=temp.u;
if(vis[u]){continue;}vis[u]=true;
for(int i=head[u]; i; i=E[i].nxt){
int v=E[i].to,w=E[i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if(!vis[v]) Q.push((Node){v,dis[v]});
}
}
}
return;
}
signed main(){
n=read(),m=read(),s=read();
for(int i=1; i<=m; i++){
int u=read(),v=read(),w=read();
addedge(u,v,w);
}
dijkstra(s);
for(int i=1; i<=n; i++){
printf("%d ",dis[i]);
}
}

浙公网安备 33010602011771号