博客04 图

这个作业属于哪个班级 数据结构--网络2011/2012
这个作业的地址 DS博客作业04--图
这个作业的目标 学习图结构设计及相关算法
姓名 骆梦钒

0.PTA得分截图

图题目集总得分,请截图,截图中必须有自己名字。题目至少完成2/3,否则本次作业最高分5分。

1.本周学习总结(6分)

1.1 图的存储结构

1.1.1 邻接矩阵(不用PPT上的图)

1.邻接矩阵,

顾名思义,是一个矩阵,一个存储着边的信息的矩阵,而顶点则用矩阵的下标表示。对于一个邻接矩阵M,如果M(i,j)=1,则说明顶点i和顶点j之间存在一条边,对于无向图来说,M (j ,i) = M (i, j),所以其邻接矩阵是一个对称矩阵;对于有向图来说,则未必是一个对称矩阵。邻接矩阵的对角线元素都为0。

邻接矩阵的特点如下:

(1)图的邻接矩阵表示是唯一的。

(2)无向图的邻接矩阵一定是一个对称矩阵。

因此,按照压缩存储的思想,在具体存放邻接矩阵时只需存放上(或下)三角形阵的元素即可。

(3)不带权的有向图的邻接矩阵一般来说是一个稀疏矩阵。

因此,当图的顶点较多时,可以采用三元组表的方法存储邻接矩阵。

(4)对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度。

(5)对于有向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的出度(或入度)。

(6)用邻接矩阵方法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。这是用邻接矩阵存储图的局限性。

2.邻接矩阵的结构体定义

#define VERTEX_MAX 6
#define MAXVALUE 32767
typedef struct{
    int vertex[VERTEX_MAX];
    int edges[VERTEX_MAX][VERTEX_MAX];
    int vertexNum,edgesNum;
    int grapType;
}MatrixGraph;

3.建图函数

void createGraph(MatrixGraph *g){
    int start,end;
    printf("Please input  vertexs\n");
    for(int i=0;i<g->vertexNum;i++){
        printf("the %d vertex is ",i+1);
        scanf("%d",&g->vertex[i]);
    }
    printf("\nPlease input the edges. The former is that the first input is the start vertex and the second input is the end vertex!\n");
    for(int i=0;i<g->edgesNum;i++){
        printf("the %d edge:(please input data like above!) ",i+1);
        scanf("%d%d",&start,&end);
        g->edges[start-1][end-1]=1;
        if(g->grapType==0)
        g->edges[end-1][start-1]=1;
    }
}

1.1.2邻接表

1.图的邻接表存储方法

是一种顺序分配与链式分配相结合的存储方法。

在邻接表中,对图中每个顶点建立一个单链表,第i个单链表中的节点表示依附于顶点i的边(对有向图是以顶点i为尾的边)。每个单链表上附设一个表头节点。

其中,表节点由三个域组成,adjvex指示与顶点i邻接的点在图中的位置,nextarc指示下一条边或弧的节点,info存储与边或弧相关的信息,如权值等。
表头节点由两个域组成,data存储顶点i的名称或其他信息,firstarc指向链表中第一个节点。

邻接表的特点如下:

(1)邻接表表示不唯一。

这是因为在每个顶点对应的单链表中,各边节点的链接次序可以是任意的,取决于建立邻接表的算法以及边的输入次序。

(2)对于有n个顶点和e条边的无向图,其邻接表有n个顶点节点和2e个边节点。

显然,在总的边数小于n(n-1)/2的情况下,邻接表比邻接矩阵要节省空间。

(3)对于无向图,邻接表的顶点i对应的第i个链表的边节点数目正好是顶点i的度。

(4)对于有向图,邻接表的顶点i对应的第i个链表的边节点数目仅仅是顶点i的出度。其入度为邻接表中所有adjvex域值为i的边节点数目。

2.邻接矩阵的结构体定义

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
#define M 20            //预定义图的最大顶点数
typedef char DataType; //顶点信息数据类型
typedef struct node{      //边表结点
    int adjvex;      //邻接点
    struct node *next;
}EdgeNode;
typedef struct vnode{     //头结点类型
    DataType vertex;         //顶点信息
    EdgeNode *FirstEdge;  //邻接链表头指针
}VertexNode;
typedef struct{          //邻接表类型
    VertexNode adjlist[M]; //存放头结点的顺序表
    int n,e;                 //图的顶点数与边数
}LinkedGraph;
       

3.建图函数

void creat(LinkedGraph *g,char *filename,int c)
{
    int i,j,k;
    EdgeNode *s;
    FILE *fp;
    fp=fopen(filename,"r");
    if (fp)
    {
        fscanf(fp,"%d%d",&g->n,&g->e);             //读入顶点数与边数
        for(i=0;i<g->n;i++)
        {
            fscanf(fp,"%1s",&g->adjlist[i].vertex);   //读入顶点信息
            g->adjlist[i].FirstEdge=NULL;        //边表置为空表
        }
        for(k=0;k<g->e;k++)                    //循环e次建立边表
        {
            fscanf(fp,"%d%d",&i,&j);                // 输入无序对(i,j)
            s=(EdgeNode *)malloc(sizeof(EdgeNode));
            s->adjvex=j;                        //邻接点序号为j
            s->next=g->adjlist[i].FirstEdge;
            g->adjlist[i].FirstEdge=s;         //将新结点*s插入顶点vi的边表头部
            if (c==0)                           //无向图
            {
                s=(EdgeNode *)malloc(sizeof(EdgeNode));
                s->adjvex=i;                        //邻接点序号为i
                s->next=g->adjlist[j].FirstEdge;
                g->adjlist[j].FirstEdge=s;     //将新结点*s插入顶点vj的边表头部
            }
        }
        fclose(fp);
    }
    else
        g->n=0;
}

1.1.3 邻接矩阵和邻接表表示图的区别

1.对于一个具有n个顶点e条边的无向图
它的邻接表表示有n个顶点表结点2e个边表结点
对于一个具有n个顶点e条边的有向图
它的邻接表表示有n个顶点表结点e个边表结点
如果图中边的数目远远小于n2称作稀疏图,这是用邻接表表示比用邻接矩阵表示节省空间;
如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。

2.对于DFS,BFS遍历来说,时间复杂度和存储结构有关:
n表示有n个顶点,e表示有e条边。
(1)若采用邻接矩阵存储,
时间复杂度为O(n^2);
(2)若采用邻接链表存储,建立邻接表或逆邻接表时,
若输入的顶点信息即为顶点的编号,则时间复杂度为O(n+e);
若输入的顶点信息不是顶点的编号,需要通过查找才能得到顶点在图中的位置,则时间复杂度为O(n*e);

1.2 图遍历

1.2.1 深度优先遍历

1.无向图


对上无向图进行深度优先遍历,从A开始:

第1步:访问A。

第2步:访问B(A的邻接点)。 在第1步访问A之后,接下来应该访问的是A的邻接点,即"B,D,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"D和F"的前面,因此,先访问B。

第3步:访问G(B的邻接点)。 和B相连只有"G"(A已经访问过了)

