最小生成树
树、生成树、最小生成树
树:一个无向连通图,不包含回路(连通图中不存在环,该无向连通图就是树
生成树:覆盖途中每一个顶点的树
最小生成树:有权网络中满足 各边权值 之和最小的支撑树
一个有N个点的图,边一定是大于等于N-1条的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。
应用:
要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
实现最小生成树的两种算法
都可以归结为贪心算法
一、prim (普里姆算法)——让一棵树长大
在图里选一个作为生成树的根节点,生成树最先收录了V1这个顶点,从V1能长出去的方位(V1的各条邻边)中选择一条最小的边,这条边将带来有一个节点V2,将V2收入生成树中,于是生成树由一个顶点长大到了两个顶点……
在 已经收录到生成树中的顶点们 延伸出去的边 中找到权值最小的边,直到所有的顶点收录进生成树
当然,不能仅看边的权值,还应判断 选择这条边加入这个顶点是否会给生成树带来回路
dist【v】顶点V到生成树的最小距离,起初选定s为根节点,各顶点的dist【i】初始值为该顶点到根节点s的距离
不需要真正定义一个树节点,把树构建起来,对每一个顶点,存储它父节点的编号,根节点父亲编号 -1
将顶点收录进最小生成树<=> 该顶点到生成树的距离变为0
结束条件:
1、所有顶点收录进生成树中
2、所有没收录的顶点dist都是无穷大,意味者该图 不是连通图
#include <iostream>
using namespace std;
const int MAX=500;
#define inf 0x3f3f3f
int map[MAX][MAX];//存储边的关系
int dist[MAX];//顶点到生成树的距离
int vis[MAX];
int n,ee,s;
int prim(int s){
// 初始化这棵生成树 一开始只有s这个节点
for(int i=1;i<=n;i++){//初始化各节点到生成树的距离
dist[i]=map[s][i];
}
vis[s]=1;
int sum=0;
for(int t=0;t<n-1;t++){//还有n-1个节点需要收录
int v=-1;
int minx=inf;
for(int i=1;i<=n;i++){//找距离生成树距离最小的节点
if(!vis[i]&&dist[i]<minx){
v=i;
minx=dist[i];
}
}
if(v==-1){//n-1个顶点没全部收录就 找不到和生成树有边的节点了
cout<<"该图不连通"<<endl;
return 0;
}
vis[v]=1;
sum+=dist[v];
// 顶点v收进生成树后dist[v]=0
for(int j=1;j<=n;j++){
if(!vis[j]&&dist[j]>map[v][j]){
dist[j]=map[v][j];
}
}
}
return sum;
}
int main(){
cin>>n>>ee>>s;
// 初始化map矩阵,一开始视作顶点间不存在边
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i!=j)map[i][j]=inf;
}
}
//添加边的信息
int x,y,w;
for(int i=1;i<=ee;i++){
cin>>x>>y>>w;
map[x][y]=w;//无向图
map[y][x]=w;
}
cout<<prim(s);
return 0;
}
例题来源
输入
4 5 1
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出
6
7-28 畅通工程之局部最小花费问题 (35 分)
已知局部联通,求最小生成树(可能局部联通,构成两个或多个不同的联通子集)
已知局部联通,求最小生成树,一开始直接用链式前向星那种优化版本,结果就是有一个测试点一直过不去。因为把给出的联通点全部当作收录进生成树的点(大错特错),可能给出的这些已经联通的顶点构成的是两个不同的联通子集,那哪个作为生成树的大本营呢?这时,对于各顶点的初始化不能直接根据初始是否修路,初始化为顶点到生成树之间的距离,只能老老实实得初始化为各顶点之间的距离,自选定一个顶点作为生成树,逐一将别的顶点收录进来
#include <iostream>
using namespace std;
const int maxn=105;
const int maxe=5004;
#define inf 0x3f3f3f
int map[maxn][maxn];//存储边的关系
int dist[maxn];//顶点到生成树的距离
int vis[maxn];
int n,ee;
int prim(){
// 初始化这棵生成树 一开始只有s这个节点
for(int i=1;i<=n;i++){//初始化各节点到生成树的距离
dist[i]=map[1][i];
}
vis[1]=1;
dist[1]=0;
int sum=0;
for(int t=0;t<n-1;t++){//还有n-1个节点需要收录
int v=-1;
int minx=inf;
for(int i=1;i<=n;i++){//找距离生成树距离最小的节点
if(!vis[i]&&dist[i]<minx){
v=i;
minx=dist[i];
}
}
if(v==-1){//n-1个顶点没全部收录就 找不到和生成树有边的节点了
cout<<"该图不连通"<<endl;
return 0;
}
vis[v]=1;
sum+=dist[v];
// 顶点v收进生成树后dist[v]=0
for(int j=1;j<=n;j++){
if(!vis[j]&&dist[j]>map[v][j]){
dist[j]=map[v][j];
}
}
}
return sum;
}
int main(){
cin>>n;
ee=n*(n-1)/2;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i!=j)map[i][j]=inf;
}
}
//添加边的信息
int x,y,w,f;
for(int i=1;i<=ee;i++){
cin>>x>>y>>w>>f;
if(f==0){
map[x][y]=w;//无向图
map[y][x]=w;
}
else map[x][y]=map[y][x]=0;
}
cout<<prim();
return 0;
}
以下未过的prim代码(一个测试点过不了)
#include <iostream>
#include <queue>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=105;
const int maxe=5005;
struct edge{
int to;
int next;
int w;
}e[maxe<<1];
int cnt=0;//边的编号,从1开始
int head[maxn];//以i为起点的边在e数组中的编号
void add(int u,int v,int w){
e[++cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
int n,m;
int dist[maxn];//每个顶点到生成树的距离
int vis[maxn];//顶点是否收录进生成树
//priority_queue< pair<int,int>,vector< pair<int,int> >,greater< pair<int,int> > > Q;
int prim(){
// if(Q.empty()){//至少保证收录了一个点,可能测试数据一条已修的路都没有
// dist[1]=0;
// Q.push(make_pair(0,1));
// }
int sum=0;
for(int t=0;t<n;t++){
//在未收录的顶点里 找离生成树最近的顶点
int v=0;
int minx=inf;
for(int i=1;i<=n;i++){//找距离生成树距离最小的节点
if(!vis[i]&&dist[i]<minx){
v=i;
minx=dist[i];
}
}
// if(Q.empty()){
// return sum;
// return 0;
// }
// pair<int,int> p;
// if(!Q.empty()){
// p=Q.top();
// Q.pop();
// int v=p.second;
// if(vis[v]){//跳过之后这次不能算进总次数,因为没收到顶点
// t--;//t代表收入的顶点数
// continue;
// }
vis[v]=1;
sum+=dist[v];
// cout<<v<<" "<<dist[v]<<endl;
for(int j=head[v];j;j=e[j].next){
int to=e[j].to;
int w=e[j].w;
if(!vis[to]&&dist[to]>w){
dist[to]=w;
// Q.push(make_pair(w,to));
}
}
}
return sum;
}
int main(){//最小生成树
cin>>n;
m=n*(n-1)/2;
for(int i=1;i<=n;i++){
dist[i]=inf;
vis[i]=0;
}
int u,v,w,f;
int flag=0;
while(m--){
cin>>u>>v>>w>>f;
add(u,v,w);
add(v,u,w);
if(f==1){
// vis[u]=vis[v]=1;
dist[u]=dist[v]=0;
flag=1;
// Q.push(make_pair(0,u));
// Q.push(make_pair(0,v));
}
}
if(!flag)dist[1]=0;
int res=prim();
cout<<res;//随便啦,以1为源点
return 0;
}
//1、可能初始没有道路,直接计算原图的最小生成树,至少初始化一个顶点dist为0
//2、给定了n*(n-1)/2这么多可选的边,一定可以搞出生成树,
//v不需要初始化为-1(第二条无甚影响)
//3、可能已有的道路已经联通了所有村庄,花费可能为0
kruskal做法
优先选修好路的边(某种意义上权值最小啊)
修好路的边不算近花费,计算边数照旧
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=1005;
const int maxe=3005;
typedef struct edge{
int u;
int v;
int w;
int flag;
edge(int x,int y,int z,int f):u(x),v(y),w(z),flag(f){}
bool operator<(const edge& ee)const{
if(flag==ee.flag)return w<ee.w;
else return flag>ee.flag;
}
}edge;
//}e[maxe];结构体里有构造函数,不能这样开个数组
// Kruskal算法无向图也不用开两倍空间啦
vector<edge> e;//sort(e.begin(),e.end() )
int fa[maxn];
int find(int x){//找x所在集合的编号
if(x==fa[x])return x;
return fa[x]=find(fa[x]);
}
int main(){//最小生成树
int n,m;
cin>>n;
m=n*(n-1)/2;
for(int i=0;i<=n;i++)fa[i]=i;
int x,y,z,f;
for(int i=0;i<m;i++){
cin>>x>>y>>z>>f;
e.push_back(edge(x,y,z,f));
}
// sort(e=1,e+n+1);
sort(e.begin(),e.end() ) ;
int cnt=0;
int sum=0;
for(int i=0;i<m;i++){//用了vector,push_back自然是从0开始的
if(cnt==n-1)break;//n个顶点收集n-1条边
int u=e[i].u;
int v=e[i].v;
int x=find(u);
int y=find(v);
if(x!=y){
if(!e[i].flag)sum+=e[i].w;
cnt++;
fa[x]=y;
}
}
cout<<sum;//一定会有结果
// if(cnt==n-1)cout<<sum;
// else cout<<"Impossible";
// set<int> se;//更严谨
// for(int i=1;i<=n;i++){
// se.insert(find(i));
// }
// if(se.size()!=1)cout<<"Impossible";
// else cout<<sum;
return 0;
}
链式前向星,最小堆优化注意点
1、链式前向星存储,遍历邻接结点时,明确 起点是v,终点是to,即将收录的顶点是v,不要把节点在e数组中存储的下标j搞混
2、用了 最小堆,遍历邻接结点并将dist【to】修改成立最小值后要即时插入到最小堆中进行排序
priority_queue实现机制是最小堆,堆顶元素Q.top()
队列queue的队头元素是Q.front();
3、有一处错误,/* lack---------*/处缺失了对出队节点是否被收录的判断
#include <iostream>
#include <queue>
using namespace std;
const int MAX=500;
#define inf 0x3f3f3f
struct edge{
int to;
int next;
int w;
}e[MAX];
int head[MAX];
int cnt;
void add(int u,int v,int w){
e[++cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
int dist[MAX];//顶点到生成树的距离
int vis[MAX];
int n,ee,s;
int prim(int s){
// 初始化这棵生成树 一开始只有s这个节点
for(int i=1;i<=n;i++){//初始化各节点到生成树的距离
dist[i]=inf;
vis[i]=0;
}
dist[s]=0;
int sum=0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater< pair<int,int> > >Q;//小顶堆
Q.push(make_pair(0,s));
for(int t=0;t<n;t++){
/*
int v=-1;
int minx=inf;
for(int i=1;i<=n;i++){//找距离生成树距离最小的节点
if(!vis[i]&&dist[i]<minx){
v=i;
minx=dist[i];
}
}
if(v==-1){//n-1个顶点没全部收录就 找不到和生成树有边的节点了
cout<<"该图不连通"<<endl;
return 0;
}
*/
if(Q.empty()){
cout<<"Impossible";
return 0;
}
pair<int,int> p;
p=Q.top();
Q.pop();
int v=p.second;
/*lack---------*/
vis[v]=1;
sum+=dist[v];
for(int j=head[v];j;j=e[j].next){//起点v,终点to
int to=e[j].to;
int w=e[j].w;
if(!vis[to]&&dist[to]>w){//顶点v收进生成树后dist[v]=0
dist[to]=w;
Q.push(make_pair(w,to));
}
}
}
return sum;
}
int main(){
cin>>n>>ee>>s;
//添加边的信息
int x,y,w;
for(int i=1;i<=ee;i++){
cin>>x>>y>>w;
add(x,y,w);//无向图
add(y,x,w);
}
cout<<prim(s);
return 0;
}
有一处错误,/* lack---------*/处缺失了对出队节点是否被收录的判断
例题
6——3
5——3
收录6,5这两个点时都将3这个顶点放进了队列中,以至于其中3——5——1这条边收录后(假设这条边先收录),vis[3]标记为1,但3——6——1出队时,如果根本不考虑vis值鲁莽收录就会造成一个点重复收录两次的错误
#include <iostream>
#include <queue>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=1005;
const int maxe=3005;
struct edge{
int to;
int next;
int w;
}e[maxe<<1];
int cnt=0;//边的编号,从1开始
int head[maxn];//以i为起点的边在e数组中的编号
void add(int u,int v,int w){
e[++cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
int n,m;
int dist[maxn];//每个顶点到生成树的距离
int vis[maxn];//顶点是否收录进生成树
int prim(int s){
for(int i=1;i<=n;i++){
dist[i]=inf;
vis[i]=0;
}
dist[s]=0;
int sum=0;
priority_queue< pair<int,int>,vector< pair<int,int> >,greater< pair<int,int> > > Q;
Q.push(make_pair(0,s));
for(int t=0;t<n;t++){
//在未收录的顶点里 找离生成树最近的顶点
// int minx=inf;
// int v=-1;
// for(int i=1;i<=n;i++){
// if(!vis[i]&&dist[i]<minx){
// minx=dist[i];
// v=i;
// }
// }
// if(v==-1){
// cout<<"Impossible";
// return 0;
// }
//----------------------------------
// while(vis[Q.top().second]){//这种方式会超时
// Q.pop();
// }
if(Q.empty()){
cout<<"Impossible";
return 0;
}
pair<int,int> p;
p=Q.top();
Q.pop();
int v=p.second;
if(vis[v]){//跳过之后这次不能算进总次数,因为没收到顶点
t--;//t代表收入的顶点数
continue;
}
vis[v]=1;
sum+=dist[v];
// cout<<v<<" "<<dist[v]<<endl;
for(int j=head[v];j;j=e[j].next){
int to=e[j].to;
int w=e[j].w;
if(!vis[to]&&dist[to]>w){
dist[to]=w;
Q.push(make_pair(w,to));
}
}
}
return sum;
}
int main(){//最小生成树
cin>>n>>m;
int u,v,w;
while(m--){
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
int res=prim(1);
if(res)cout<<res;//随便啦,以1为源点
return 0;
}
kruskal做法(当模板好啦)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=1005;
const int maxe=3005;
typedef struct edge{
int u;
int v;
int w;
edge(int x,int y,int z):u(x),v(y),w(z){}
bool operator<(const edge& ee)const{
return w<ee.w;
}
}edge;
//}e[maxe];结构体里有构造函数,不能这样开个数组
// Kruskal算法无向图也不用开两倍空间啦
vector<edge> e;//sort(e.begin(),e.end() )
int fa[maxn];
int find(int x){//找x所在集合的编号
if(x==fa[x])return x;
return fa[x]=find(fa[x]);
}
int main(){//最小生成树
int n,m;
cin>>n>>m;
for(int i=0;i<=n;i++)fa[i]=i;
int x,y,z;
for(int i=0;i<m;i++){
cin>>x>>y>>z;
e.push_back(edge(x,y,z));
}
// sort(e=1,e+n+1);
sort(e.begin(),e.end() ) ;
int cnt=0;
int sum=0;
for(int i=0;i<m;i++){//用了vector,push_back自然是从0开始的
if(cnt==n-1)break;//n个顶点收集n-1条边
int u=e[i].u;
int v=e[i].v;
int x=find(u);
int y=find(v);
if(x!=y){
sum+=e[i].w;
cnt++;
fa[x]=y;
}
}
// if(cnt==n-1)cout<<sum;
// else cout<<"Impossible";
set<int> se;//更严谨
for(int i=1;i<=n;i++){
se.insert(find(i));
}
if(se.size()!=1)cout<<"Impossible";
else cout<<sum;
return 0;
}
二、kruskal算法 ——合木成林
认为在初始状况下每个顶点都是一棵树,不断收入权值最小的边,将两棵树并成一颗,最后将所有节点并成一棵树
收录的是边,不断搜寻权重最小的边(最小堆),该条边不能给生成树带来回路,并在原图中将该条边删除
结束条件:收录的边 < n-1&&原图中还有边
判断是否生成回路:
每个顶点都是一个独立的集合, 收录一条边,把两棵树并在一起相当于把两个集合并成一个,收录进新的边时,该边两头的顶点 u , v ,如果位于不同的集合,则不会产生回路。
#include <iostream>
#include<algorithm>
using namespace std;
const int MAX=500;
#define inf 0x3f3f3f
struct edge{
int from;
int to;
int w;
bool operator<(const edge& n)const{
return w<n.w;
}
}e[MAX];
int n,ee,s;
int fa[MAX];
int find(int x){//寻找x所在树的根节点,即x节点所在的集合编号
// while(x!=fa[x]){
// x=fa[x];
// }
// return x; 都可以
if(x==fa[x])return x;
return fa[x]=find(fa[x]);
}
int merge(int x,int y){//判断xy俩节点是否处在同一集合
int a=find(x);
int b=find(y);
if(a==b)return 1;
else{
//fa[x]=b;//不是简答把x节点加入y节点,也许x节点所在的集合有10个节点,y节点所在集合只有y节点,如果输入给出的边是y x还好,把y节点加入x所在的集合,
//如果输入给出的边是x y,就把x节点从原来所在集合删除了加入了y集合,这不胡闹吗,何况y集合也可能不止一个节点
fa[a]=b;//x,y不在同一结合,将y节点所在的集合中所有节点加入x所在的集合
return 0;
}
}
int main(){
cin>>n>>ee>>s;
//添加边的信息
int x,y,w;
for(int i=1;i<=ee;i++){
// cin>>x>>y>>w;
// e[i].from=x;
// e[i].to=y;
// e[i].w=w;
cin>>e[i].from>>e[i].to>>e[i].w;
}
// cout<<kruskal();
sort(e+1,e+n+1);//将各边按权值升序排列
for(int i=1;i<=n;i++){
fa[i]=i;//每个顶点视作一个集合(连通分量的编号)一开始各不相同
}//在中输入父子关系未说明根节点时也可以这样找根节点
// int r=n;//见树形DP那篇
// while(fa[r]!=r){
// r=fa[r];
// }
int cnt=0;
int sum=0;
for(int i=1;i<=ee;i++){//找到最小的一条边
int u=e[i].from;
int v=e[i].to;
//这条边的起点、终点,链式前向星本来不存起点的,邻接表嘛
// get一个节点点根据起点遍历邻接结点
// 除了不能直接用起点终点定位以外,前向星几乎是完美的
// 前向星用数组下标当作指针,一对e数组排序,数组下标全乱了
// if(merge(u,v))continue;
if(!merge(u,v)){//这条边的两个顶点不在同一集合
cnt++;//收了几条边
sum+=e[i].w;
}
if(cnt==n-1)break;//n个顶点的生成树要收录n-1条边
}
if(cnt==n-1)cout<<sum;
else cout<<"No!";
return 0;
}