作业(关于图的总结)

1.思维导图

(学艺不精见谅)

 

 

 

2.具体内容

 

(1).图的定义及相关概念

  • 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其 中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

       注意:线性表中可以没有元素,称为空表。树中可以没有结点,叫做空树。但是在图中不允许 没有顶点,可以没有边。

  • 无向边:若顶点Vi和Vj之间的边没有方向,称这条边为无向边(Edge),用(Vi,Vj)来表示。
  • 无向图(Undirected graphs):图中任意两个顶点的边都是无向边。
  • 有向边:若从顶点Vi到Vj的边有方向,称这条边为有向边,也称为弧(Arc),用<Vi, Vj>来表示,其中Vi称为弧尾(Tail),Vj称为弧头(Head)。
  • 有向图(Directed graphs):图中任意两个顶点的边都是有向边。
  • 简单图:不存在自环(顶点到其自身的边)和重边(完全相同的边)的图
  • 无向完全图:无向图中,任意两个顶点之间都存在边。
  • 有向完全图:有向图中,任意两个顶点之间都存在方向相反的两条弧。
  • 稀疏图;有很少条边或弧的图称为稀疏图,反之称为稠密图。
  • 权:表示从图中一个顶点到另一个顶点的距离或耗费。
  • 网:带有权重的图
  • 度:与特定顶点相连接的边数;
  • 出度、入度:有向图中的概念,出度表示以此顶点为起点的边的数目,入度表示以此顶点为终点的边的数目;
  • 环:第一个顶点和最后一个顶点相同的路径;
  • 简单环:除去第一个顶点和最后一个顶点后没有重复顶点的环;
  • 连通图:任意两个顶点都相互连通的图;
  • 极大连通子图:包含竟可能多的顶点(必须是连通的),即找不到另外一个顶点,使得此顶点能够连接到此极大连通子图的任意一个顶点;
  • 连通分量:极大连通子图的数量;
  • 强连通图:此为有向图的概念,表示任意两个顶点a,b,使得a能够连接到b,b也能连接到a 的图;
  • 生成树:n个顶点,n-1条边,并且保证n个顶点相互连通(不存在环);
  • 最小生成树:此生成树的边的权重之和是所有生成树中最小的;
  • AOV网:在有向图中若以顶点表示活动,有向边表示活动之间的先后关系
  • AOE网:在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间

 

 

(2)图的存储结构

2.1、邻接矩阵

定义:图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称邻接矩阵)存储图中的边或弧的信息。

无向图由于边不区分方向,所以其邻接矩阵是一个对称矩阵。邻接矩阵中的0表示边不存在,主对角线全为0表示图中不存在自环。

在带权有向图的邻接矩阵中,数字表示权值weight,「无穷」表示弧不存在。由于权值可能为0,所以不能像在无向图的邻接矩阵中那样使用0来表示弧不存在。

void CreateMGraph(MGraph* G)
{
    int i, j, k, w;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d", &G->numVertexes, &G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes; i++)    //读入顶点信息,建立顶点表
        scanf("%c", &G->vers[i]);
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes;i++)
        for (j = 0; j < G->numVertexes;j++)
            G->arc[i][j] = INFINITY;    //邻接矩阵初始化

    for (k = 0; k < G->numEdges;k++)    //读入numEdges条边,建立邻接矩阵
    {
        printf("input edge(vi,vj) row(i),col(j),weight(w):\n");
        scanf("%d,%d,%d", &i, &j, &w);    //输入边(vi,vj),以及上面的权值
        getchar();    //可以获取回车符
        G->arc[i][j] = w;
        G->arc[j][i] = G->arc[i][j];    //因为是无向图,所有是对称矩阵
    }
}

优缺点:

  • 优点:结构简单,操作方便
  • 缺点:对于稀疏图,这种实现方式将浪费大量的空间。

2.2、邻接表

邻接表是一种将数组与链表相结合的存储方法。其具体实现为:将图中顶点用一个一维数组存储,每个顶点Vi的所有邻接点用一个单链表来存储。这种方式和树结构中孩子表示法一样。如图:

有向图的邻接表是以顶点为弧尾来存储边表的,这样很容易求一个顶点的出度(顶点对应单链表的长度),但若求一个顶点的入度,则需遍历整个图才行。这时可以建立一个有向图的逆邻接表即对每个顶点v都建立一个弧头尾v的单链表。如上图所示。

void CreateALGraph(GraphAdjList* G)
{
    int i, j ,k,w;
    EdgeNode *e;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d",&G->numVertexes,&G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes;i++)    //输入顶点信息
    {
        scanf("%c", &G->adjList[i].data);    //输入顶点信息
        G->adjList[i].firstedge = NULL;    //将边表置为空
    }
    getchar();    //可以获取回车符
    for (k = 0; k < G->numEdges;k++)
    {
        printf("input edge(vi,vj) vertexs series and the weight:\n");
        scanf("%d,%d,%d", &i, &j,&w);
        getchar();
        //由于是无向图,对称矩阵,当我们设置边以后,需要在两个地方设置结点
        e = (EdgeNode *)malloc(sizeof(EdgeNode));
        //使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
        e->adjvex = j;
        e->next = G->adjList[i].firstedge;    
        e->weight = w;
        G->adjList[i].firstedge = e;

        e = (EdgeNode*)malloc(sizeof(EdgeNode));
        e->adjvex = i;
        e->next = G->adjList[j].firstedge;
        e->weight = w;
        G->adjList[j].firstedge = e;
    }
}

2.3、十字链表

十字链表(Orthogonal List)是将邻接表和逆邻接表相结合的存储方法,它解决了邻接表(或逆邻接表)的缺陷,即求入度(或出度)时必须遍历整个图。

十字链表的结构如下:

图中:

  • firstIn表示入边表(即是逆邻接表中的单链表)头指针,firstOut表示出边表(即是邻接表中的单链表)头指针,data表示顶点数据。
  • tailVex表示边的起点在顶点数组中的下标,tailNext值出边表指针域,指向起点相同的下一条边。
  • headVex表示边的终点在顶点数组中的下标,headNext指入边表指针域,指向终点相同的下一条边。

 

 

 

(3)图的遍历

3.1、深度优先遍历

遍历思想:基本思想:首先从图中某个顶点v0出发,访问此顶点,然后依次从v相邻的顶点出发深度优先遍历,直至图中所有与v路径相通的顶点都被访问了;若此时尚有顶点未被访问,则从中选一个顶点作为起始点,重复上述过程,直到所有的顶点都被访问。

深度优先遍历用递归实现比较简单,只需用一个递归方法来遍历所有顶点,在访问某一个顶点时:

  • 将它标为已访问
  • 递归的访问它的所有未被标记过的邻接点

代码实现:

//深度优先遍历
int visitedDFS[MAXV] = { 0 };				//全局数组,记录是否遍历
void DFS(ListGraph* LG, int v) {
	EdgeNode* p;
	visitedDFS[v] = 1;						//记录已访问,置 1
	printf("%2d", v);						//输出顶点编号
	p = LG->adjList[v].firstEdge;			//p 指向顶点 v 的第一个邻接点
	while (p != NULL) {
		if (visitedDFS[p->adjVer] == 0 && p->weight != INF) {	
            //如果 p->adjVer 没被访问,递归访问它
			DFS(LG, p->adjVer);
		}
		p = p->nextEdge;		//p 指向顶点 v 的下一个邻接点
	}
}

3.2、广度优先遍历

遍历思想:

(1)从图中的某个初始点 v0 出发,首先访问初始点 v0。
(2)接着访问该顶点的所有未访问过的邻接点 v01 v02 v03 ……v0n。
(3)然后再选择 v01 v02 v03 ……v0n,访问它们的未被访问的邻接点,v010 v011 v012……v01n。
(4)直到所有与初始顶点 v 联通的顶点都被访问。

代码实现:

//广度优先遍历
void BFS(ListGraph* LG, int v) {
	int ver;														//定义出队顶点
	EdgeNode* p;
	SqQueue* sq;													//定义指针
	initQueue(sq);													//初始化队列
	int visitedBFS[MAXV] = { 0 };									//初始化访问标记数组
	enQueue(sq, v);													//初始点进队
	printf("%2d", v);
	visitedBFS[v] = 1;												//打印并标记要出队顶点													
	while (!emptyQueue(sq)) {										//队为空结束循环
		ver = deQueue(sq, v);										//出队,并得到出队信息
		p = LG->adjList[ver].firstEdge;								//指向出队的第一个邻接点
		while (p != NULL) {											//查找 ver 的所有邻接点
			if (visitedBFS[p->adjVer] == 0 && p->weight != INF) {	//如果没被访问
				printf("%2d", p->adjVer);							//打印该顶点信息
				visitedBFS[p->adjVer] = 1;							//置已访问状态
				enQueue(sq, p->adjVer);								//该顶点进队
			}
			p = p->nextEdge;										//找下一个邻接点
		}
	}
	printf("\n");
}

 

 

 

(4)最小生成树

图的生成树是它的一棵含有所有顶点的无环连通子图。一棵加权图的最小生成树(MST)是它的一棵权值(所有边的权值之和)最小的生成树。

通常来说,要解决最小生成树问题,通常采用两种算法:Prim算法Kruskal算法。先假设要求一个连通无向图G=(V, E)的最小生成树T,且以其中的一个顶点V1为T的根结点。下面就分别对这两种算法进行介绍。

4.1Prim算法

Prim算法构建最小生成树的过程是:先构建一棵只包含根结点V1的树A,然后每次在连接树A结点和图G中树A以外的结点的所有边中,选取一条权重最小的边加入树A,直至树A覆盖图G中的所有结点。

从顶点0开始,首先将顶点0加入到树中(标记),顶点0和其它点的横切边(这里即为顶点0的邻接边)加入优先队列,将权值最小的横切边出队,加入生成树中。此时相当于也向树中添加了一个顶点2,接着将集合(顶点1,2组成)和另一个集合(除1,2的顶点组成)间的横切边加入到优先队列中,如此这般,直到队列为空。

代码实现:

#define Maximum 1000
#define Biggest 100000000

typedef struct EdgeListNode{
    int adjId;
    int weight;
    EdgeListNode* next;
};

typedef struct VertexListNode{
    int data;
    EdgeListNode* firstadj;
};

typedef struct GraphAdjList{
    int vertexnumber;
    int edgenumber;
    VertexListNode vertextlist[Maximum];
};

typedef struct MiniTreeEdge {
    int s;
    int e;
    int weight;
    MiniTreeEdge *next;
};

typedef struct MiniTree {  //最小生成树
    MiniTreeEdge *head;  //指向最小生成树的根节点
    int vertextnumber;
};

void MiniSpanTree_Prim(GraphAdjList g, MiniTree tree, int start_node) {
    tree.head = NULL;
    int *distance = (int*)malloc(sizeof(int) * g.vertexnumber + 2);
    int *miniadj = (int*)malloc(sizeof(int) * g.vertexnumber + 2);
    int i, j, k, lastnode, thisnode;
    lastnode = start_node;
    for(i=1; i<=g.vertexnumber; i++) {
        distance[i] = Biggest;
        miniadj[i] = i;
    }

    distance[start_node] = 0;
    tree.vertextnumber = 1;

    while(tree.vertextnumber < g.vertexnumber) {
        EdgeListNode *temp = g.vertextlist[lastnode].firstadj;
        while(temp != NULL) {
            j = temp->adjId;
            if(distance[j] && distance[j]>temp->weight) {
                distance[j] = temp->weight;
                miniadj[j] = lastnode;
            }
            temp = temp->next;
        }

        k = Biggest;
        for(i=1; i<=g.vertexnumber; i++) {
            if(distance[i] && k>distance[i]) {
                k = distance[i];
                thisnode = i;
            }
        }

        MiniTreeEdge *temp1 = (MiniTreeEdge*)malloc(sizeof(MiniTreeEdge));
        temp1->e = thisnode; //新加入的结点
        temp1->s = miniadj[thisnode]; //最小生成树中与新加入结点相连的结点
        temp1->weight = k; //新加入的边的权重
        temp1->next = NULL;
        temp1->next = tree.head;
        tree.head = temp1;
        distance[thisnode] = 0;

        lastnode = thisnode;
        tree.vertextnumber++;
    }

	//打印最小生成树
    MiniTreeEdge *e = tree.head;
    while(e != NULL) {
        cout<<e->s<<" -> "<<e->e<<"  :  "<<e->weight<<endl;
        e = e->next;
    }

}