第4步:访问E(G的邻接点)。 在第3步访问了B的邻接点G之后,接下来应该访问G的邻接点,即"E和H"中一个(B已经被访问过,就不算在内)。而由于E在H之前,先访问E。

第5步:访问C(E的邻接点)。 和E相连只有"C"(G已经访问过了)。

第6步:访问D(C的邻接点)。

第7步:访问H。因为D没有未被访问的邻接点;因此,一直回溯到访问G的另一个邻接点H。

第8步:访问(H的邻接点)F。

因此访问顺序是:A -> B -> G -> E -> C -> D -> H -> F

2.有向图


对上有向图进行深度优先遍历,从A开始:

第1步:访问A。

第2步:访问(A的出度对应的字母)B。 在第1步访问A之后,接下来应该访问的是A的出度对应字母,即"B,C,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"C和F"的前面,因此,先访问B。

第3步:访问(B的出度对应的字母)F。 B的出度对应字母只有F。

第4步:访问H(F的出度对应的字母)。 F的出度对应字母只有H。

第5步:访问(H的出度对应的字母)G。

第6步:访问(G的出度对应字母)E。 在第5步访问G之后,接下来应该访问的是G的出度对应字母,即"B,C,E"中的一个。但在本文的实现中,顶点B已经访问了,由于C在E前面,所以先访问C。

第7步:访问(C的出度对应的字母)D。

第8步:访问(C的出度对应字母)D。 在第7步访问C之后,接下来应该访问的是C的出度对应字母,即"B,D"中的一个。但在本文的实现中,顶点B已经访问了,所以访问D。

第9步:访问E。D无出度,所以一直回溯到G对应的另一个出度E。

因此访问顺序是:A -> B -> F -> H -> G -> C -> D -> E

3.深度遍历代码

//从v出发深度优先遍历的递归函数
void MGraph::DFS(int v)
{
    int n=vertexNum;//顶点数目
    if(v<0||v>=n) throw "位置出错";
    cout<<vertex[v]<<" ";//输出顶点v
    visited[v]=1;//被访问过
    for(int j=0;j<n;j++)
        if(visited[j]==0&&adj[v][j]==1)//没被访问过且存在边(v,j)
            DFS(j);
}

//从v出发深度优先遍历的非递归函数
void MGraph::DFS1(int v)
{
    int S[MaxSize],n=vertexNum,top=-1,j;
    if(v<0||v>=n) throw "位置出错";
    cout<<vertex[v]<<" ";//输出顶点v
    visited[v]=1;//被访问过
    S[++top]=v;//顶点v进栈
    while(top!=-1)
    {
        v=S[top];//栈顶元素出栈
        for(j=0;j<n;j++)
        {
            if(visited[j]==0&&adj[v][j]==1)//没被访问过且存在边(v,j)
            {
                cout<<vertex[j]<<" ";
                visited[j]=1;
                S[++top]=j;
                break;
            }
        }
        if(j==n) top--;
    }
}

4.深度遍历适用哪些问题的求解。(可百度搜索)

(1)八皇后

一个如下的 6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

上面的布局可以用序列 2 4 6 1 3 5 来描述,第 ii 个数字表示在第 ii 行的相应位置有一个棋子,如下:

行号 1 2 3 4 5 6

列号 2 4 6 1 3 5

这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前 33 个解。最后一行是解的总个数。

#include<bits/stdc++.h>
using namespace std;
int p[100];//储存路径
int visy[100]={0};//记录列是否访问
int vxie1[100]={0};//记录左下到右上斜线是否访问
int vxie2[100]={0};//左上到右下
int n;//n*n棋盘
int total=0;//记录搜索到结果总数
void dfs(int x){
	if(x==n){
		if(total<=2)
		for(int i=0;i<n;i++) printf("%d ",p[i]+1);
		if(total<2) printf("\n");
		total++;
		return ;
	}//如果X=n表示已经搜到最后一行,total++,一条路走到尽头
	//否则这一行,有n种情况,如果符合题意,每个情况继续深搜
	for(int j=0;j<n;j++){
		if(!visy[j]&&!vxie1[x+j]&&!vxie2[x-j+n]){
			p[x]=j;//储存路径,表示x行选择j列
			visy[j]=1;vxie1[x+j]=1;vxie2[x-j+n]=1;//标记这一行,列,斜线
			dfs(x+1);//深搜下一行
			visy[j]=0;vxie1[x+j]=0;vxie2[x-j+n]=0;//深搜结束
			//回到这一行初始状态就是回溯,进行下一种情况深搜
		}
	}
}
int main(){
	    cin>>n;
		dfs(0);
		printf("\n%d",total); 
}
(2)分考场

问题描述
  n个人参加某项特殊考试。
  为了公平,要求任何两个认识的人不能分在同一个考场。
  求是少需要分几个考场才能满足条件。
输入格式
  第一行,一个整数n(1<n<100),表示参加考试的人数。
  第二行,一个整数m,表示接下来有m行数据
  以下m行每行的格式为:两个整数a,b,用空格分开 (1<=a,b<=n) 表示第a个人与第b个人认识。
输出格式
  一行一个整数,表示最少分几个考场。

#include<bits/stdc++.h>
using namespace std;
int n,m,min_num;
int mp[102][102]={{0}};
int r[102][102]={{0}};
void dfs(int x,int num){//x表示开始安排编号X,已经用了num个房间
	if(num>=min_num) return ;//剪枝
	if(x>n){
		if(num<min_num) min_num=num;
	}//如果已经安排完,比较后更新最少房间数
	//八皇后一样到x行,都会有n种情况。对于此题到达分配编号x,有num个房间
	for(int i=1;i<=num;i++){
		int k=0;
		while(r[i][k]&&!mp[x][r[i][k]]) k++;//找到放在房间的那一个位置
		if(r[i][k]==0){
			r[i][k]=x;
			dfs(x+1,num);
			r[i][k]=0;
		}
	}//找不到合适的房间,就开一个新的房间继续深搜,代码就是把流程写了一遍
	r[num+1][0]=x;
	dfs(x+1,num+1);
	r[num+1][0]=0;
}
int main()
{
	cin>>n>>m;
	min_num=n;
	while(m--){
		int x,y;
		cin>>x>>y;
		mp[x][y]=mp[y][x]=1;
	}
	dfs(1,0);
	printf("%d\n",min_num);
}
(3)迷宫题目地址

题目背景
给定一个N*M方格的迷宫,迷宫里有T处障碍,障碍处不可通过。给定起点坐标和终点坐标,问: 每个方格最多经过1次,有多少种从起点坐标到终点坐标的方案。在迷宫中移动有上下左右四种方式,每次只能移动一个方格。数据保证起点上没有障碍。

输入格式
第一行N、M和T,N为行,M为列,T为障碍总数。第二行起点坐标SX,SY,终点坐标FX,FY。接下来T行,每行为障碍点的坐标。

输出格式
给定起点坐标和终点坐标,问每个方格最多经过1次,从起点坐标到终点坐标的方案总数。

