图中的一些基本术语:
顶点(Vertex) ,边(Edge),子图(Subgraph)
有向图和无向图关系比较:
1.度和边:
无向图度TD(vi)是边e的两倍
有向图TD(vi)=ID(vi)+OD(vi)
2.顶点n和边e:
无向图:0≤e≤n(n-1)/2
无向完全图(Undirected Complete Graph,UCG):e=n(n-1)/2 任意两个顶点之间均存在一条边
有向图:0≤e≤n(n-1)
有向完全图((Directed Complete Graph,DCG):e=n(n-1) 任意两个顶点之间存在方向相反的两条弧
3.无向图(u,v):u,v互为邻接点(Adjacent)
有向图<u,v>:顶点u的出边,顶点v的入边
无向边(va,vb);有向边<va,vb>
4.连通和强连通
无向图:
连通图(Connected Graph):有向图中,任意一对顶点 都连通
连通分量(Connected Component):非连通图的极大连通子图。连通图只有一个连通分量是本身
有向图:
强连通图(Srtongly Connected Graph):有向图中,每一对顶点之间都有一条互相到达的路径
强连通分量(Strongly Connected Component):非强连通图的极大强连通子图
权和网:
权(Weight):边上的数值
网(Network):带权的图
路径长度:
(不带权)图中指路径上经过边的数量
网中指路径上经过各边权之和
简单路径:
经过的顶点不重复,否则为非简单路径
回路/环(Cycle):
路径中第一个和最后一个顶点相同
简单回路(Simple Cycle):第一个顶点和最后一个顶点相同
连通图(Connected Graph)&连通分量(Connected Component):
连通图:有向图中,任意一对顶点 都连通
连通分量:非连通图的极大连通子图。连通图只有一个连通分量是本身
生成树和生成森林:
无向图:
生成树(Spanning Tree):具有G中全部顶点的一个极小连通子图。
有向图:
有向树:恰有一个顶点的入度为0,其余顶点入度为1
生成森林(Spanning Forest)由若干棵有向树组成
最小生成树的两种算法
Prim 适合点少边多, Kruskal 适合边多点少。
1.Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
Kruskal代码如下:
时间复杂度为O(elog2e):因为sort()用的是快排思想O(nlog2n),e表示边数
先从小到大排序边,找到当前当前两顶点的父亲结点,在同一棵树上就丢弃,最后合并树
#include<iostream>
#include<algorithm>
using namespace std;
int f[200],n,m,k,len,Veru,Verv,vertex;
struct Edge{
int u,v,w;
}a[200];
bool cmp(Edge u,Edge v){return u.w<v.w;}//规定排序
int find(int x){//作用:获取父亲顶点
return f[x]=(f[x]==x?x:find(f[x]));
}
void Kruskal(){
sort(a,a+m,cmp);//第一步:把所有边排序
for(int i=1;i<=m;i++){
Veru=find(a[i].u),Verv=find(a[i].v);//找当前这两顶点所在的父亲结点,即树
if(Veru==Verv) continue;//两顶点在同一颗树上就丢弃
f[Veru]=Verv;//合并树
len+=a[i].w;
if(++vertex==n-1) break;//最后:此时所有顶点已经在同一棵树上,返回
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) f[i]=i;//刚开始顶点各自成树 例顶点1表示树1
for(int i=1;i<=m;i++) cin>>a[i].u>>a[i].v>>a[i].w;//给所有边依次赋值
Kruskal();
cout<<len;
return 0;
}
测试数据:
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
7
2.Prim算法
Prim算法时间复杂度为O(n2)
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
Prim代码如下:
#include<iostream>
#include<cstring>
using namespace std;
#define N 20
bool vis[N];//记录走过的顶点
int a[N],G[N][N],n,m,len=0;
int Prim(){
vis[1]=true;//从1号顶点开始
for(int i=1;i<=n;i++) a[i]=G[1][i];
for(int i=1;i<n;i++){
int u=-1,t=0x3f3f3f;
for(int j=1;j<=n;j++){//找到当前顶点最小代价的顶点
if(t>a[j]&&vis[j]==false){
t=a[j]; u=j;//u记录最小代价的顶点
}
}
len+=a[u]; vis[u]=true;
for(int i=1;i<=n;i++)
a[i]=min(a[i],G[u][i]);//更新代价,若之前的代价比现在小,则不更新
}
return len;
}
int main(){
cin>>n>>m;
memset(G,0x3f3f3f,sizeof(G));
memset(a,0x3f3f3f,sizeof(a));
for(int i=0;i<m;i++){
int u,v,w; cin>>u>>v>>w;
G[u][v]=G[v][u]=w;//初始化
}
int t=Prim(); cout<<t;
return 0;
}
测试数据:
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
15
最短路径的两种算法
Dijkstra求单源最短路径,Flyod求多源最短路径
1.Dijkstra算法
Dijkstra算法时间复杂度为O(n2)
先找到当前顶点到源顶点的最小距离dis[u],接着更新当前顶点周围顶点到源顶点的距离,然后依次往下找,直到找到最后一个顶点。
单源最短路问题可以看成对一个带权有向图构造最短路径树(SPT)的问题
图中绿色部分为当前访问过的顶点
Dijkstra代码如下:
#include<iostream>
#include<cstring>
using namespace std;
#define N 200
int n,m,G[N][N],dis[N];
bool vis[N];
void Dijkstra(){
for(int i=1;i<=n;i++) dis[i]=G[1][i];
vis[1]=true; dis[1]=0;//源顶点到自身为0
for(int i=2;i<=n;i++){//遍历剩下n-1个顶点
int u=0;
for(int j=1;j<=n;j++){
if(vis[j]==false&&dis[u]>dis[j])
u=j;
}
vis[u]=true;
for(int k=1;k<=n;k++)//这里是该算法的核心代码,看图理解即可
dis[k]=min(dis[k],dis[u]+G[u][k]);//松弛操作
}//dis[u]为当前标记顶点到源顶点的距离,dis[u]+G[u][k]为更新当前顶点周围点到源顶点的距离
for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
}
int main()
{
cin>>n>>m;
memset(G,0x3f3f3f,sizeof G);
memset(dis,0x3f3f3f,sizeof dis);
for(int i=0;i<m;i++){
int u,v,w; cin>>u>>v>>w;
G[u][v]=w;
}
Dijkstra();
return 0;
}
测试数据:
3 3
1 2 2
2 3 1
1 3 4
3
2.Floyd算法
Flyod时间复杂度为O(n3)
确定从点i到点j的代价,接着i经过k到j时有更小的代价就更新。例如1到3,1经过2到3,更小的话更新。1经过4到3,更小的话接着更新…
求每对顶点之间的最短路径是指带权有向图
Floyd代码如下:
#include<iostream>
using namespace std;
#define Inf 0x3f3f3f
int n,m,u,v,w,G[20][20];
int main()
{
cin>>n>>m;
//初始化
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i==j) G[i][j]=0;
else G[i][j]=Inf;
//读入边
for(int i=1;i<=m;i++){
cin>>u>>v>>w;
G[u][v]=w;
}
//执行Floyd核心代码
for(int k=1;k<=n;k++)//中间顶点
for(int i=1;i<=n;i++)//源顶点i
for(int j=1;j<=n;j++)//目标顶点j
G[i][j]=min(G[i][j],G[i][k]+G[k][j]);
//输出任意两点间最短的代价
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)
printf("%10d",G[i][j]);
cout<<endl;
}
return 0;
}
测试数据:
4 4
1 2 4
2 3 7
2 4 1
3 4 6
拓扑排序算法
时间复杂度O(n+e)
采用邻接表存储
检测AOV网中是否存在环
拓扑排序代码如下:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int n,m,in[100];
vector<int> v[100];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) v[i].clear();
for(int i=1;i<=m;i++){
int A,T; cin>>A>>T;//源顶点指向目标顶点
v[A].push_back(T);
in[T]++;//目标顶点的入度加1
}
priority_queue<int,vector<int>,greater<int> >q;//优先队列,设置顶点关系从小到大排序
for(int i=1;i<=n;i++)
if(in[i]==0) q.push(i);//1.把入度为0的点压入队列
while(!q.empty()){
int x=q.top(); q.pop();
n--;//每次去掉一个点
for(int i=0;i<v[x].size();i++){//依次删掉到当前度为0的这一条关系
int y=v[x][i];//2.从入度0的顶点开始,找目标顶点
in[y]--;//3.目标顶点的度少这一条
if(in[y]==0) q.push(y);//4.如果当前顶点的入度变为0,则压入队列中
}
}
if(n) cout <<"NO"<<endl;//如果有环的话节点数不会为0
else cout <<"YES"<<endl;
return 0;
}
测试数据:
3 3
1 2
2 3
1 3
染色法判定二部图算法代码暂时先不整理…
AOV和拓扑排序
(1) DAG(有向无环图):不含环的有向图,没有回路。
(2) AOV(顶点表示活动的网):顶点表示活动,有向边表示活动之间的优先制约关系。
(3) AOV网中不允许存在回路,回路的出现意味着某项活动的开工将以自身工作的完成作为先决条件,产生死锁。检测有向图中是否存在环的方法是进行拓扑排序。
AOE网与关键路径
(1) AOE(边表示活动的网),一个带权有向图,有向边表示活动,权值表示活动的持续时间,顶点表示时间。
(2) 特殊顶点:源点表示所有活动的开始,汇点表示整个活动的结束
(3) AOE网中不允许存在回路且唯一存在源点和汇点
(4) 关键路径:AOE网中最大长度的路径。关键活动:关键路径上的活动
图的2种遍历算法:
1.图的DFS算法
DFS算法代码如下:
#include<iostream>
using namespace std;
const int INF=0x3f3f3f;
int n,m,u,v,sum,a[20][20];
bool vis[20];
void Dfs(int U){
sum++;
cout<<U<<" ";//输出当前访问的顶点
if(sum==n) return ; //已访问所有顶点,结束。
for(int i=1;i<=n;i++){
if(vis[i]==false&&a[U][i]==1){ //访问未访问过的点
vis[i]=true; //标记找到的点
Dfs(i); //递归查找。
}
}
return ;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){ //初始化
for(int j=1;j<=n;j++)
if(i==j) a[i][j]=0;
else a[i][j]=INF;
}
for(int i=1;i<=m;i++){
cin>>u>>v; //二维数组存储
a[u][v]=1;
}
vis[1]=true;
Dfs(1);//从顶点1开始
return 0;
}
测试样例 :
5 5
1 2
1 3
1 5
2 4
3 5
结果输出:
1 2 4 3 5