4.2、Kruskal算法

假设现在要求无向连通图G=(V, E)的最小生成树T,Kruskal算法的思想是令T的初始状态为|V|个结点而无边的非连通图,T中的每个顶点自成一个连通分量。接着,每次从图G中所有两个端点落在不同连通分量的边中,选取权重最小的那条,将该边加入T中,如此往复,直至T中所有顶点都在同一个连通分量上。

关键:(1)在生成最小生成树前,要对图中的所有边进行排序;
(2)如何判断一条边的两个端点是否落在不同的连通分量上:

#define Maximum 1000
#define Biggest 100000000

typedef struct EdgeListNode{
    int adjId;
    int weight;
    EdgeListNode* next;
};

typedef struct VertexListNode{
    int data;
    EdgeListNode* firstadj;
};

typedef struct GraphAdjList{
    int vertexnumber;
    int edgenumber;
    VertexListNode vertextlist[Maximum];
};

typedef struct MiniTreeEdge {
    int s;
    int e;
    int weight;
    MiniTreeEdge *next;
};

typedef struct MiniTree {
    MiniTreeEdge *head;
    int edgenumber;
};

typedef struct EdgeArrayData {
    int l;
    int r;
    int weight;
};

bool compare(EdgeArrayData a, EdgeArrayData b) {
    return a.weight < b.weight;
}

int find_parent(int node, int *parent) {
    while(parent[node] != node) {
        node = parent[node];
    }
    return node;
}

void  MiniSpanTree_Kruskal(GraphAdjList g, MiniTree *tree) {
    int i, j, k, edge_index, *parent;
    MiniTreeEdge *e;
    EdgeArrayData *edge = (EdgeArrayData*)malloc(sizeof(EdgeArrayData)*(g.edgenumber+2));
    parent = (int*)malloc(sizeof(int)*(g.vertexnumber+2));
    tree = (MiniTree*)malloc(sizeof(MiniTree));
    EdgeListNode *v;

	//将图中的每条边存储在edge里
    edge_index = 0;
    for(i=1; i<=g.vertexnumber; i++) {
        v = g.vertextlist[i].firstadj;
        parent[i] = i;
        while(v != NULL) {
            if(v->adjId > i) {  //为了避免将一条边存两次
                edge[edge_index].l = i;
                edge[edge_index].r = v->adjId;
                edge[edge_index].weight = v->weight;
                edge_index++;
            }
            v = v->next;
        }
    }
    sort(edge, edge+edge_index, compare); //将边按权重从小到大排序
    
    tree->edgenumber = 0;
    tree->head = NULL;
    for(i=0; i<edge_index; i++) {
        j = find_parent(edge[i].l, parent); 
        k = find_parent(edge[i].r, parent);
        if(j != k) {
            parent[j] = k;
            e = (MiniTreeEdge*)malloc(sizeof(MiniTreeEdge));
            e->s = edge[i].l;
            e->e = edge[i].r;
            e->weight = edge[i].weight;
            e->next = tree->head;
            tree->head = e;
            tree->edgenumber++;
        }
        if(tree->edgenumber == g.vertexnumber - 1) {
            break;
        }
    }

    MiniTreeEdge *ee = tree->head;
    while(ee != NULL) {
        cout<<ee->s<<" -> "<<ee->e<<"  :  "<<ee->weight<<endl;
        ee = ee->next;
    }
}

 

 

 

 

(5)最短路径

最短路径指两顶点之间经过的边上权值之和最少的路径,并且称路径上的第一个顶点为源点,最后一个顶点为终点。

5.1、Dijkstra算法

求单源最短路径,即求一个顶点到任意顶点的最短路径,其时间复杂度为O(n*n)

算法实现:


void Dijkstra(AMGraph g,int dist[],int path[],int v0){
	int n=g.vexnum,v;
	int set[n];//set数组用于记录该顶点是否归并 
	//第一步:初始化 
	for(int i=0;i<n;i++){
		set[i]=0;
		dist[i]=g.arcs[v0][i];
		if(dist[i]<MaxInt){//若距离小于MaxInt说明两点之间有路可通 
			path[i]=v0;//则更新路径i的前驱为v 
		}else{
			path[i]=-1; //表示这两点之间没有边
		 } 
	}
	set[v0]=1;//将初始顶点并入 
	path[v0]=-1;//初始顶点没有前驱
	
	//第二步 
	for(int i=1;i<n;i++){//共n-1个顶点 
		int min=MaxInt;
		//第二步:从i=1开始依次选一个距离顶点的最近顶点 
		for(int j=0;j<n;j++){
			if(set[j]==0&&dist[j]<min){
				v=j;
				min=dist[j];
		}
	}
	//将顶点并入 
	set[v]=1;	
	//第三步:在将新结点并入后,其初始顶点v0到各顶点的距离将会发生变化,所以需要更新dist[]数组
	for(int j=0;j<n;j++){
		if(set[j]==0&&dist[v]+g.arcs[v][j]<dist[j]){
			dist[j]=dist[v]+g.arcs[v][j];
			path[j]=v;
		}
	} 	
 } 

Dijkstra算法的局限性:图中边的权重必须为正,但可以是有环图。时间复杂度为O(elogn),空间复杂度O(n)

5.2、 Floyd算法

算法描述:

a.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。   

b.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

 

算法实现:


void Floyd(AMGraph g,int path[][MaxVexNum]){
	int n=g.vexnum;
	int A[n][n];
	//第一步:初始化path[][]和A[][]数组 
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			A[i][j]=g.arcs[i][j];
			path[i][j]=-1; 
		}
	}
	//第二步:三重循环,寻找最短路径 
	for(int v=0;v<n;v++){//第一层是代表中间结点 
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++){
				if(A[i][j]>A[i][v]+A[v][j]){
					A[i][j]=A[i][v]+A[v][j];
					path[i][j]=v;
				}
			}
		} 
	} 
} 

 

 

 

(6)拓扑排序

在一个有向图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。

先统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向的节点的入度减一。

一直做改操作,直到所有的节点都被分离出来。

如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是很多题目的无解的情况。

代码实现:

 //b[]为每个点的入度
for(i=1;i<=n;i++){
   for(j=1;j<=n;j++){
      if(b[j]==0){   //找到一个入度为0的点
        ans=j;
        vis[cnt++]=j;
        b[j]--;
        break;
       }
    }
    for(j=1;j<=n;j++)
        if(a[ans][j]) b[j]--; //与入度为0的点相连的点的入度减一
}
    printf("%d",vis[0]);
    for(i=1;i<cnt;i++) printf(" %d",vis[i]);
    printf("\n");

 

 

 

(7)关键路径

  • 关键路径:AOE-网中,从起点到终点最长的路径的长度(长度指的是路径上边的权重和)

7.1、AOV网介绍

AOV网:
顶点活动(Activity On Vertex,AOV)网是指用顶点表示活动,而用边集表示活动间优先关系的有向图。例如图10-57的先导课程示意图就是AOV网,其中图的顶点表示各项课程,也就是“活动”;有向边表示课程的先导关系,也就是“活动间的优先关系”。显然,图中不应当存在有向环,否则会让优先关系出现逻辑错误。

7.2、AOE网介绍
我们在学习拓扑排序的时候,已经接触了什么是AOV-网,AOV-网是优先考虑顶点的思路,而我们也同样可以优先考虑边,这个就是AOE-网的思路。

若在带权的有向无环图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向无环图称为AOE网。记住AOE-网只是比AOV-网多了一个边的权重,而且AOV-网一般是设计一个庞大的工程各个子工程实施的先后顺序,而我们的AOE-网就是不仅仅关系整个工程中各个子工程的实施的先后顺序,同时也关系整个工程完成最短时间。

因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。