#include<bits/stdc++.h>
using namespace std;
int n,m,t,total=0;//n行,m列,t个障碍物
int sx,sy,x2,y2;//起点x,y,终点x,y
int mp[10][10]={{0}};
int vis[10][10]={{0}};
int dir[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
void dfs(int x,int y){
	if(x==x2&&y==y2){
		total++;
		return ;
	}
	for(int i=0;i<4;i++){
		int xx=x+dir[i][0];
		int yy=y+dir[i][1];
		if(!vis[xx][yy]&&xx>=1&&xx<=n&&yy>=1&&yy<=m&&!mp[xx][yy]){
			vis[x][y]=1;
			dfs(xx,yy);
			vis[x][y]=0;
		}
	}
}
int main(){
	cin>>n>>m>>t;	
	cin>>sx>>sy>>x2>>y2;
	for(int i=0;i<t;i++){
		int zx,zy;
		cin>>zx>>zy;
		mp[zx][zy]=1;
	}
	dfs(sx,sy);
	printf("%d\n",total);
} 

1.2.2 广度优先遍历

1.无向图的广度优先遍历

从A开始,有4个邻接点,“B,C,D,F”,这是第二层;

在分别从B,C,D,F开始找他们的邻接点,为第三层。以此类推。

因此访问顺序是:A -> B -> C -> D -> F -> G -> E -> H

2.有向图的广度优先遍历


与无向图类似

因此访问顺序是:A -> B -> C -> F -> D -> H -> E -> G

3.广度遍历代码

/* 邻接矩阵的广度优先遍历 */
void BFSTraverse(MGraph* G)
{
    int i = 0, j = 0;
    queue<int> myqueue;
 
    /* 初始化,把每一个定点都设为未访问过 */
    for(i = 0; i < G->numVertexes; i++)
    {
        visited[i] = 0;
    }
 
    /* 对每一个定点做循环 */
    for(i = 0; i < G->numVertexes; i++)
    {
        /* 如果节点没有被访问过 */
        if(visited[i] == 0)
        {
            /* 该节点设置为已经被访问 */
            visited[i] = 1;
 
            /* 打印出该节点,并把该节点入队列 */
            cout << G->vexs[i] << " ";
            myqueue.push(i);
 
            /* 若当前的队列不为空 */
            while(!myqueue.empty())
            {
                i = myqueue.front();
                myqueue.pop();
 
                for(j = 0; j < G->numVertexes; j++)
                {
                    /* 判断其他定点若与当前的定点存在边且未访问过 */
                    if(G->arc[i][j] != 65536 && visited[j] == 0)
                    {
                        visited[j] = 1;
                        cout << G->vexs[j] << " ";
                        myqueue.push(j);
                    }
                }
            }
        }
    }
 
}

4.广度遍历适用哪些问题的求解。(可百度搜索)

(1)最短路径

问题:求不带权连通图G中从顶点u到顶点v的一条最短路径。

测试用图结构:

#include <stdio.h>
#include <malloc.h>
#include "graph.h"
typedef struct
{
    int data;                   //顶点编号
    int parent;                 //前一个顶点的位置
} QUERE;                        //非环形队列类型
void ShortPath(ALGraph *G,int u,int v)
{
    //输出从顶点u到顶点v的最短逆路径
    ArcNode *p;
    int w,i;
    QUERE qu[MAXV];             //非环形队列
    int front=-1,rear=-1;       //队列的头、尾指针
    int visited[MAXV];
    for (i=0; i<G->n; i++)      //访问标记置初值0
        visited[i]=0;
    rear++;                     //顶点u进队
    qu[rear].data=u;
    qu[rear].parent=-1;
    visited[u]=1;
    while (front!=rear)         //队不空循环
    {
        front++;                //出队顶点w
        w=qu[front].data;
        if (w==v)               //找到v时输出路径之逆并退出
        {
            i=front;            //通过队列输出逆路径
            while (qu[i].parent!=-1)
            {
                printf("%2d ",qu[i].data);
                i=qu[i].parent;
            }
            printf("%2d\n",qu[i].data);
            break;
        }
        p=G->adjlist[w].firstarc;   //找w的第一个邻接点
        while (p!=NULL)
        {
            if (visited[p->adjvex]==0)
            {
                visited[p->adjvex]=1;
                rear++;             //将w的未访问过的邻接点进队
                qu[rear].data=p->adjvex;
                qu[rear].parent=front;
            }
            p=p->nextarc;           //找w的下一个邻接点
        }
    }
}
int main()
{
    ALGraph *G;
    int A[9][9]=
    {
        {0,1,1,0,0,0,0,0,0},
        {0,0,0,1,1,0,0,0,0},
        {0,0,0,0,1,1,0,0,0},
        {0,0,0,0,0,0,1,0,0},
        {0,0,0,0,0,1,1,0,0},
        {0,0,0,0,0,0,0,1,0},
        {0,0,0,0,0,0,0,1,1},
        {0,0,0,0,0,0,0,0,1},
        {0,0,0,0,0,0,0,0,0}
    };  //请画出对应的有向图
    ArrayToList(A[0], 9, G);
    ShortPath(G,0,7);
    return 0;
}
(2)最远顶点

问题:求不带权连通图G中,距离顶点v最远的顶点k

测试用图结构:

#include <stdio.h>
#include <malloc.h>
#include "graph.h"
int Maxdist(ALGraph *G,int v)
{
    ArcNode *p;
    int i,j,k;
    int Qu[MAXV];               //环形队列
    int visited[MAXV];              //访问标记数组
    int front=0,rear=0;             //队列的头、尾指针
    for (i=0; i<G->n; i++)          //初始化访问标志数组
        visited[i]=0;
    rear++;
    Qu[rear]=v;                 //顶点v进队
    visited[v]=1;               //标记v已访问
    while (rear!=front)
    {
        front=(front+1)%MAXV;
        k=Qu[front];                //顶点k出队
        p=G->adjlist[k].firstarc;       //找第一个邻接点
        while (p!=NULL)             //所有未访问过的相邻点进队
        {
            j=p->adjvex;            //邻接点为顶点j
            if (visited[j]==0)          //若j未访问过
            {
                visited[j]=1;
                rear=(rear+1)%MAXV;
                Qu[rear]=j; //进队
            }
            p=p->nextarc;           //找下一个邻接点
        }
    }
    return k;
}
int main()
{
    ALGraph *G;
    int A[9][9]=
    {
        {0,1,1,0,0,0,0,0,0},
        {0,0,0,1,1,0,0,0,0},
        {0,0,0,0,1,1,0,0,0},
        {0,0,0,0,0,0,1,0,0},
        {0,0,0,0,0,1,1,0,0},
        {0,0,0,0,0,0,0,1,0},
        {0,0,0,0,0,0,0,1,1},
        {0,0,0,0,0,0,0,0,1},
        {0,0,0,0,0,0,0,0,0}
    };  //请画出对应的有向图
    ArrayToList(A[0], 9, G);
    printf("离顶点0最远的顶点:%d",Maxdist(G,0));
    return 0;
}

1.3 最小生成树

在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即),而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集(即)且为无循环图,使得
的 w(T) 最小,则此 T 为 G 的最小生成树。

最小生成树其实是最小权重生成树的简称

在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

1.3.1 Prim算法求最小生成树

此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息,

