文章首发于:My Blog 欢迎大佬们前来逛逛
概念
最小生成树的定义 在一张带权无向图中,最小生成树是一棵生成树,它的边权值之和最小。
生成树是一颗包含原图中所有顶点的树,它的边集合是原图的一个子集,且任意两个顶点之间都有且仅有一条简单路径。
最小生成树的算法目前,最常用的两种最小生成树算法是Kruskal算法和Prim算法。
Kruskal算法
Kruskal算法 Kruskal算法的主要思想是按照边权值从小到大依次选择边,如果这条边的两个端点不在同一个连通块中,就把这条边加入到最小生成树的边集合中。 具体实现步骤:
(1)将所有边按照边权值从小到大排序。
(2)从小到大依次选择每一条边,如果这条边的两个端点不在同一个连通块中,就把这条边加入到最小生成树的边集合中,并合并这两个连通块。
(3)重复步骤(2),直到最小生成树中有n-1条边为止。 Kruskal算法的时间复杂度为O(mlogm),其中m为图中的边数。
Kruskal算法存储每一条边(不是双向边),并且还需要对于每个节点的辅助数组,因此对于边和点的数据类型的范围应该这样定义:
//5000个点, 200000条边
const int N=5e3+10,M=2e5+10;
struct Edge{
int a,b,w;
}edge[M]; //边集合
int head[N],cnt; //head点集合
Kruskal算法适合于稀疏图
int parent[N];
//并查集初始化
void init(){
for (int i=1;i<=n;i++){
parent[i]=i;
}
}
//查找连通块:并查集的查找+路径压缩
void find_set(int x){
if (x!=parent[x]){
parent[x]=find_set(parent[x]);
}
return parent[x];
}
void kruskal(){
//将所有的边按照权值从小到达排序
comp(edge+1,edge+1+m,comp); //注意此处为m边数,不是顶点数
init();
int cnt=0;//记录边的数量
int ans=0;//记录最后的最小生成树的权值之和
for (int i=1;i<=m;i++){
//获得边与边权
int a=edge[i].a,b=edge[i].b=edge[i].w;
//查找某一条边的对应的两个顶点
int pa=find_set(a),pb=find_set(b);
//如果这两个顶点不属于同一个集合中,则我们直接把他们合并即可。
if (pa!=pb){
//则合并到同一连通块
parent[pa]=pb;
//ans加上此边权
ans+=w;
//遍历的边数+1
cnt++;
}
//直到最后遍历到了最后一个顶点
if (cnt==n-1) break;
}
return ans;
}
证明:如果某个连通图属于最小生成树,那么所有从外部连接到该连通图的边中的一条最短的边必然属于最小生成树。所以不难发现,当最小生成树被拆分成彼此独立的若干个连通分量的时候,所有能够连接任意两个连通分量的边中的一条最短边必然属于最小生成树
Prim算法
Prim算法 Prim算法的主要思想是从任意一个顶点开始,每次选择一个与当前连通块相邻的权值最小的边,并将其加入到最小生成树的边集合中,直到所有顶点都被加入到最小生成树中。 具体实现步骤:
(1)随机选择一个顶点作为起始点。
(2)将与起始点相邻的边按照边权从小到大排序,并选择权值最小的边加入到最小生成树的边集合中。
(3)将新加入的点加入到连通块中,并将与它相邻的边按照边权从小到大排序。
(4)重复步骤(2)和(3),直到所有顶点都被加入到最小生成树中。
Prim算法的时间复杂度为O(n^2),其中n为图中的顶点数。但是,如果使用堆优化的Prim算法,时间复杂度可以优化到O(mlogn)。
Prim算法存储每一条边,而且是双向边,使用链式前向星存图的边集合和点集合的数据范围应该如下定义:
//5000个点, 200000条边
const int N=5e3+10,M=2e5+10;
struct Edge{
int a,b,w;
}edge[M<<1]; //边集合,需要开双倍空间
int head[N],cnt; //head点集合
/********************/
//如果不给你边数,只给你点数,则:N=2000
//则需要边数edge的范围就会达到:2000*1999/2 = 199万
const int N=2000,M=2e6+10
edge[M<<1]; //开双倍,防止
int head[N];
Prim算法适合于稠密图
//链式前向星存图
struct Edge {
int to, w, next;
}edge[N];
int head[N], cnt;
int n, m, ans;
//Prim算法需要的两个数组
int dis[N];
bool vis[N];
//加边
void add_edge(int u, int v, int w) {
edge[++cnt].next = head[u];
edge[cnt].to = v;
edge[cnt].w = w;
head[u] = cnt;
}
void Prim(){
//默认从1开始,则从2起,我们需要把dis设置为极大值
for (int i=2;i<=n;i++){
dis[i]=INF;
}
//首先从1开始,设置与1相连的所有边的权值
for (int i=head[1];i;i=edge[i].next){
int v=edge[i].to;
dis[v]=min(dis[v],edge[i].w);//可能存在重边
}
int now=1;//当前点为1
//注意:i少遍历一次,因为1省略了
for (int i=1;i<n;i++){
vis[now]=true;
int minF=INF;
//遍历所有的点,找到未遍历过的,并且边权值最小的那一条边
for (int j=1;j<=n;j++){
if (!vis[j] && dis[j]<minF){
minF=dis[j]; //这一条边的权值
now=j; //当前这条边的一个顶点
}
}
ans+=minF; //边权相加
//遍历所有与now相连的边
for (int j=head[now];j;j=edge[j].next){
int v=edge[j].to; //边的另一个顶点
if (!vis[v] && dis[v]>edge[j].w){
//更新dis数组中这边
dis[v]=edge[j].w;
}
}
}
for (int i=1;i<=n;i++){
if (dis[i]==INF){
cout<<"improssible\n";
return;
}
}
cout<<ans;
}
最小生成树的应用 最小生成树广泛应用于网络设计、电路设计、城市规划等领域。
例如,在网络设计中,最小生成树可以用来寻找最短路径,从而优化网络性能;
在城市规划中,最小生成树可以用于道路和管道的布局,从而降低建设成本。
3728. 城市通电
题目要求:由n个城市,每两个城市之间可以选择连电线或者建电站,规定要使得所有的城市都必须通电。城市通电的要求是这个城市建有电站或者与其他建有电站的城市连接。并且规定每个城市建电站和建电线费用,求得最小的使得全部城市通电的总费用。
我们很容易看出这是一个最小生成树的问题,貌似只需要求出任意两点之间的边的边权,我们直接用Prim或者Kruskal不就结束了吗。
但是我们这样直接做是基于图中只存在边权,即只有建电线;但是这道题还规定了我们可以不用建电线,还可以在自己所在的位置建电站。
如果三个点信息是这样的:
电站代价:1 1 2;电线代价:5 5 8
则我们直接求最小生成树一定是错误的,这样的话答案为 13 ,但是很明显直接在每个点上建电站不就好了,这样的话答案为 4.
如果解决这种问题呢? 使用超级源点即可。
我们规定一个不存在的点连接连接每一个城市,并且规定所连的边的权值为每个对应的城市建电站的代价。
则我们用图来表示这样的关系:
我们既然求绿色的最小生成树不可行,则建立虚拟源点后,直接求对于整个的图的最小生成树,这样就解决了我们刚才的问题。
即如果刚才每个城市都建立电站,则对与整个图来说,最小生成树就是每个点的建电站的代价和(因为超级源点到每个点的边权规定为建电站的代价)
注意:对于原图中每两个点之间的权值需要我们单独求出。
然后直接对这整个图进行一次最小生成树即可
注意Prim算法的建边所需要的空间
Prim
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
typedef pair<int, int> PII;
const int N = 2e3+10,M=3e6+10,INF=9999999999;
//建边
struct Edge{
int to,next;
int w;
}edge[M<<1];//边集合
int head[N],cnt;
//main
int n;
PII pos[N];
int c[N],k[N];
vector<int> vec1;
vector<PII> vec2;
//Prim算法
bool vis[N];
int fa[N],dis[N];
//---------------------------------------
void add_edge(int u,int v,int w){
edge[++cnt].next=head[u];
edge[cnt].to=v;
edge[cnt].w=w;
head[u]=cnt;
}
int get_dis(int x,int y){
int dx=abs(pos[x].first-pos[y].first);
int dy=abs(pos[x].second-pos[y].second);
return (int)(dx+dy)*(k[x]+k[y]);
}
int Prim(){
for (int i=0;i<=n;i++){
dis[i]=INF;
}
//建立超级源点:0连接到每一个点
for (int i = 1; i <= n; i ++ ){
add_edge(i,0,c[i]);
add_edge(0,i,c[i]);
}
//设置初始边权值:遍历与超级源点相连的每一个边,计算出初始边权值
for (int i = head[0]; i ; i = edge[i].next ){
int v=edge[i].to;
dis[v]=min(dis[v],edge[i].w);
}
int now=0;//当前处理的点
int ans=0;//答案
//遍历到n-1
for (int i=0;i<n;i++){
vis[now]=true;
int minF=INF;
//遍历所有的没有被访问过的点,找出最小权值的边
for (int j = 1; j <= n; j ++ ){
if (!vis[j] && dis[j]<minF){
minF=dis[j];
now=j;
}
}
ans+=minF;
if (!fa[now]){
//如果fa[now]==0,则意味着这个点的父节点是超级源点,则在图中就表示为他与超级源点相连,只能建电站
vec1.push_back(now);
}
else{
//如果不为0,则意味着这个点与原图中的某个点相连,则它与这个点之间建电线
vec2.push_back({now,fa[now]});
}
//更新dis数组,遍历所有的now的相连的边
for (int j=head[now];j;j=edge[j].next){
int v=edge[j].to;
if (!vis[v] && dis[v] > edge[j].w){
dis[v] = edge[j].w;
fa[v]=now;
}
}
}
return ans;
}
signed main(){
cin>>n;
for (int i = 1; i <= n; i ++ ){
cin>>pos[i].first>>pos[i].second;
}
for (int i = 1; i <= n; i ++ ){
cin>>c[i];
}
for (int i = 1; i <= n; i ++ ){
cin>>k[i];
}
for (int i=1;i<=n;i++){
for (int j=1;j<i;j++){
int w=get_dis(i,j);
add_edge(i,j,w);
add_edge(j,i,w);
}
}
cout<<Prim()<<endl;
cout<<vec1.size()<<endl;
for (auto &x:vec1) cout<<x<<' ';
cout<<endl<<vec2.size()<<endl;
for (auto &x:vec2) cout<<x.first<<' '<<x.second<<endl;
return 0;
}
Kruskal
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
const int N = 2e3+10;
struct Edge{
int a,b,c;
bool operator<(const Edge& p){
return c<p.c;
}
}edge[N*N];
int n,m;
typedef pair<int, int> PII;
PII pos[N];
int c[N],k[N];
int e;//总边数
int parent[N];
bool vis[N*N];
int get_dis(int p1,int p2){
int dx=abs(pos[p1].first-pos[p2].first);
int dy=abs(pos[p1].second-pos[p2].second);
return (int)(dx+dy)*(k[p1]+k[p2]);
}
int find(int x) // 并查集
{
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
int kruskal(){
//超级源点连边
for (int i = 1; i <= n; i ++ ){
edge[++e]={i,0,c[i]};
}
//初始化并查集
for (int i=1;i<=n;i++){
parent[i]=i;
}
//排序边集合
sort(edge+1,edge+1+e);
vis[0]=true;
int ans=0;
for (int i=1;i<=e;i++){
int a=edge[i].a,b=edge[i].b,c=edge[i].c;
int pa=find(a),pb=find(b);
if (pa!=pb){
parent[pa]=pb;
ans+=c;
vis[i]=true;
}
}
return ans;
}
signed main()
{
cin>>n;
for (int i = 1; i <= n; i ++ ){
cin>>pos[i].first>>pos[i].second;
}
for (int i = 1; i <= n; i ++ ){
//建电站
cin>>c[i];
}
for (int i = 1; i <= n; i ++ ){
//建电线
cin>>k[i];
}
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j < i; j ++ ){
int w=get_dis(i,j);
edge[++e]={i,j,w};
}
}
vector<int> vec1;
vector<PII> vec2;
cout<<kruskal()<<endl;
for (int i=1;i<=e;i++){
if (vis[i]){
if (edge[i].b==0){
vec1.push_back(edge[i].a);
}
else{
vec2.push_back({edge[i].a,edge[i].b});
}
}
}
cout<<vec1.size()<<endl;
for (auto& x:vec1) cout<<x<<' ';
cout<<endl<<vec2.size()<<endl;
for (auto& x:vec2) cout<<x.first<<" "<<x.second<<'\n';
return 0;
}