设图 G = (V, E)是一个有 n 个顶点的图,则图的邻接矩阵G.arcs[n][n]定义为:
无向图的邻接矩阵是对称的,在无向图中,第 i 行/列 1 的个数就是顶点i的度。
有向图的邻接矩阵可能是不对称的,在有向图中,每个1对应的行为起点i,对应的列为终点j,第 i 行 1 的个数就是顶点 i 的出度,第 j 列 1 的个数就是顶点 j 的入度。
带权图(网):
代码实现:
class AdjMatrix{
int mat[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
};
class MGraph{
VertexType vexs[MAX_VERTEX_NUM]; //顶点表
AdjMatrix arcs; //邻接矩阵
int vexnum,arcnum; //图的顶点数和弧数
};
邻接表的结构如下:
// 边结点(链式)
class EdgeNode {
public:
int adjvex; //这条边指向哪个顶点
int weight; //权值
EdgeNode* next; //指向下一条依附该顶点的边
};
// 顶点结构
class VertexNode {
public:
int value; //顶点的值
EdgeNode *firstedge; //指向第一条依附该顶点的边
};
// 图结构
class GraphList {
public:
VertexNode adjList[N]; //顶点信息
int numVertex; //顶点数
int numEdges; //边数
};
下面的图所得到的邻接表如下图所示:
adr | value | firstedge (adjvex,weight) |
---|---|---|
0 | A | -> 1(10) -> 2(6) -> 3(4) |
1 | B | -> 2(7) |
2 | C | -> 1(9) -> 3(5) |
3 | D | -> 1(8) |
同时还有存储入边的逆邻接表:
已知存储结构,接下来我们需要根据输入,完成函数void createGraph(GraphList* g)
,使用邻接表的方式来实现有向图的创建。输入包含3个部分:
void createGraph(GraphList* g){
int v,e;
cin>>v>>e;
g->numVertex=v;
g->numEdges=e;
//图的顶点与边的个数,构建图
for(int i=0;i<v;i++){
cin>>g->adjList[i].value;
}//各个顶点的值,构建顶点
for(int i=0;i<e;i++){
int vi,vj,w;
cin>>vi>>vj>>w;
EdgeNode* newe=new EdgeNode;//创建一条新的边
newe->weight=w;//边的权值
newe->adjvex=vj;//边指向vj
EdgeNode *p;
p=g->adjList[vi].firstedge; //这条边以vi开头
if(p==NULL){
g->adjList[vi].firstedge=newe;
}
else {
while(p->next != NULL){
p=p->next;
}
p->next=newe;
}//将这条边连接至边结点的最后
}//构建边
}
在邻接表中,我们可以通过g->adjList[i]
访问每一个顶点;令p=g->adjList[i].firstedge
,则p
所指向的链表的长度为点i
的出度;如果需要得到点i
的入度,需要依次访问所有的点,统计所有的p->adjvex
:
int count[g->numVertex];//统计入度
for(int i=0;i<g->numVertex;i++){
count[i]=0;
}
for(int i=0;i<g->numVertex;i++){
EdgeNode *p=g->adjList[i].firstedge;
while(p){
count[p->adjvex]++;
p=p->next;
}
}
在代码实现时,利用了一个辅助数组visit
,用于标记该结点是否已经被访问。
代码实现如下:
/*
图以邻接表形式储存
*/
void DFS(GraphList *g,int i,int *visited){
cout<<g->adjList[i].value<<" "; //访问该点
visited[i]=1;//已访问标记
EdgeNode *p;
p=g->adjList[i].firstedge;//找到该点的第一个邻接点
while(p!=NULL){
if(visited[p->adjvex]==0)
DFS(g,p->adjvex,visited);//当该邻接点未被访问时,递归访问该邻接点
p=p->next;//寻找下一个邻接点
}
}
void DFSTraverse(GraphList *g){
int *visited= new int [g->numVertex];
for(int i=0;i<g->numVertex;i++){
visited[i]=0;
} //visited数组记录点是否已经访问
for(int i=0;i<g->numVertex;i++){//依次访问图中顶点
if(visited[i]==0)DFS(g,i,visited);//当该点没被访问过,进行深度优先搜索
}
delete [] visited;
}
代码实现如下:
/*
图以邻接表形式储存
*/
void BFS(GraphList *g,int i,int *visited){
cout<<g->adjList[i].value<<" "; //访问该顶点
visited[i]=1; //已访问标记
queue<int> q;
q.push(i); //将起始点入队
while(!q.empty()){
//每个while循环将一个顶点出队,并依次访问其所有邻接点
int w=q.front();
q.pop();
EdgeNode *p;
p=g->adjList[w].firstedge;//用于访问所有邻接点
while(p!=NULL){//访问邻接链表中的所有邻接点
if(visited[p->adjvex]==0){
cout<<g->adjList[p->adjvex].value<<" "; //访问该顶点
visited[p->adjvex]=1;//已访问标记
q.push(p->adjvex);
}
p=p->next;
}
}
}
void BFSTraverse(GraphList *g){
int *visited= new int [g->numVertex];
for(int i=0;i<g->numVertex;i++){
visited[i]=0;
}//visited数组记录点是否已经访问
for(int i=0;i<g->numVertex;i++){
if(visited[i]==0)BFS(g,i,visited);//依次对未被访问过的顶点进行广度优先搜索
}
delete [] visited;
}
两种遍历方法,使用邻接矩阵表示时,总的时间代价均为O(n^2)。
使用邻接表表示时,深度优先搜索扫描边的时间为O(e),而且对所有顶点递归访问1次,所以遍历图的时间复杂性为O(n+e);广度优先搜索循环的总时间代价为d_0 + d_1 + … + d_{n-1} = O(e),其中的d_i是顶点 i 的度。
选择新的边时必须有一个顶点在已构成的树中。
假设共有n个顶点,我们需要设置一个辅助数组closedge[n]
,该数组包含两个元素:
lowcost[i]
:(当前操作时)生成树内顶点与该顶点相连的最短的边的权值;起始顶点为0,未直接相连的顶点为∞。adjvex[i]
:(当前操作时)与该顶点距离最近的生成树内顶点的值,生成树内的顶点的该值为-1。class closedge{
int lowcost,adjvex;
};//辅助数组
class TreeNode{
int vi,vj;
int weight;
};//生成树
以下图为例,我们一步步得到最小生成树。
adjvex[i]!=-1
中lowcost[i]
最小所对应的i,对i进行操作:
adjvex[i]=-1
,并将边[i,j,w]
存入生成树集合;
[i,j]
,当adjvex[j]!=-1
时(不形成环),比较每条边的权值与lowcost[j]
的大小,令其取最小值,并令adjvex[j]=i
;
adjvex[i]
的值均为-1,lowcost[i]
的和为总权值。
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 28 | ∞ | ∞ | ∞ | 10 | ∞ |
adjvex | -1 | 0 | 0 | 0 | 0 | 0 | 0 |
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 28 | ∞ | ∞ | 25 | 10 | ∞ |
adjvex | -1 | 0 | 0 | 0 | 5 | -1 | 0 |
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 28 | ∞ | 22 | 25 | 10 | 24 |
adjvex | -1 | 0 | 0 | 4 | -1 | -1 | 4 |
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 28 | 12 | 22 | 25 | 10 | 18 |
adjvex | -1 | 0 | 3 | -1 | -1 | -1 | 3 |
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 16 | 12 | 22 | 25 | 10 | 18 |
adjvex | -1 | 2 | -1 | -1 | -1 | -1 | 3 |
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 16 | 12 | 22 | 25 | 10 | 14 |
adjvex | -1 | -1 | -1 | -1 | -1 | -1 | 1 |
N | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
lowcost | 0 | 16 | 12 | 22 | 25 | 10 | 14 |
adjvex | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
最终得到最小生成树:
我们以邻接表存储图,代码实现该算法:
class closedge{
int lowcost,adjvex;
};//辅助数组
class TreeNode{
int vi,vj;
int weight;
};//生成树
void Prim(GraphList G, MST& T, int u ) {//u为起点
float lowcost[G.NumVertices];
int nearvex[G.NumVertices];
for ( int i = 0; i < G.NumVertices; i++ ) {
lowcost[i] = G.Edge[u][i]; //Vu到各点代价
adjvex[i] = u; //及最短带权路径
}
adjvex[u] = -1; //加到生成树顶点集合
int k = 0; //存放最小生成树的结点
for ( i = 0; i < G.NumEdges-1; i++ )//循环n-1次, 加入n-1条边
{
if ( i != u )
{
EdgeData min = MaxValue;
int v = 0;
for ( int j = 0; j < NumVertices; j++ )
if ( adjvex[j] != -1 && lowcost[j] < min )
{ // =-1不参选
v = j;
min = lowcost[j];
}
//求生成树外顶点到生成树内顶点具有最小权值的边, v是当前具最小权值的边
if ( v )
{ //v=0表示再也找不到要求顶点
T[k].tail = adjvex[v]; //选边加入生成树
T[k].head = v;
T[k].cost = lowcost[v];
k++;
adjvex[v] = -1; //该边加入生成树标记
for ( j = 0; j < G.n; j++ )
if ( adjvex[j] != -1 && G.Edge[v][j] < lowcost[j] )
{
lowcost[j] = G.Edge[v][j]; //修改
adjvex[j] = v;
}
}
}
}
选择新的边时选择最小的不成环的边构成树。
代码实现:
typedef int Vertex;//顶点信息
struct Edge//边的信息
{
Vertex begin;
Vertex end;
int edge;//边的权值
};
int n;//顶点数
int m;//边的数目
Edge Graph[5000];//以边存储图
int pre[110];//并查集基本操作
int sum;//最小的生成树权值和
void Init()
{
for(int i=1;i<=n;i++)
{
pre[i]=i;
}
}
int find(int x)
{
int r=x;
while(pre[r]!=r)
r=pre[r];
int i=x,j;
while(i!=r)
{
j=pre[i];
pre[i]=r;
i=j;
}
return r;
}
void join(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx!=fy)
pre[fx]=fy;
}
bool comp(Edge a,Edge b)//对边权值进行排序
{
return a.edge<b.edge;
}
void Kruskal()
{
sort(Graph,Graph+m,comp);//排序,最小权值的边最先
int number=0; //记录当前用于连接顶点的边数
Init();//初始化并查集
sum=0;
for(int i=0;i<m;i++)
{
if(number==n-1)//n个顶点若连接n-1条边,则图已经连通
break;
if(find(Graph[i].begin)!=find(Graph[i].end))//当前边所连接的顶点处于未连通的状态
{
join(Graph[i].begin,Graph[i].end);
sum+=Graph[i].edge;
number++;
}
}
}
count[ ]
,记录各顶点入度。void Graphcircle(GraphList *g){
int count[g->numVertex],del[g->numVertex];
for(int i=0;i<g->numVertex;i++){
count[i]=0;
}
for(int i=0;i<g->numVertex;i++){
EdgeNode *p=g->adjList[i].firstedge;
while(p){
count[p->adjvex]++;
p=p->next;
}
}//初始化count,得到所有顶点的入度
stack<int> sta;
int num=0;
for(int i=0;i<g->numVertex;i++){
if(count[i]==0){
sta.push(i);
}
}//将初始时所有入度为0的顶点入栈
while(!sta.empty()){
int w=sta.top();
sta.pop();//弹出一个顶点,对其进行操作
//cout<<g->adjList[w].value; //输出排序
num++;//统计弹出顶点数
EdgeNode *p=g->adjList[w].firstedge;//访问该顶点的邻接点
while(p){
count[p->adjvex]--;
if(count[p->adjvex]==0){
sta.push(p->adjvex);
}//更新剩余顶点的入度,并马上判断是否有新的顶点入度为0
p=p->next;
}
}
if(num==g->numVertex) cout<<"no circle";//无环,得到拓扑排序
else cout<<"has circle";//有环
}
无有向环的带权有向图中:
入度为零的点叫源点,出度为零的点叫汇点。完成整个工程所需的时间取决于从源点到汇点的最长路径长度,这条路径长度最长的路径就叫做关键路径,路径上的活动叫做关键活动。
使用邻接矩阵mat[][]存储图,利用4个辅助数组ve[],vl[],e[],l[]进行计算,以下的顶点v_0指所有入度为零的点,顶点v_{n-1}指所有入度为零的点:
ve[i]:事件最早发生时间,源点v_0到顶点v_i的最长路径长度。
可从ve[0]
开始递推,ve[0]=0
,ve[j]=max(ve[j],ve[i]+mat[i][j])
,此时必须确保点j的最早发生时间已确定,具体实现时需要使用拓扑排序。
vl[i]:事件最迟允许时间,是在保证汇点v_{n-1}在ve[n-1] 时刻完成的前提下,事件v_{i}的允许的最迟开始时间。
可从vl[n-1]
开始递推,vl[n-1]=ve[n-1]
,vl[i]=min(vl[i],vl[j]-mat[i][j])
,此时必须确保点j的最迟允许时间已确定,具体实现时需要逆序使用拓扑排序。
设活动k在路径<i,j>上:
e[i]
:活动最早发生时间,直接通过e[k] = ve[i]
得到即可。
l[i]
:活动最迟允许时间,直接通过l[k] = vl[j] - mat[i][j]
得到。
当l[k] == e[k]
时,活动k就是关键活动,所有关键活动组成关键路径。
void AOE(){
int n,m;
cin>>n>>m;
int mat[n][n],ve[n],vl[n],e[m],l[m],edge[m][2];
for(int i=0;i<n;i++){
ve[i]=-1;
vl[i]=999;
for(int j=0;j<n;j++)
mat[i][j]=0;
}
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
mat[a][b]=w;
edge[i][0]=a;
edge[i][1]=b;
}
//拓扑
int count[n];
for(int i=0;i<n;i++){
count[i]=0;
}
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(mat[i][j]>0)count[j]++;
}
}
stack<int> sta,last;
int num=0;
for(int i=0;i<n;i++){
if(count[i]==0){
sta.push(i);
ve[i]=0;
}
}//第一个入栈
while(!sta.empty()){
int w=sta.top();
sta.pop();
last.push(w);
for(int i=0;i<n;i++){
if(i!=w && mat[w][i]!=0){
count[i]--;
ve[i]=max(ve[i],ve[w]+mat[w][i]);
if(count[i]==0)
sta.push(i);
}
}
}
int w=last.top();
vl[w]=ve[w];
while(!last.empty()){
int w=last.top();
last.pop();
for(int i=0;i<n;i++){
if(i!=w && mat[i][w]!=0){
vl[i]=min(vl[i],vl[w]-mat[i][w]);
}
}
}
for(int i=0;i<m;i++){
e[i]=ve[edge[i][0]];
l[i]=vl[edge[i][1]]-mat[edge[i][0]][edge[i][1]];
}
for(int i=0;i<n;i++){
cout<<ve[i]<<"\t";
}
cout<<endl;
for(int i=0;i<n;i++){
cout<<vl[i]<<"\t";
}cout<<endl;
for(int i=0;i<m;i++){
cout<<e[i]<<"\t";
}
cout<<endl;
for(int i=0;i<m;i++){
cout<<l[i]<<"\t";
}
cout<<endl;
return 0;
}
为了实现算法,我们引用3个辅助数组:
算法步骤如下——
以下面的图为例:
初始化:
i | v1 | v2 | v3 | v4 | v5 | v6 |
---|---|---|---|---|---|---|
dist[i] | ∞ | 20 | ∞ | 0 | ∞ | 15 |
path[i] | -1 | 4 | -1 | 4 | -1 | 4 |
S[i] | 0 | 0 | 0 | 1 | 0 | 0 |
V6:
i | v1 | v2 | v3 | v4 | v5 | v6 |
---|---|---|---|---|---|---|
dist[i] | ∞ | 20 | ∞ | 0 | ∞ | 15 |
path[i] | -1 | 4 | -1 | 4 | -1 | 4 |
S[i] | 0 | 0 | 0 | 1 | 0 | 1 |
V2:
i | v1 | v2 | v3 | v4 | v5 | v6 |
---|---|---|---|---|---|---|
dist[i] | 30 | 20 | ∞ | 0 | 50 | 15 |
path[i] | 2 | 4 | -1 | 4 | 2 | 4 |
S[i] | 0 | 1 | 0 | 1 | 0 | 1 |
V1:
i | v1 | v2 | v3 | v4 | v5 | v6 |
---|---|---|---|---|---|---|
dist[i] | 30 | 20 | 45 | 0 | 50 | 15 |
path[i] | 2 | 4 | 1 | 4 | 2 | 4 |
S[i] | 1 | 1 | 0 | 1 | 0 | 1 |
V3:
i | v1 | v2 | v3 | v4 | v5 | v6 |
---|---|---|---|---|---|---|
dist[i] | 30 | 20 | 45 | 0 | 50 | 15 |
path[i] | 2 | 4 | 1 | 4 | 2 | 4 |
S[i] | 1 | 1 | 1 | 1 | 0 | 1 |
V5:
i | v1 | v2 | v3 | v4 | v5 | v6 |
---|---|---|---|---|---|---|
dist[i] | 30 | 20 | 45 | 0 | 50 | 15 |
path[i] | 2 | 4 | 1 | 4 | 2 | 4 |
S[i] | 1 | 1 | 1 | 1 | 1 | 1 |
代码实现如下:
void Dijkstra(){
int n,m,v;//顶点数n、边数m、起点v
cin>>n>>m>>v;
int mat[n][n];//邻接矩阵
int dist[n],path[n],S[n];//三个辅助数组
for(int i=0;i<n;i++){
dist[i]=0;
path[i]=-1;
S[i]=0;
for(int j=0;j<n;j++)
mat[i][j]=0;
}//数组初始化
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
mat[a][b]=w;
}//邻接矩阵输入
dist[v]=0;
path[v]=v;
S[v]=1;
for(int i=0;i<n;i++){
if(mat[v][i]>0){
dist[i]=mat[v][i];
path[i]=v;
}
}//算法初始化
// for(int j=0;j<n;j++){
// cout<<dist[j]<<"\t";
// }
// cout<<endl;
// for(int j=0;j<n;j++){
// cout<<path[j]<<"\t";
// }
// cout<<endl;
//输出检测
for(int i=0;i<n-1;i++){//执行n-1个循环,依次确定每个点的最短路径
int min=999,min_ind=0;
for(int j=0;j<n;j++){
if(S[j]==0){
if(min>dist[j]){
min=dist[j];
min_ind=j;
}
}
}//找到未得到最小路径的点中的最小值
S[min_ind]=1;//该点确定最小路径,添加至S
for(int j=0;j<n;j++){
if(mat[min_ind][j]>0){
if(dist[j]>(dist[min_ind]+mat[min_ind][j])){
dist[j]=dist[min_ind]+mat[min_ind][j];//修改路径长度
path[j]=min_ind;//标记路径
}
}
}
}
return 0;
}
算法思路与Dijkstra算法相同,不过直接对邻接矩阵进行操作,得出所有顶点间的路径,时间复杂度为O(n^3)。
邻接矩阵A[i][j]
存储最小路径<i,j>;增加一矩阵path[i][j]
存储当前路径下顶点j的上一顶点。
矩阵A_{k+1}得到的路径是路线中间点序号不超过k的最短路径,最终得到不超过n-1的最短路径,即最终路径。
邻接矩阵递推关系如下:
算法步骤如下——
示例:
代码实现如下:
void Floyd(){
int n,m;//顶点数n、边数m
cin>>n>>m;
int mat[n][n];//邻接矩阵
int path[n][n];//三个辅助数组
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
mat[i][j]=999;
path[i][j]=-1;
}
mat[i][i]=0;
path[i][i]=i;
}//数组初始化
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
mat[a][b]=w;
path[a][b]=a;
}//算法初始化
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
//当新的路径更加短时,更新mat和path
if(mat[i][j]>mat[i][k]+mat[k][j]){
mat[i][j]=mat[i][k]+mat[k][j];
path[i][j]=path[k][j];
}
}
}
}
// for(int i=0;i<n;i++){
// for(int j=0;j<n;j++){
// cout<<mat[i][j]<<"\t";
// }
// cout<<endl;
// }
// cout<<endl;
//输出检测
return 0;
}