(1)采用的是邻接矩阵的方式存储图,代码如下

//进行prim算法实现,使用的邻接矩阵的方法实现。
void Prim(Graph g,int begin) {

    //close_edge这个数组记录到达某个顶点的各个边中的权重最大的那个边
    Assis_array *close_edge=new Assis_array[g.vexnum];

    int j;

    //进行close_edge的初始化,更加开始起点进行初始化
    for (j = 0; j < g.vexnum; j++) {
        if (j != begin - 1) {
            close_edge[j].start = begin-1;
            close_edge[j].end = j;
            close_edge[j].weight = g.arc[begin - 1][j];
        }
    }
    //把起点的close_edge中的值设置为-1,代表已经加入到集合U了
    close_edge[begin - 1].weight = -1;
    //访问剩下的顶点,并加入依次加入到集合U
    for (j = 1; j < g.vexnum; j++) {

        int min = INT_MAX;
        int k;
        int index;
        //寻找数组close_edge中权重最小的那个边
        for (k = 0; k < g.vexnum; k++) {
            if (close_edge[k].weight != -1) {  
                if (close_edge[k].weight < min) {
                    min = close_edge[k].weight;
                    index = k;
                }
            }
        }
        //将权重最小的那条边的终点也加入到集合U
        close_edge[index].weight = -1;
        //输出对应的边的信息
        cout << g.information[close_edge[index].start] 
            << "-----" 
            << g.information[close_edge[index].end]
            << "="
            <<g.arc[close_edge[index].start][close_edge[index].end]
            <<endl;

        //更新我们的close_edge数组。
        for (k = 0; k < g.vexnum; k++) {
            if (g.arc[close_edge[index].end][k] <close_edge[k].weight) {
                close_edge[k].weight = g.arc[close_edge[index].end][k];
                close_edge[k].start = close_edge[index].end;
                close_edge[k].end = k;
            }
        }
    }
}

时间复杂度的分析:
其中我们建立邻接矩阵需要的时间复杂度为:O(nn),然后,我们Prim函数中生成最小生成树的时间复杂度为:O(nn).

(2)采用的是邻接表的方式存储图,代码如下

void Prim(Graph_List g, int begin) {
    cout << "图的最小生成树:" << endl;
    //close_edge这个数组记录到达某个顶点的各个边中的权重最大的那个边
    Assis_array *close_edge=new Assis_array[g.vexnum];
    int j;
    for (j = 0; j < g.vexnum; j++) {
        close_edge[j].weight = INT_MAX;
    }
    ArcNode * arc = g.node[begin - 1].firstarc;

    while (arc) {
        close_edge[arc->adjvex].end = arc->adjvex;
        close_edge[arc->adjvex].start = begin - 1;
        close_edge[arc->adjvex].weight = arc->weight;
        arc = arc->next;
    }
    //把起点的close_edge中的值设置为-1,代表已经加入到集合U了
    close_edge[begin - 1].weight = -1;
    //访问剩下的顶点,并加入依次加入到集合U
    for (j = 1; j < g.vexnum; j++) {
        int min = INT_MAX;
        int k;
        int index;
        //寻找数组close_edge中权重最小的那个边
        for (k = 0; k < g.vexnum; k++) {
            if (close_edge[k].weight != -1) {
                if (close_edge[k].weight < min) {
                    min = close_edge[k].weight;
                    index = k;
                }
            }
        }

        //输出对应的边的信息
        cout << g.node[close_edge[index].start].data
            << "-----"
            << g.node[close_edge[index].end].data
            << "="
            << close_edge[index].weight
            <<endl;
        //将权重最小的那条边的终点也加入到集合U
        close_edge[index].weight = -1;
        //更新我们的close_edge数组。            
        ArcNode * temp = g.node[close_edge[index].end].firstarc;
        while (temp) {
            if (close_edge[temp->adjvex].weight > temp->weight) {
                close_edge[temp->adjvex].weight = temp->weight;
                close_edge[temp->adjvex].start = index;
                close_edge[temp->adjvex].end = temp->adjvex;
            }
            temp = temp->next;
        }
    }

}

时间复杂分析:
在建立图的时候的时间复杂为:O(n+e),在执行Prim算法的时间复杂还是:O(n*n),总体来说还是邻接表的效率会比较高,因为虽然Prim算法的时间复杂度相同,但是邻接矩阵的那个常系数是比邻接表大的。

另外,Prim算法的时间复杂度都是和边无关的,都是O(n*n),所以它适合用于边稠密的网建立最小生成树。但是了,我们即将介绍的克鲁斯卡算法恰恰相反,它的时间复杂度为:O(eloge),其中e为边的条数,因此它相对Prim算法而言,更适用于边稀疏的网。

1.3.2 Kruskal算法求解最小生成树

此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

  1. 把图中的所有边按代价从小到大排序;
  2. 把图中的n个顶点看成独立的n棵树组成的森林;
  3. 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
  4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
void Kruskal() {
    int Vexnum = 0;
    int edge = 0;
    cout << "请输入图的顶点数和边数:" << endl;
    cin >> Vexnum >> edge;
    while (!check(Vexnum, edge)) {
        cout << "你输入的图的顶点数和边数不合法,请重新输入:" << endl;
        cin >> Vexnum >> edge;
    }

    //声明一个边集数组
    Edge * edge_tag;
    //输入每条边的信息
    createGraph(edge_tag, Vexnum, edge);

    int * parent = new int[Vexnum]; //记录每个顶点所在子树的根节点下标
    int * child = new int[Vexnum]; //记录每个顶点为根节点时,其有的孩子节点的个数
    int i;
    for (i = 0; i < Vexnum; i++) {
        parent[i] = i;
        child[i] = 0;
    }
    //对边集数组进行排序,按照权重从小到达排序
    qsort(edge_tag, edge, sizeof(Edge), cmp);
    int count_vex; //记录输出的边的条数

    count_vex = i = 0;
    while (i != edge) {
        //如果两颗树可以组合在一起,说明该边是生成树的一条边
        if (union_tree(edge_tag[i], parent, child)) {
            cout << ("v" + std::to_string(edge_tag[i].start))
                << "-----"
                << ("v" + std::to_string(edge_tag[i].end))
                <<"="
                << edge_tag[i].weight
                << endl;
            edge_tag[i].visit = true;
            ++count_vex; //生成树的边加1
        }
        //这里表示所有的边都已经加入成功
        if (count_vex == Vexnum - 1) {
            break;
        }
        ++i;
    }

    if (count_vex != Vexnum - 1) {
        cout << "此图为非连通图!无法构成最小生成树。" << endl;
    }
    delete [] edge_tag;
    delete [] parent;
    delete [] child;
}

Kruskal算法每次要从都要从剩余的边中选取一个最小的边。通常我们要先对边按权值从小到大排序,这一步的时间复杂度为为O(|Elog|E|)。Kruskal算法的实现通常使用并查集,来快速判断两个顶点是否属于同一个集合。最坏的情况可能要枚举完所有的边,此时要循环|E|次,所以这一步的时间复杂度为O(|E|α(V)),其中α为Ackermann函数,其增长非常慢,我们可以视为常数。所以Kruskal算法的时间复杂度为O(|Elog|E|)。