AOE-网还有一个特点就是:只有一个起点(入度为0的顶点)和一个终点(出度为0的顶点),并且AOE-网有两个待研究的问题:

1.完成整个工程需要的时间
2.哪些活动是影响工程进度的关键

7.3求关键路径算法设计:
关键路径算法是一种典型的动态规划法,设图G=(V, E)是个AOE网,结点编号为1,2,...,n,其中结点1与n 分别为始点和终点,ak=<i, j>∈E是G的一个活动。算法关键是确定活动的最早发生时间ve[k]和最晚发生时间vl[k],进而获取顶点的最早开始时间e[k]和最晚开始时间l[k]。
 

代码设计:

//拓扑序列
 
stack<int>topOrder;
 
//拓扑排序,顺便求ve数组
 
bool topologicalSort()
 
{
 
    queue<int>q;
 
    for(int i=0;i<n;i++)
 
        if(inDegree[i]==0)
 
            q.push(i);
 
    while(!q.empty())
 
    {
 
        int u=q.front();
 
        q.pop();
 
        topOrder.push(u);//将u加入拓扑序列
 
        for(int i=0;i<G[u].size();i++)
 
        {
 
            int v=G[u][i].v;//u的i号后继结点编号为v
 
            inDegree[v]--;
 
            if(inpegree[v]==0)
 
                q.push(v);
 
            //用ve[u]来更新u的所有后继结点
 
            if(ve[u]+G[u][i].w> ve[v])
 
                ve[v]=ve[u]+G[u][i].w;
 
        }
 
    }
 
    if(toporder.size()== n)
 
        return true;
 
    else
 
        return false;
 
}

 

 

(8)关于图的疑难问题

1.题目:

现在你总共有 n 门课需要选,记为 0 到 n-1。 在选修某些课程之前需要一些先修课程。例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]  给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。 可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。 
 示例 1: 
 输入: 2, [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
123 
 示例 2: 
 输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
 

2.解题思路:

拓扑排序

  • 从 DAG 图中找出所有入度为0的顶点,放入队列。
  • 每次从队列取出一个结点,从图中删除该顶点以及所有以它为起点的有向边。
  • 每删除一条有向边,该边的终结点的入度-1,如果入度为0,将终结点加入队列。
  • 重复以上步骤,直到当前图中不存在无前驱的顶点。

3.代码实现:

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;

/**
 * 使用拓扑排序来完成
 */
public class Solution {

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // 先处理极端情况
        if (numCourses <= 0) {
            return new int[0];
        }
        // 邻接表表示
        HashSet<Integer>[] graph = new HashSet[numCourses];
        for (int i = 0; i < numCourses; i++) {
            graph[i] = new HashSet<>();
        }
        // 入度表
        int[] inDegree = new int[numCourses];
        // 遍历 prerequisites 的时候,把 邻接表 和 入度表 都填上
        for (int[] p : prerequisites) {
            graph[p[1]].add(p[0]);
            inDegree[p[0]]++;
        }
        LinkedList<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.addLast(i);
            }
        }
        ArrayList<Integer> res = new ArrayList<>();
        while (!queue.isEmpty()) {
            // 当前入度为 0 的结点
            Integer inDegreeNode = queue.removeFirst();
            // 加入结果集中
            res.add(inDegreeNode);
            // 下面从图中删去
            // 得到所有的后继课程,接下来把它们的入度全部减去 1
            HashSet<Integer> nextCourses = graph[inDegreeNode];
            for (Integer nextCourse : nextCourses) {
                inDegree[nextCourse]--;
                // 马上检测该结点的入度是否为 0,如果为 0,马上加入队列
                if (inDegree[nextCourse] == 0) {
                    queue.addLast(nextCourse);
                }
            }
        }
        // 如果结果集中的数量不等于结点的数量,就不能完成课程任务,这一点是拓扑排序的结论
        int resLen = res.size();
        if (resLen == numCourses) {
            int[] ret = new int[numCourses];
            for (int i = 0; i < numCourses; i++) {
                ret[i] = res.get(i);
            }
            return ret;
        } else {
            return new int[0];
        }
    }
}
1

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值