赞
踩
树之
(一).了解
自由树就是一个无回路的连通图(没有确定根)(在自由树中选定一顶点做根,则成为一棵通常的树)。从根开始,为每个顶点(在树中通常称作结点)的孩子规定从左到右的次序,则它就成为一棵有序树。在图的应用中,我们常常需要求给定图的一个子图,使该子图是一棵树。
(①).生成树:
在图论中,如果连通图 的一个子图是一棵包含 的所有顶点的树,则该子图称为G的生成树(SpanningTree)。
生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得
到不同的生成树。
通用定义:
若从图的某顶点出发,可以系统地访问到图中所有顶点,则遍历时经过的边和图的所有顶点所构成的子图,称作
该图的生成树。
(1)若G是强连通的有向图,则从其中任一顶点v出发,都可以访问遍G中的所有顶点,从而得到以v为根的生成树。
(2)若G是有根的有向图,设根为v,则从根v出发可以完成对G的遍历,得到G的以v为根的生成树。
(3)若G是非连通的无向图,则要若干次从外部调用DFS(或BFS)算法,才能完成对G的遍历。每一次外部调用,
只能访问到G的一个连通分量的顶点集,这些顶点和遍历时所经过的边构成了该连通分量的一棵DFS(或BPS)生成树。
G的各个连通分量的DFS(或BFS)生成树组成了G的DFS(或BFS)生成森林。
(4)若G是非强连通的有向图,且源点又不是有向图的根,则遍历时一般也只能得到该有向图的生成森林。
(②).DFS生成树和BFS生成树
1)生成树的求解方法
设图 是一个具有n个顶点的连通图。则从G的任一顶点(源点)出发,作一次深度优先搜索
(广度优先搜索),搜索到的n个顶点和搜索过程中从一个已访问过的顶点 搜索到一个未曾访问过的邻接点 ,
所经过的边 (共n-1条)组成的极小连通子图就是生成树。(源点是生成树的根)通常,由深度优先搜索得到
的生成树称为深度优先生成树,简称为DFS生成树;由广度优先搜索得到的生成树称为广度优先生成树,简称为
BFS生成树。
概念
对于连通的带权图(连通网)G,其生成树也是带权的。生成树T各边的权值总和称为该树的权,记作:
其中,TE表示T的边集,w(u,v)表示边(u,v)的权。权最小的生成树称为G的最小生成树(Minimum SpannirngTree)。
最小生成树可简记为MST。
今天着重讲的是最小生成树!!!
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的
最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。
(二).算法
求最小生成树的算法
(1) 克鲁斯卡尔算法
图的存贮结构采用边集数组,且权值相等的边在数组中排列次序可以是任意的.该方法对于边相对比较多的不是很
实用,浪费时间.
(2) 普里姆算法
图的存贮结构采用邻接矩阵.此方法是按各个顶点连通的步骤进行,需要用一个顶点集合,开始为空集,以后将以连通
的顶点陆续加入到集合中,全部顶点加入集合后就得到所需的最小生成树 .
(①).Kruskal算法
1.Kruskal是一个计算最小生成树的算法,其算法原理如下。首先,将每个顶点放入其自身的数据集合中。然后,按照
权值的升序来选择边。当选择每条边时,判断定义边的顶点是否在不同的数据集中。如果是,将此边插入最小生成
树的集合中,同时,将集合中包含每个顶点的联合体取出,如果不是,就移动到下一条边。重复这个过程直到所有
的边都探查过。
1 初始情况,一个联通图,定义针对边的数据结构,包括起点,终点,边长度:
struct _node{
int
val;
//长度
int
start;
//边的起点
int
end;
//边的终点
}Node;
3 继续找到第二短的边,将c
, d
再放入同一个集合里:
4 继续找,找到第三短的边ab
,因为a
,e
已经在一个集合里,再将b
加入:
5 继续找,找到b
,e
,因为b
,e
已经同属于一个集合,连起来的话就形成环了,所以边be
不加入最小生成树:
6 再找,找到bc
,因为c
,d
是一个集合的,a
,b
,e
是一个集合,所以再合并这两个集合:
这样所有的点都归到一个集合里,生成了最小生成树。
2.先写一下一个基本的能够形成最小生成树能够找到最短路径的Kruskal算法代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 7
using namespace std;
struct Node
{
int start;
int endd;
int leng;
}V[N];
int edge[N][3]={{ 0, 1, 3 },
{ 0, 4, 1 },
{ 1, 2, 5 },
{ 1, 4, 4 },
{ 2, 3, 2 },
{ 2, 4, 6 },
{ 3, 4, 7}
};
int father[N]={0,};//记录每个点的父结点(属于哪个集合)
int dis[N]={0,};//记录一个集合的长度
int cmp(const void *a,const void *b)//排序用到的cmp()函数
{
return (*(Node*)a).leng-(*(Node*)b).leng; //此为升序的方式
}
int find_f(int x)//找寻父结点
{
if(x!=father[x])
father[x]=find_f(father[x]);
return father[x];
}
void Merge(int a,int b)//合并两个集合
{
int x=find_f(a);
int y=find_f(b);
if(x==y)
return ;
if(dis[x]<dis[y])
father[x]=find_f(y);
else
{
if(dis[x]==dis[y])
dis[x]+=dis[y];
father[y]=find_f(x);
}
}
int Kruskal()
{
int counts=0;//记录总的路程;
for(int i=0;i<N;i++)//初始化集合,让所有的点都各成一个集合,每个集合都只包含自己
{
father[i]=i;//记录每个点的父结点
dis[i]=1;//记录每个边长度
}
for(int i=0;i<N;i++)
{
if(find_f(V[i].start)!=find_f(V[i].endd))
{
Merge(V[i].start,V[i].endd);
counts+=V[i].leng;
}
}
return counts;
}
int main()
{
for(int i=0;i<N;i++)//将存储的数据赋值给结构体
{
V[i].start=edge[i][0];
V[i].endd=edge[i][1];
V[i].leng=edge[i][2];
}
qsort(V,N,sizeof(V[0]),cmp);//用qsort()函数先进行升序排序
printf("%d",Kruskal());
return 0;
}
3.例题
①.https://vjudge.net/contest/173215#problem/E
题意:
Kruskal算法代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define N 2001
using namespace std;
char maps[N][10];
int dis[N];
int m,n;
int father[N];
struct Node//建立一个能够表达成一条边的结构体
{
int start;
int endd;
int leng;
}node[N*N/2];
//sort函数的排序函数
int cmp(Node a,Node b)
{
return a.leng<b.leng;
}
//通过判断不同字母的个数形成这条边的长度
int Get_leng(int x,int y)
{
int co=0;
for(int i=0;i<7;i++)
if(maps[x][i]!=maps[y][i])
co++;
return co;
}
//寻找一个点的父结点(这个点所在的集合)
int find_f(int x)
{
if(x!=father[x])
father[x]=find_f(father[x]);
return father[x];
//return x==father[x]? x:father[x]=find_f(father[x]);//上边的完全可以由这一行代替
}
//合并两个不同的集合
void Merge(int a,int b)
{
int x=father[a];
int y=father[b];
if(x==y)
return ;
if(dis[x]>dis[y])
father[y]=father[x];
else
{
if(dis[x]==dis[y])
dis[y]++;
father[x]=father[y];
}
}
//Kruskal算法
int Kruskal()
{
int counts=0;
for(int i=1;i<=n;i++)
father[i]=i;
for(int i=0;i<m;i++)
if(find_f(node[i].start)!=find_f(node[i].endd))//判断这条边是否在同一个集合里面防止成环;
{
Merge(node[i].start,node[i].endd);//合并
counts+=node[i].leng;//将符合条件的边进行加和得到总的路径长度
}
return counts;
}
int main()
{
while(~scanf("%d",&n)&&n)
{
for(int i=1;i<=n;i++)
scanf("%s",maps[i]);
m=0;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
{
node[m].start=i;
node[m].endd=j;
node[m++].leng=Get_leng(i,j);
}
sort(node,node+m,cmp);
printf("The highest possible quality is 1/%d.\n",Kruskal());
}
return 0;
}
(②).Prim算法
1.Prim
算法是一种产生最小生成树的算法。
Prim
算法从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。Prim
算法在找当前最近顶点时使用到了贪婪算法。
算法描述:
1. 在一个加权连通图中,顶点集合V
,边集合为E
2. 任意选出一个点作为初始顶点,标记为visit
,计算所有与之相连接的点的距离,选择距离最短的,标记visit
.
3. 重复以下操作,直到所有点都被标记为visit
:
在剩下的点钟,计算与已标记visit
点距离最小的点,标记visit
,证明加入了最小生成树。
下面我们来看一个最小生成树生成的过程:
1 .起初,从顶点a
开始生成最小生成树
2 .选择顶点a
后,顶点啊置成visit
(涂黑),计算周围与它连接的点的距离:
3. 与之相连的点距离分别为7
,6
,4
,选择C
点距离最短,涂黑C
,同时将这条边高亮加入最小生成树:
4 .计算与a,c
相连的点的距离(已经涂黑的点不计算),因为与a
相连的已经计算过了,只需要计算与c
相连的点,如果一个点与a,c
都相连,那么它与a
的距离之前已经计算过了,如果它与c的距离更近,则更新距离值,这里计算的是未涂黑的点距离涂黑的点的最近距离,很明显,b
和a
为7
,b
和c
的距离为6
,更新b
和已访问的点集距离为6
,而f
,e
和c
的距离分别是8
,9
,所以还是涂黑b
,边bc高亮
:
5. 接下来很明显,d
距离b
最短,将d
涂黑,bd
高亮:
6. f
距离d
为7
,距离b
为4
,更新它的最短距离值是4
,所以涂黑f
,高亮bf
:
7 .最后只有e
了:
2.在写一下一个基本的能够形成最小生成树能够找到最短路径的Prim算法代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#define INF 10000
using namespace std;
const int N=6;//常量
int index;//存储需要处理的点
int counts;//所求的形成最小生成树的边长加和
bool visit[N];//作为标记点是否被处理的bool类型数组
int dis[N];//记录存储一个点到其他点的距离
int Edge[N][N]={{INF,7,4,INF,INF,INF}, //INF代表两点之间不可达
{7,INF,6,2,INF,4},
{4,6,INF,INF,9,8},
{INF,2,INF,INF,INF,7},
{INF,INF,9,INF,INF,1},
{INF,4,8,7,1,INF}
};
int Prim(int cur)
{
index=cur;
counts=0;
printf("%d ",index);//输出第一个点
memset(visit,0,sizeof(visit[0]));//初始化标记数组
visit[cur]=1;//表明cur这个点已经被处理了;
for(int i=0;i<N;i++)
dis[i]=Edge[cur][i];
for(int i=1;i<N;i++)//开始找寻另外的N-1个点
{
int pos=INF;//只是作为一个比较值
for(int j=0;j<N;j++)
if(!visit[j]&&dis[j]<pos)//找到未访问的点中,距离当前最小生成树距离最小的点
{
index=j;
pos=dis[j];
}
visit[index]=1;//更新这个点的状态(已经被处理)
counts+=pos;
printf("%d ",index);
for(int j=0;j<N;j++) //执行更新,如果点距离当前点的距离更近,就更新dis[j];
if(!visit[j]&&dis[j]>Edge[index][j])
dis[j]=Edge[index][j];
}
printf("\n");
return counts;//返回最小生成树的总路径值
}
int main()
{
printf("%d\n",Prim(0));//传递的参数就起始的点
return 0;
}
3.例题
(①).上面的那个Kruskal算法可以解决的例题https://vjudge.net/contest/173215#problem/E
同样可以用Prim算法解决
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define INF 100000
#define N 2100
using namespace std;
char a[N][10];//由字符串组成的数据
int maps[N][N]; //存储由字符串转换成数字的数
int n;//总的输入多少种情况
int dis[N],visit[N]; //记录一个点到其他点的距离,,标记一个点是否被处理
//将字符转换为两点之间的长度
int Get_l(int x,int y)
{
int co=0;
for(int i=0;i<7;i++)
if(a[x][i]!=a[y][i])
co++;
return co;
}
//Prim算法
int Prim()
{
int counts=0;//记录形成最小生成树走过的总的路径
int pos;//记录正要处理的点
int mins;//记录最小的值
memset(visit,0,sizeof(visit));//初始化标记数组
for(int i=0;i<n;i++)//初始化未被处理的边
dis[i]=INF;
for(int i=1;i<n;i++)//将第一个点所连接的边赋给dis()
dis[i]=maps[0][i];
visit[0]=1;
//Prim算法的基本模式
for(int i=1;i<n;i++)
{
mins=INF;
for(int j=0;j<n;j++)
{
if(!visit[j]&&dis[j]<mins)
{
pos=j;
mins=dis[j];
}
}
visit[pos]=1;
counts+=mins;
for(int j=0;j<n;j++)
if(!visit[j]&&dis[j]>maps[pos][j])
dis[j]=a[pos][j];
}
return counts;
}
int main()
{
while(~scanf("%d",&n)&&n)
{
for(int i=0;i<n;i++)
scanf("%s",a[i]);
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
{
int m=Get_l(i,j);//转换函数
maps[i][j]=m;
maps[j][i]=m;
}
printf("The highest possible quality is 1/%d.\n",Prim());
}
return 0;
}
(三).知识点
并查集
(①).了解
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
定义:
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。
主要操作:
①.初始化
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
②.查找
查找元素所在的集合,即根节点。
③.合并
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
(②).算法
在最小生成树里面只有Kruskal算法用到了并查集;
先是建立数据结构:
struct Node//建立一个能够表达成一条边的结构体
{
int start;
int endd;
int leng;
}node[N*N/2];
明确在Kruskal算法中对并查集的应用:
for(int i=1;i<=n;i++)
father[i]=i;
if(find_f(node[i].start)!=find_f(node[i].endd))//判断这条边是否在同一个集合里面防止成环;
{
Merge(node[i].start,node[i].endd);//合并
counts+=node[i].leng;//将符合条件的边进行加和得到总的路径长度
}
①.初始化
for(int i=1;i<=n;i++)
father[i]=i;
②.查找
//寻找一个点的父结点(这个点所在的集合)
int find_f(int x)
{
if(x!=father[x])
father[x]=find_f(father[x]);
return father[x];
//return x==father[x]? x:father[x]=find_f(father[x]);//上边的完全可以由这一行代替
}
③.合并
//合并两个不同的集合
void Merge(int a,int b)
{
int x=father[a];
int y=father[b];
if(x==y)
return ;
if(dis[x]>dis[y])
father[y]=father[x];
else
{
if(dis[x]==dis[y])
dis[y]++;
father[x]=father[y];
}
}
(四).总结:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。