1.4 最短路径

1.4.1 Dijkstra算法求解最短路径

(1)算法特点:

迪科斯彻算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

(2)算法的思路:

Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

(3)Dijkstra算法示例演示


首先第一步,我们先声明一个dis数组,该数组初始化的值为:

顶点集T的初始化为:T={v1}

既然是求 v1顶点到其余各个顶点的最短路程,那就先找一个离 1 号顶点最近的顶点。通过数组 dis 可知当前离v1顶点最近是 v3顶点。当选择了 2 号顶点后,dis[2](下标从0开始)的值就已经从“估计值”变为了“确定值”,即 v1顶点到 v3顶点的最短路程就是当前 dis[2]值。将V3加入到T中。
为什么呢?因为目前离 v1顶点最近的是 v3顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得 v1顶点到 v3顶点的路程进一步缩短了。因为 v1顶点到其它顶点的路程肯定没有 v1到 v3顶点短.

OK,既然确定了一个顶点的最短路径,下面我们就要根据这个新入的顶点V3会有出度,发现以v3 为弧尾的有: < v3,v4 >,那么我们看看路径:v1–v3–v4的长度是否比v1–v4短,其实这个已经是很明显的了,因为dis[3]代表的就是v1–v4的长度为无穷大,而v1–v3–v4的长度为:10+50=60,所以更新dis[3]的值,得到如下结果:

因此 dis[3]要更新为 60。这个过程有个专业术语叫做“松弛”。即 v1顶点到 v4顶点的路程即 dis[3],通过 < v3,v4> 这条边松弛成功。这便是 Dijkstra 算法的主要思想:通过“边”来松弛v1顶点到其余各个顶点的路程。

然后,我们又从除dis[2]和dis[0]外的其他值中寻找最小值,发现dis[4]的值最小,通过之前是解释的原理,可以知道v1到v5的最短距离就是dis[4]的值,然后,我们把v5加入到集合T中,然后,考虑v5的出度是否会影响我们的数组dis的值,v5有两条出度:< v5,v4>和 < v5,v6>,然后我们发现:v1–v5–v4的长度为:50,而dis[3]的值为60,所以我们要更新dis[3]的值.另外,v1-v5-v6的长度为:90,而dis[5]为100,所以我们需要更新dis[5]的值。更新后的dis数组如下图:

然后,继续从dis中选择未确定的顶点的值中选择一个最小的值,发现dis[3]的值是最小的,所以把v4加入到集合T中,此时集合T={v1,v3,v5,v4},然后,考虑v4的出度是否会影响我们的数组dis的值,v4有一条出度:< v4,v6>,然后我们发现:v1–v5–v4–v6的长度为:60,而dis[5]的值为90,所以我们要更新dis[5]的值,更新后的dis数组如下图:

然后,我们使用同样原理,分别确定了v6和v2的最短路径,最后dis的数组的值如下:

因此,从图中,我们可以发现v1-v2的值为:∞,代表没有路径从v1到达v2。所以我们得到的最后的结果为:

起点 终点 最短路径 长度
v1 v2 无 ∞
v3 {v1,v3} 10
v4 {v1,v5,v4} 50
v5 {v1,v5} 30
v6 {v1,v5,v4,v6} 60

(4)代码

#include<iostream>
#include<string>
using namespace std;

/*
本程序是使用Dijkstra算法实现求解最短路径的问题
采用的邻接矩阵来存储图
*/
//记录起点到每个顶点的最短路径的信息
struct Dis {
    string path;
    int value;
    bool visit;
    Dis() {
        visit = false;
        value = 0;
        path = "";
    }
};

class Graph_DG {
private:
    int vexnum;   //图的顶点个数
    int edge;     //图的边数
    int **arc;   //邻接矩阵
    Dis * dis;   //记录各个顶点最短路径的信息
public:
    //构造函数
    Graph_DG(int vexnum, int edge);
    //析构函数
    ~Graph_DG();
    // 判断我们每次输入的的边的信息是否合法
    //顶点从1开始编号
    bool check_edge_value(int start, int end, int weight);
    //创建图
    void createGraph();
    //打印邻接矩阵
    void print();
    //求最短路径
    void Dijkstra(int begin);
    //打印最短路径
    void print_path(int);
};
#include"Dijkstra.h"

//构造函数
Graph_DG::Graph_DG(int vexnum, int edge) {
    //初始化顶点数和边数
    this->vexnum = vexnum;
    this->edge = edge;
    //为邻接矩阵开辟空间和赋初值
    arc = new int*[this->vexnum];
    dis = new Dis[this->vexnum];
    for (int i = 0; i < this->vexnum; i++) {
        arc[i] = new int[this->vexnum];
        for (int k = 0; k < this->vexnum; k++) {
            //邻接矩阵初始化为无穷大
                arc[i][k] = INT_MAX;
        }
    }
}
//析构函数
Graph_DG::~Graph_DG() {
    delete[] dis;
    for (int i = 0; i < this->vexnum; i++) {
        delete this->arc[i];
    }
    delete arc;
}

// 判断我们每次输入的的边的信息是否合法
//顶点从1开始编号
bool Graph_DG::check_edge_value(int start, int end, int weight) {
    if (start<1 || end<1 || start>vexnum || end>vexnum || weight < 0) {
        return false;
    }
    return true;
}

void Graph_DG::createGraph() {
    cout << "请输入每条边的起点和终点(顶点编号从1开始)以及其权重" << endl;
    int start;
    int end;
    int weight;
    int count = 0;
    while (count != this->edge) {
        cin >> start >> end >> weight;
        //首先判断边的信息是否合法
        while (!this->check_edge_value(start, end, weight)) {
            cout << "输入的边的信息不合法,请重新输入" << endl;
            cin >> start >> end >> weight;
        }
        //对邻接矩阵对应上的点赋值
        arc[start - 1][end - 1] = weight;
        //无向图添加上这行代码
        //arc[end - 1][start - 1] = weight;
        ++count;
    }
}

void Graph_DG::print() {
    cout << "图的邻接矩阵为:" << endl;
    int count_row = 0; //打印行的标签
    int count_col = 0; //打印列的标签
    //开始打印
    while (count_row != this->vexnum) {
        count_col = 0;
        while (count_col != this->vexnum) {
            if (arc[count_row][count_col] == INT_MAX)
                cout << "∞" << " ";
            else
            cout << arc[count_row][count_col] << " ";
            ++count_col;
        }
        cout << endl;
        ++count_row;
    }
}
void Graph_DG::Dijkstra(int begin){
    //首先初始化我们的dis数组
    int i;
    for (i = 0; i < this->vexnum; i++) {
        //设置当前的路径
        dis[i].path = "v" + to_string(begin) + "-->v" + to_string(i + 1);
        dis[i].value = arc[begin - 1][i];
    }
    //设置起点的到起点的路径为0
    dis[begin - 1].value = 0;
    dis[begin - 1].visit = true;

    int count = 1;
    //计算剩余的顶点的最短路径(剩余this->vexnum-1个顶点)
    while (count != this->vexnum) {
        //temp用于保存当前dis数组中最小的那个下标
        //min记录的当前的最小值
        int temp=0;
        int min = INT_MAX;
        for (i = 0; i < this->vexnum; i++) {
            if (!dis[i].visit && dis[i].value<min) {
                min = dis[i].value;
                temp = i;
            }
        }
        //cout << temp + 1 << "  "<<min << endl;
        //把temp对应的顶点加入到已经找到的最短路径的集合中
        dis[temp].visit = true;
        ++count;
        for (i = 0; i < this->vexnum; i++) {
            //注意这里的条件arc[temp][i]!=INT_MAX必须加,不然会出现溢出,从而造成程序异常
            if (!dis[i].visit && arc[temp][i]!=INT_MAX && (dis[temp].value + arc[temp][i]) < dis[i].value) {
                //如果新得到的边可以影响其他为访问的顶点,那就就更新它的最短路径和长度
                dis[i].value = dis[temp].value + arc[temp][i];
                dis[i].path = dis[temp].path + "-->v" + to_string(i + 1);
            }
        }
    }

}
void Graph_DG::print_path(int begin) {
    string str;
    str = "v" + to_string(begin);
    cout << "以"<<str<<"为起点的图的最短路径为:" << endl;
    for (int i = 0; i != this->vexnum; i++) {
        if(dis[i].value!=INT_MAX)
        cout << dis[i].path << "=" << dis[i].value << endl;
        else {
            cout << dis[i].path << "是无最短路径的" << endl;
        }
    }
}
#include"Dijkstra.h"


//检验输入边数和顶点数的值是否有效,可以自己推算为啥:
//顶点数和边数的关系是:((Vexnum*(Vexnum - 1)) / 2) < edge
bool check(int Vexnum, int edge) {
    if (Vexnum <= 0 || edge <= 0 || ((Vexnum*(Vexnum - 1)) / 2) < edge)
        return false;
    return true;
}
int main() {
    int vexnum; int edge;

    cout << "输入图的顶点个数和边的条数:" << endl;
    cin >> vexnum >> edge;
    while (!check(vexnum, edge)) {
        cout << "输入的数值不合法,请重新输入" << endl;
        cin >> vexnum >> edge;
    }
    Graph_DG graph(vexnum, edge);
    graph.createGraph();
    graph.print();
    graph.Dijkstra(1);
    graph.print_path(1);
    system("pause");
    return 0;
}

1.4.2 Floyd算法求解最短路径

(1)算法介绍

Floyd算法(Floyd-Warshall algorithm)又称为弗洛伊德算法、插点法,是解决给定的加权图中顶点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。
适用范围:无负权回路即可,边权可正可负,运行一次算法即可求得任意两点间最短路。

优缺点:

Floyd算法适用于APSP(AllPairsShortestPaths),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法。

优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单

缺点:时间复杂度比较高,不适合计算大量数据。

时间复杂度:O(n3);空间复杂度:O(n2);

任意节点i到j的最短路径两种可能:

直接从i到j;
从i经过若干个节点k到j。
map(i,j)表示节点i到j最短路径的距离,对于每一个节点k,检查map(i,k)+map(k,j)小于map(i,j),如果成立,map(i,j) = map(i,k)+map(k,j);遍历每个k,每次更新的是除第k行和第k列的数。

(2)算法思路

通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入两个矩阵,矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。矩阵P中的元素b[i][j],表示顶点i到顶点j经过了b[i][j]记录的值所表示的顶点。

假设图G中顶点个数为N,则需要对矩阵D和矩阵P进行N次更新。初始时,矩阵D中顶点a[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则a[i][j]=∞,矩阵P的值为顶点b[i][j]的j的值。 接下来开始,对矩阵D进行N次更新。第1次更新时,如果”a[i][j]的距离” > “a[i][0]+a[0][j]”(a[i][0]+a[0][j]表示”i与j之间经过第1个顶点的距离”),则更新a[i][j]为”a[i][0]+a[0][j]”,更新b[i][j]=b[i][0]。 同理,第k次更新时,如果”a[i][j]的距离” > “a[i][k-1]+a[k-1][j]”,则更新a[i][j]为”a[i][k-1]+a[k-1][j]”,b[i][j]=b[i][k-1]。更新N次之后,操作完成!

(3)Floyd算法的实例过程

第一步,我们先初始化两个矩阵,得到下图两个矩阵:

第二步,以v1为中阶,更新两个矩阵:
发现,a[1][0]+a[0][6] < a[1][6] 和a[6][0]+a[0][1] < a[6][1],所以我们只需要矩阵D和矩阵P,结果如下:

通过矩阵P,我发现v2–v7的最短路径是:v2–v1–v7

第三步:以v2作为中介,来更新我们的两个矩阵,使用同样的原理,扫描整个矩阵,得到如下图的结果:

(4)算法代码

#include<iostream>
#include<string>
using namespace std;

class Graph_DG {
private:
    int vexnum;   //图的顶点个数
    int edge;     //图的边数
    int **arc;   //邻接矩阵
    int ** dis;   //记录各个顶点最短路径的信息
    int ** path;  //记录各个最短路径的信息
public:
    //构造函数
    Graph_DG(int vexnum, int edge);
    //析构函数
    ~Graph_DG();
    // 判断我们每次输入的的边的信息是否合法
    //顶点从1开始编号
    bool check_edge_value(int start, int end, int weight);
    //创建图
    void createGraph(int);
    //打印邻接矩阵
    void print();
    //求最短路径
    void Floyd();
    //打印最短路径
    void print_path();
};
#include"Floyd.h"


//构造函数
Graph_DG::Graph_DG(int vexnum, int edge) {
    //初始化顶点数和边数
    this->vexnum = vexnum;
    this->edge = edge;
    //为邻接矩阵开辟空间和赋初值
    arc = new int*[this->vexnum];
    dis = new int*[this->vexnum];
    path = new int*[this->vexnum];
    for (int i = 0; i < this->vexnum; i++) {
        arc[i] = new int[this->vexnum];
        dis[i] = new int[this->vexnum];
        path[i] = new int[this->vexnum];
        for (int k = 0; k < this->vexnum; k++) {
            //邻接矩阵初始化为无穷大
            arc[i][k] = INT_MAX;
        }
    }
}
//析构函数
Graph_DG::~Graph_DG() {

    for (int i = 0; i < this->vexnum; i++) {
        delete this->arc[i];
        delete this->dis[i];
        delete this->path[i];

    }
    delete dis;
    delete arc;
    delete path;
}

// 判断我们每次输入的的边的信息是否合法
//顶点从1开始编号
bool Graph_DG::check_edge_value(int start, int end, int weight) {
    if (start<1 || end<1 || start>vexnum || end>vexnum || weight < 0) {
        return false;
    }
    return true;
}

void Graph_DG::createGraph(int kind) {
    cout << "请输入每条边的起点和终点(顶点编号从1开始)以及其权重" << endl;
    int start;
    int end;
    int weight;
    int count = 0;
    while (count != this->edge) {
        cin >> start >> end >> weight;
        //首先判断边的信息是否合法
        while (!this->check_edge_value(start, end, weight)) {
            cout << "输入的边的信息不合法,请重新输入" << endl;
            cin >> start >> end >> weight;
        }
        //对邻接矩阵对应上的点赋值
        arc[start - 1][end - 1] = weight;
        //无向图添加上这行代码
        if(kind==2)
        arc[end - 1][start - 1] = weight;
        ++count;
    }
}

void Graph_DG::print() {
    cout << "图的邻接矩阵为:" << endl;
    int count_row = 0; //打印行的标签
    int count_col = 0; //打印列的标签
                       //开始打印
    while (count_row != this->vexnum) {
        count_col = 0;
        while (count_col != this->vexnum) {
            if (arc[count_row][count_col] == INT_MAX)
                cout << "∞" << " ";
            else
                cout << arc[count_row][count_col] << " ";
            ++count_col;
        }
        cout << endl;
        ++count_row;
    }
}

void Graph_DG::Floyd() {
    int row = 0;
    int col = 0;
    for (row = 0; row < this->vexnum; row++) {
        for (col = 0; col < this->vexnum; col++) {
            //把矩阵D初始化为邻接矩阵的值
            this->dis[row][col] = this->arc[row][col];
            //矩阵P的初值则为各个边的终点顶点的下标
            this->path[row][col] = col;
        }
    }

    //三重循环,用于计算每个点对的最短路径
    int temp = 0;
    int select = 0;
    for (temp = 0; temp < this->vexnum; temp++) {
        for (row = 0; row < this->vexnum; row++) {
            for (col = 0; col < this->vexnum; col++) {
                //为了防止溢出,所以需要引入一个select值
                select = (dis[row][temp] == INT_MAX || dis[temp][col] == INT_MAX) ? INT_MAX : (dis[row][temp] + dis[temp][col]);
                if (this->dis[row][col] > select) {
                    //更新我们的D矩阵
                    this->dis[row][col] = select;
                    //更新我们的P矩阵
                    this->path[row][col] = this->path[row][temp];
                }
            }
        }
    }
}

void Graph_DG::print_path() {
    cout << "各个顶点对的最短路径:" << endl;
    int row = 0;
    int col = 0;
    int temp = 0;
    for (row = 0; row < this->vexnum; row++) {
        for (col = row + 1; col < this->vexnum; col++) {
            cout << "v" << to_string(row + 1) << "---" << "v" << to_string(col+1) << " weight: "
                << this->dis[row][col] << " path: " << " v" << to_string(row + 1);
            temp = path[row][col];
            //循环输出途径的每条路径。
            while (temp != col) {
                cout << "-->" << "v" << to_string(temp + 1);
                temp = path[temp][col];
            }
            cout << "-->" << "v" << to_string(col + 1) << endl;
        }

        cout << endl;
    }
}
#include"Floyd.h"


//检验输入边数和顶点数的值是否有效,可以自己推算为啥:
//顶点数和边数的关系是:((Vexnum*(Vexnum - 1)) / 2) < edge
bool check(int Vexnum, int edge) {
    if (Vexnum <= 0 || edge <= 0 || ((Vexnum*(Vexnum - 1)) / 2) < edge)
        return false;
    return true;
}
int main() {
    int vexnum; int edge;
    cout << "输入图的种类:1代表有向图,2代表无向图" << endl;
    int kind;
    cin >> kind;
    //判读输入的kind是否合法
    while (1) {
        if (kind == 1 || kind == 2) {
            break;
        }
        else {
            cout << "输入的图的种类编号不合法,请重新输入:1代表有向图,2代表无向图" << endl;
            cin >> kind;
        }
    }

    cout << "输入图的顶点个数和边的条数:" << endl;
    cin >> vexnum >> edge;
    while (!check(vexnum, edge)) {
        cout << "输入的数值不合法,请重新输入" << endl;
        cin >> vexnum >> edge;
    }
    Graph_DG graph(vexnum, edge);
    graph.createGraph(kind);
    graph.print();
    graph.Floyd();
    graph.print_path();
    system("pause");
    return 0;
}

1.5 拓扑排序

1.定义

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:

每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。

它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:

(1)从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
(2)从图中删除该顶点和所有以它为起点的有向边。
(3)重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。

于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。

通常,一个有向无环图可以有一个或多个拓扑排序序列。

2.拓扑排序的代码实现

采用邻接矩阵实现,map[i][j]=0,表示节点i和j没有关联;map[i][j]=1,表示存在边<i,j>,并且j的入度加1;

#include<iostream>
#include<stdlib.h>
#include<stdio.h>
#define MAX 100
usingnamespace std;

void toposort(int map[MAX][MAX],int indegree[MAX],int n)
{
    int i,j,k;
    for(i=0;i<n;i++) //遍历n次
    {
        for(j=0;j<n;j++) //找出入度为0的节点
        {
            if(indegree[j]==0)
            {
                indegree[j]--;
                cout<<j<<endl;
                for(k=0;k<n;k++) //删除与该节点关联的边
                {
                    if(map[j][k]==1)
                    {
                        indegree[k]--;
                    }
                }
                break;
            }
        }
    }
}


int main(void)
{
    int n,m; //n:关联的边数,m:节点数
    while(scanf("%d %d",&n,&m)==2&&n!=0)
    {
        int i;
        int x,y;
        int map[MAX][MAX]; //邻接矩阵
        int indegree[MAX]; //入度
        memset(map,0,sizeof(map));
        memset(indegree,0,sizeof(indegree));
        for(i=0;i<n;i++)
        {
            scanf("%d %d",&x,&y);
            if(!map[x][y])
            {
                map[x][y]=1;
                indegree[y]++;
            }
        }
        toposort(map,indegree,m);
    }
    return0;
}

3.时间复杂度分析

取顶点的顺序不同会得到不同的拓扑排序序列,当然前提是该图存在多个拓扑排序序列。
由于输出每个顶点的同时还要删除以它为起点的边,故上述拓扑排序的时间复杂度为O(V+E)。

1.6 关键路径

(1)什么叫AOE-网?

在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。

·只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。
·只有在进入某点的各有向边所代表的活动都已结束,该顶点所代表的时事件才能发生。

(2)什么是关键路径概念?

关键路径是指设计中从输入到输出经过的延时最长的逻辑路径。优化关键路径是一种提高设计工作速度的有效方法。一般地,从输入到输出的延时取决于信号所经过的延时最大路径,而与其他延时小的路径无关。在优化设计过程中关键路径法可以反复使用,直到不可能减少关键路径延时为止。EDA工具中综合器及设计分析器通常都提供关键路径的信息以便设计者改进设计,提高速度。

(3)什么是关键活动?

关键活动是为准时完成项目而必须按时完成的活动。即处于关键路径上的活动。所有项目都是由一系列活动组成,而在这些活动中存在各种链接关系和活动约束。其中有些活动如果延误就会影响整个项目工期。在项目中总存在这样一类直接影响项目工期变化的活动,这些活动就是关键活动。

2.PTA实验作业(4分)

2.1 六度空间(2分)

“六度空间”理论又称作“六度分隔(Six Degrees of Separation)”理论。这个理论可以通俗地阐述为:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过五个人你就能够认识任何一个陌生人。”如图1所示。

“六度空间”理论虽然得到广泛的认同,并且正在得到越来越多的应用。但是数十年来,试图验证这个理论始终是许多社会学家努力追求的目标。然而由于历史的原因,这样的研究具有太大的局限性和困难。随着当代人的联络主要依赖于电话、短信、微信以及因特网上即时通信等工具,能够体现社交网络关系的一手数据已经逐渐使得“六度空间”理论的验证成为可能。

2.1.1 伪代码

(1)思路

本题需要输出距离不超过6的节点数占节点总数的百分比,显然需要用到广度优先遍历(BFS),即一层一层搜索,不断统计搜索到的节点数,同时用一个变量count记录访问的层数,当count=6时便可以退出遍历。

·对每个节点进行广度优先搜索
·搜索过程中累计访问的节点数
·需要记录层次,仅计算6层以内的节点数

采用邻接表的存储结构进行操作

采用BFS搜索,若用DFS会有一个测试点无法通过。该题不是把一整个图都遍历一遍,二十只遍历指定深度的点,用DFS会导致一些在指定深度范围内的点范围不到,遍历就结束了。

·在BFS中需要知道遍历到每层最后一个结点并层数+1

引入2个变量 last tail 分别指向 当前层数的最后一个元素 和 一层的最后一个元素 ,tail变量记录每层入队时的结点,last变量记录每层最后一个元素且在该层入队后出来更新last=temp

(2)伪代码

int main()
{
	for i=1 to e
	 then
		输入n1,n2
		edgex[n1][n2]=1
		edgex[n2][n1]=1
	for i=1 to n
	 then
		for j=1 to n
		 then
			visited[j]=0
		 end
		node=BFS(i)//广度优先遍历
		result=100.00*node/n
		printf()
	end
}
int BFS(int v)
{
	while !qu.empty()且level<6
	 then
		temp=qu.front()
		qu.pop()
		for i=0 to n
		 then
			if visited[i]等于0且edges[temp][i]等于1
				count加一
				qu.push(i)
				visited[i]=1
				tail=i
		end
		if last等于temp
			level加一
			last=tail
		end
		return count

}
#include<iostream>
#include<stdlib.h>
#include<stdio.h>
#include<queue>
#define maxv 10001
using namespace std;
int n, e;
int visited[maxv];
int edgex[maxv][maxv];
int BFS(int v);
int main()
{
	int i, j;
	cin >> n >> e;
	int n1, n2;
	int node;
	double result;
	for (i = 1; i <= e; i++)
	{
		cin >> n1 >> n2;
		edgex[n1][n2] = 1;
		edgex[n2][n1] = 1;

	}
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
			visited[j] = 0;
		node = BFS(i);//广度优先遍历
		result = 100.00 * node / n;
		printf("%d: %.2lf%%\n", i, result);
	}
	return 0;
}
int BFS(int v)
{
	int level = 0;//每个结点与该结点的距离
	int count = 1; //每个结点输出与该结点距离不超过6的结点数
	int last = v;
	int tail, temp;
	int i;
	queue<int>qu;
	qu.push(v);
	visited[v] = 1;
	while (!qu.empty() && level < 6)
	{
		temp = qu.front();
		qu.pop();
		for (i = 1; i <= n; i++)
		{
			if ( visited[i] == 0 &&edgex[temp][i] == 1)//相邻
			{
				count++;
				qu.push(i);
				visited[i] = 1;
				tail = i;
			}
		}
		if (last == temp)
		{
			level++;
			last = tail;
		}
	}
	return count;
}

2.1.2 提交列表

2.1.3 本题知识点

此题有点的像树的层次遍历,使用queue辅助搜索,visite[i]标记已经认识过的人,
利用邻接矩阵进行广度遍历,通过广度遍历进行层数的判断,需要引入last和tail进行结点访问的层数判断以及结点层数的改变,通过比较last可以判断层数是否需要改变,并及时返回数量。把所有人都认识完或者已经达到第六层(最多通过五个人认识)就可以结束搜索。

2.2 村村通(2分)

2.2.1 伪代码(贴代码,本题0分)

(1)解题思路:

本题使用Prim算法思想,找到最小值,但这题用图存数据然后运用prim算法会很容易超时,所以通过输入数据,找到合适的化简方法,通过输入的路径权重升序排序,然后每次遍历这组数据,找到最短的一条路径满足一个点已经被读取,另一个点没被读取,然后这条路径就是要找的,然后把未读取的点标记为已读取,并sum+=该路径权重,然后重新循环,若中途有一次遍历完这组数据都找不到该路径,则该图应该不是连通图,返回-1,结束函数

(2)伪代码

void HVV()
{
	for i=0 to M
	 then
		输入abc
		X[i].E=a
		X[i].S=b
		X[i].value=c
	end
	sort(X,X+M,BJ)
}
int Lowcost()
{
	x[X[0].E]=1//把最小的路径的两个点都标记并且sum+=该路径权值
	x[X[0].S]=1
	sum+=X[0].value
	for i=2 to N
		for j=1 to M
		 then
			if x[X[j].E]且!x[X[j].S]//一个点标记一个点没标记
				sum+=X[j].value
				x[X[j].S]=1
				break
			else if !x[X[j].E]且x[X[j]].S//一个点标记一个点没标记
				sum+=X[j].value
				x[X[j].E]=1
				break
		end
		if j等于M
			return -1
		end
	return sum
}
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
typedef struct Node {
	int S;
	int E;
	int value;
	
}Road;
bool BJ(Road m,Road n){//比较函数
	if (m.value < n.value)
		return true;
	return false;
}
Road X[3001];
int N, M;
void HVV();
int Lowcost();
int main()
{
	cin >> N >> M;
	HVV();
	cout << Lowcost() << flush;
	return 0;
}
void HVV()
{
	int i, a, b, c;
	for (i = 0; i < M; i++) {
		cin >> a >> b >> c;
		X[i].E = a;
		X[i].S = b;
		X[i].value = c;
	}
	sort(X,X+M,BJ);//升序排序
}
int Lowcost()
{
	int x[1001] = { 0 };
	int sum = 0;//记录最低费用
	x[X[0].E] = 1;//把最小的路径的两个点都标记并且sum+=该路径权值
	x[X[0].S] = 1;
	sum += X[0].value;
	int i, j;
	for (i = 2; i < N; i++) {
		for (j = 1; j < M; j++) {
			if (x[X[j].E] && !x[X[j].S]) {//一个点标记一个点没标记
				sum += X[j].value;
				x[X[j].S] = 1;
				break;
			}
			else if (!x[X[j].E] && x[X[j].S]) {//一个点标记一个点没标记
				sum += X[j].value;
				x[X[j].E] = 1;
				break;
			}
		}
		if (j == M)
			return -1;
	}
	return sum;
}

2.2.2 提交列表


2.2.3 本题知识点

(1)运用结构体
typedef struct Node
{
int S;
int E;
int value;

}Road;
(2)使用Prim算法思想,找到最小值
(3)通过输入的路径权重升序排序,遍历数据

posted on 2021-05-23 23:19  诗酒趁年华灬  阅读(91)  评论(1编辑  收藏  举报