数据结构与算法碎片积累(六)

前言:日常忙中偷懒写blog。

1、马踏棋盘算法(骑士周游问题)

1-1)已知条件:
国际象棋的棋盘为8*8的方格棋盘,现将“马”放在指定任意的方格中,按照“马”走棋的规则将“马”进行移动。要求每个方格只能进入一次,最终使得“马”走遍棋盘64个方格。

编写代码,实现马踏棋盘的操作,要求使用1~64来标注“马”移动的路径

1-2)对于在n*n的棋盘上,当n>=5且为偶数的时候,以任意点作为点都有解。

马踏棋盘的其中一个解:
在这里插入图片描述
1-3)一些相关知识点:

1-3-1)回溯法:
指导思想很简单,就是一条路走到黑,碰到壁了再回来一条路走到黑。一般和递归可以很好的搭配使用,还有深度优先搜索(DFS)

1-3-2)哈密顿尔路径:
图G中的哈密尔顿路径指的是经过图G中每个顶点,且只经过一次的一条轨迹。如果这条轨迹是一条闭合的路径(从起点出发不重复地遍历所有点后仍能回到起始点),那么这条路径称为哈密尔顿回路。

1-4)代码实现:

代码分析样图:
在这里插入图片描述
代码:

#include<stdio.h>
#include<time.h>

#define X 8
#define Y 8

int chess[X][Y];

//找到基于(x,y)位置的下一个可走位置
int nextxy(int* x, int* y,int count) {
	switch (count)
	{
		//结合图片来看,棋盘原点是左上角,水平向右为x正方向;垂直向下为y轴方向
		//case 的类型可以结合图中的1到8位置来看
		//马跳规则,棋盘位置没有踏过,并且保证马不能跳出棋盘,并且满足马运动方式
		//判断马可以跳到该格子后,就将该马当前位置(*x,*y)设置为该格子位置
		//当马跳到该格子后,应该表示为1,表示已经踏过了【一开始棋盘中每个格子都初始化为0】
	case 0://3
		if(*x+2<=X-1&&y-1>=0&&chess[*x+2][*y-1]==0){//防止马跳出棋盘外面;位置没有被走过
			*x += 2;
			*y -= 1;
			return 1;
		}
		break;

	case 1://4
		if (*x + 2 <= X - 1 && *y + 1 <= Y - 1 && chess[*x + 2][*y + 1] == 0) {
			*x += 2;
			*y += 1;
			return 1;
		}
		break;

	case 2://2
		if (*x + 1 <= X - 1 && *y - 2 >= 0 && chess[*x + 1][*y - 2] == 0) {
			*x += 1;
			*y -= 2;
			return 1;
		}
		break;

	case 3://5
		if (*x + 1 <= X - 1 && *y + 2 <= Y - 1 && chess[*x + 1][*y + 2]) {
			*x += 1;
			*y += 2;
			return 1;
		}
		break;

	case 4://8
		if (*x - 2 >= 0 && *y - 1 >= 0 && chess[*x - 2][*y - 1] == 0) {
			*x -= 2;
			*y -= 1;
			return 1;
		}
		break;

	case 5://7
		if (*x - 2 >= 0 && *y + 1 <= Y - 1 && chess[*x - 2][*y + 1] == 0) {
			*x -= 2;
			*y += 1;
			return 1;
		}
		break;

	case 6://1
		if (*x - 1 >= 0 && *y - 2 >= 0 && chess[*x - 1][*y - 2] == 0) {
			*x -= 1;
			*y -= 2;
			return 1;
		}
		break;

	case 7://6
		if (*x - 1 >= 0 && *y + 2 <= Y - 1 && chess[*x - 1][*y + 2] == 0) {
			*x -= 1;
			*y += 2;
			return 1;
		}
		break;

	default:
		break;
	}
	return 0;
}

//打印棋盘
void print() {
	int i, j;
	for (i = 0; i < X; i++) {
		for (j = 0; j < Y; j++) {
			printf("%2d\t", chess[i][j]);
		}
		printf("\n\n");
	}
	printf("\n\n");
}

//遍历棋盘
//深度遍历棋盘
//(x,y)为位置坐标
//tag是标记变量,每走一步,tag+1
int TravelChessBoard(int x, int y, int tag) {
	int x1 = x, y1 = y, flag = 0,count=0;
	chess[x][y] = tag;//给棋盘中每个格子编号

	if (X * Y == tag) {//tag=64时
		//打印棋盘
		print();
		return 1;
	}

	//第一次搜索
	flag = nextxy(&x1, &y1, count);
	while (0==flag&&count<7)//格子已经被踏过了,让马继续尝试下一个,直到尝试成功
							//个人觉得,这解析很迷
							//这里count就是不断尝试探路意思
	{
		count++;
		flag = nextxy(&x1, &y1, count);
	}//若找到合适格子,flag=1,跳出循环

	//找到马的下一个可走的坐标(x1,y1),如果找到flag=1,否则为0
	while (flag)
	{
		if (TravelChessBoard(x1,y1,tag+1)) {
			return 1;
		}

		//继续找,直到找到下一步可走坐标(x1,y1),如果找到flag=1,否则为0
		//如果查找失败的话,进行重新查找
		x1 = x;
		y1 = y;
		count++;

		//失败后,重新搜索
		flag= nextxy(&x1, &y1, count);
		while (0 == flag && count < 7)//格子已经被踏过了,让马继续尝试下一个,直到尝试成功
									//个人觉得,这解析很迷
									//迷的话,可以看上面同段解析,也就是马子不断试探路子,直到找到为止
		{
			count++;
			flag = nextxy(&x1, &y1, count);
		}
	}

	if (0 == flag) {//如果找不到,则将棋盘格子初始化为0
		chess[x][y] = 0;//或者说,在没有踏过前,需要把棋盘格子赋值为0
	}
	//printf("%d\n", tag);
	return 0;
}
//这个是一个编写程序代码的架构样式,注释和关键框都先写出来,后面可以分模块来写
//有一个大体思路,方便编写
//int TravelChessBoard(int x, int y, int tag) {
//	int x1 = x, y1 = y, flag = 0, count = 0;
//	chess[x][y] = tag;//给棋盘中每个格子编号
//
//	if (X * Y == tag) {//tag=64时
//		//打印棋盘
//		return 1;
//	}
//
//	//找到马的下一个可走的坐标(x1,y1),如果找到flag=1,否则为0
//	while (flag)
//	{
//		if (TravelChessBoard(x1, y1, tag + 1)) {
//			return 1;
//		}
//
//		//继续找,直到找到下一步可走坐标(x1,y1),如果找到flag=1,否则为0
//	}
//
//	if (0 == flag) {//如果找不到,则将棋盘格子初始化为0
//		chess[x][y] = 0;
//	}
//	return 0;
//}

int main() {
	int i, j;
	clock_t start, finish;//clock_t时time.h里面的函数,标记程序开始和结束时间

	start = clock();

	for (i = 0; i < X; i++) {
		for (j = 0; j < Y; j++) {
			chess[i][j] = 0;//初始化棋盘元素为0
		}
	}

	if (!TravelChessBoard(2, 0, 1)) {//这里选择一个点作为开始点
		printf("抱歉,马踏棋盘创建失败\n");
	}

	finish = clock();
	printf("\n本次计算耗时:%f秒\n\n", (double)(finish - start)/CLOCKS_PER_SEC);//CLOCKS_PER_SEC属于time.h头文件里面定义的,指的是cpu每秒心跳数

	return 0;
}
//程序有bug,有很多是0

注意,上述代码大体框架是没有问题的,还有一些bug没调整,这里贴出来供大家参考一个基本思路。

2、图的遍历(广度优先遍历)

2-1)广度优先遍历(breadthFirstSearch),又称为广度优先搜索,简称BFS

2-2)以找钥匙例子介绍:

深度优先遍历意味着要先彻底找完一个房间再开始另一个房间的搜索。

新方案是,由于钥匙在沙发地下这些可能性极低。先看看钥匙是否放在各个房间的显眼位置,如果没有,再看看各个房间的抽屉有没有。这样逐步扩大查找的范围的方式,称为广度优先遍历。

2-3)
在这里插入图片描述
右侧是广度优先遍历的样式,类似层遍历方式,并且遍历过不再重复遍历原则,完成该图的遍历。

2-4)要实现对图(结合2-3)图来看)的广度遍历,可以利用队列来实现:
在这里插入图片描述
每次出队一次,头部的优先查找。那么1,2,3理解是:A找到B,F,A出队,BF放在第二行呆着,第二行要出队列是B,F还是留着队列中,通过B找到了CIG,那么把当前找到的还没出队的依照找到先后顺序(同级中从左到右查找,注意是兄弟姐妹间查找的,并且不重复)排列。组成第三行顺序。余下依次类推。
那么,这里广度优先遍历顺序就是出队顺序,ABFCIGEDH

2-5)代码实现:
邻接矩阵的广度遍历算法

#include <stdio.h>

void BFSTraverse(MGraph G) //MGrap 图结构
{
	int i, j;
	Queue Q;				//Queue 队列结构
	bool visited[100];		//标记图中点的访问情况,这个是自己加的,不知道是否合适

	for (i = 0; i < G.numVertexes; i++) {
		visited[i] = FALSE;			//每个点都没有被访问过
	}

	initQueue(&Q);			//initQueue初始化队列

	for (i = 0; i < G.numVertexes; i++) //访问结点
	{
		if (!visited[i]) //当顶点没有被访问过就访问
		{
			printf("%c", G.vex[i]);		//访问过的,打印
			visited[i] = TRUE;			//标记为已访问
			EnQueue(&Q, i);				//插入队列

			while (!QueueEmpty(Q))		//如果队列不为空,一直进行
			{
				DeQueue(&Q, &i);        //弹出队列
				for (j = 0; j < G.numVertexes; j++)		//循环遍历顶点
				{
					if (G.art[i][j] == 1 && !visited[j]) //有边(有边表示该顶点相连的,才
														 //可以按顺序访问)且没有被访问过
					{
						printf("%c", G.vex[j]);
						visited[j] = TRUE;
						EnQueue(&Q, j);

					}
				}
			}
		}
	}
}

//邻接表的广度遍历算法类似的,只是将矩阵变成表,自己完成
//后续工作,根据前面学习队列知识以及建图知识,完善程序

3、最小生成树(普里姆算法)

3-1)引入:
在这里插入图片描述
3-2)引出主题
在这里插入图片描述
这里的A对应V0,B对应V1,其他依次类推,表格中的数字表示权值。

3-3)代码实现:

//prim算法生成最小生成树

#include <stdio.h>
struct MGrap		//自己编的,可行性有待考虑
{
	int numVertexes;
	int arc[][100];
};

#define MAXVEX 9
#define INFINITY 65535

void MiniSpanTree_Prim(MGrap G) {
	int min, i, j, k;
	int adjvex[MAXVEX];		//保留相关顶点下标【相关理解,例子:A到E顺序,对应0到4编号,已知A和C\D有联系,那么该数组就写成[0,0,1,1,0]】
	int lowcost[MAXVEX];	//保存相关顶点间边的权值【有联系点就填对应权值,没联系的填写无穷大】

	lowcost[0] = 0;			//V0作为最小生成树的根开始遍历,权值为0
	adjvex[0] = 0;			//V0第一个加入

	//初始化操作【只是给临时值,而且只是第一行而已】
	for (i = 0; i < G.numVertexes; i++) {
		lowcost[i] = G.arc[0][j];		//将邻接矩阵第0行所有权值先加入数组
		adjvex[i] = 0;					//初始化全部先为V0的下标
	}

	//真正构造最小生成树的过程
	for (i = 1; i < G.numVertexes; i++) {
		min = INFINITY;				//初始化最小权值为65535等不可能数值【表示没有连线】
		j = 1;
		k = 0;

		//遍历全部顶点
		while (j<G.numVertexes)
		{
			//找出lowcost数组已存储的最小权值
			if (lowcost[j] != 0 && lowcost[j] < min) //lowcost[j] != 0表示不是自己跟自己连线
			{
				min = lowcost[j];
				k = j;				//将发现的最小权值的下标存入k,以待使用
			}
			j++;
		}//循环找到与已知顶点权值最小下标

		//打印当前顶点边中权值最小的边
		printf("%d,%d", adjvex[k], k);//adjvex[k]存放的是上一个顶点的下标数值
		lowcost[k] = 0;			//将当前顶点的权值设置为0,表示此顶点已经完成任务,进行下一个顶点的遍历

		//邻接矩阵第k行逐个遍历全部顶点
		for (j = 1; j < G.numVertexes; j++) {
			if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) {
				lowcost[j] = G.arc[k][j];
				adjvex[j] = k;
			}
		}//结合for循环外初始化来理解,第一次只是对第一行初始化,后面的行,需要
		 //通过此循进行初始化,但是注意,结合课程图来看的话,如果找到权值最小的
		 //,对应的权重已经设置为0了,不再重复初始化了(课程图还没有设置为0,因
		 //为当时只是为了设置权值),只是初始化该行中后面的
	}
}
//这里也只是核心代码,需要完善:
//结合笔记中的图例来啃代码才行
//宏定义、建图

//以顶点展开,结合邻接表,一行一行找,并且找非零且非无穷大,找出权值最小的,就是
//结点的下一节点,同时做好标记,表示已经连接到树里面了。如此反复,把图转换为一棵
//权值最小的树,也就是最小生成树

4、最小生成树(克鲁斯卡尔算法)

4-1)普里姆(Prim)算法还是克鲁斯卡尔(kruskal)算法,考虑问题的出发点均为:使得生成树上边的权值之和达到最小,则应使生成树中每一条边的权值尽可能的小。

4-2)普里姆算法是以某顶点为起点,逐步找各个顶点上最小权值的边来构建最小生成树

4-3)克鲁斯卡尔算法的做法核心是,直接找最小权值的边来构建生成树。
在这里插入图片描述
4-4)构造最小生成树,结点不应该构成环路

4-5)下面的图,可以跟着程序,将上述的表依次输入得到。实线连接的就是我们所求的最小生成树。
在这里插入图片描述
4-6)代码实现:

//Kruskal算法生成最小生成树

#include <stdio.h>
struct MGrap
{
	int numVertexes;
	int numEdges;
};

struct Edge
{
	int begin;
	int end;
	int weight;
};

#define MAGEDGE 9



int Find(int* parent, int f) {
	while (parent[f]>0)
	{
		f = parent[f];
	}
	return f;
}

void MiniSpanTree_Kruskal(MGrap G) {
	int i, n, m;
	Edge edges[MAGEDGE];		//定义边集数组
	int parent[MAGEDGE];		//定义parent数组用来判断边与边是否形成环路

	for (i = 0; i < G.numVertexes; i++) {
		parent[i] = 0;
	}

	//一开始选择了权值最小的作为根结点

	for (i = 0; i < G.numEdges; i++) {
		n = Find(parent, edges[i].begin);	//4 2 0 1 5 3 8 6
		m = Find(parent, edges[i].end);		//7 8 1 5 8 7 6 6

		if (n != m)					//如果n==m,则形成环路,不满足
		{
			parent[n] = m;			//将此边的结尾顶点放入下标为起点的parent数组中,
									//表示此顶点已经在生成树集合中
			printf("(%d,%d) %d", edges[i].begin, edges[i].end, edges[i].weight);
		}
	}
}

//结合课程笔记里面的图,一步一步跟着代码来走,企图理解算法思路
//很多情况下,算法并不是直接跟着大脑怎么想就怎么写的,而是经过不断优化得到的结果
//所以,最佳方式就是,模仿程序走一遍,理清楚其中的含义,从而理解算法的思想

//图创建还没有做、结构体的构建和适度未确定、里面的回溯重点未理解透彻

4-7)小结:
kruskal算法是针对边展开,而prim算法是针对顶点张开;对于稀疏图(边比较少情况),kruskal算法相对有优势;对于顶点比较少而边比较多的(也就是稠密图),prim算法有优势

5、最短路径(迪杰斯特算法)

5-1)基础知识

在网图和非网图中,最短路径的含义是不同的:
----网图是两顶点经过边上权值之和最小的路径
----非网图是两顶点之间经过的边数最小的路径

路径起始的第一个顶点称为源点,最后一个顶点称为终点

关于最短路径的算法,其中有:
----迪杰斯特算法(Dijkstra)
----弗洛伊德算法(Floyd)

5-2)感性认识:
在这里插入图片描述
这里的权值表示两顶点之间的距离。从V0开始,选着最短路径走,到了V1;以V1为中心,选择最短的走,但是要注意,保证从V0的中路径也是最小;就是根据这两个原则,获取最短路径。

5-3)迪杰斯特拉算法原理,结合上面的例子理解:
并不是一下子就求出V0到V8的最短路径,而是一步步求出它们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础上,求得更远顶点的最短路径,最终获取目标最短路径。

5-4)代码实现:
在这里插入图片描述
下面几个字母对应程序里面的变量走一次所填写出的结果;P表示最短路径下标的数组;D表示权值之和;final表示的表示找到路径与否,1表示找到

//迪杰斯特算法

#define MAXVEX 9
#define INFINITY 65535

struct MGrap
{
	int numVertexes;
	int arc[][15];

};

typedef int Patharc[MAXVEX];		//用于存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX];	//用于存储到各个点最短路径的权值和

void ShortestPath_Dijkstar(MGrap G, int V0, Patharc* P, ShortPathTable* D) {
	int v, w, k, min;
	int final[MAXVEX];		//final[w]=1表示已经求得顶点V0到Vw的最短路径

	//初始化数据
	for (v = 0; v < G.numVertexes; v++) {
		final[v] = 0;
		(*D)[v] = G.arc[V0][v];
		(*P)[v] = 0;
	}
	(*D)[V0] = 0;			//V0到V0的路径为0
	final[V0] = 1;			//V0到V0不需要求路径

	//老师给定样式
	//D:	0 1 5 n n n n n n     //n==65535,表示两点没有连接
	//P:	0 0 0 0 0 0 0 0 0 
	//final:1 0 0 0 0 0 0 0 0
	//经过代码处理有:
	//D:	0 1 4 7 5 8 10 12 16
	//P:	0 0 1 4 2 4 3  6  7
	//final	1 1 1 1 1 1 1  1  1
	//个人复现失败,不知道理解错误还是跟着流程走出问题了。。。

	//开始主循环,每次求得v0到某个v顶点的最短路径
	for (v = 1; v < G.numVertexes; v++) {
		min = INFINITY;
		for (w = 0; w < G.numVertexes; w++) {
			if (!final[w] && (*D)[w] < min) {
				k = w;
				min = (*D)[w];
			}
		}//这里个人疑惑,怎么保证找到最短路径然后跳出呢?好像即使找到了w=1,合适,
		 //属于0到1点,然后0到2点也符合循环条件呢,那么也进入赋值环境,后面从w=3
		 //才不符合if条件。最后该循环使得k=2,那么岂不是0点连接到2点了,并没有设置
		 //前后权值对比。。。。。感觉这逻辑不通。。。210107
		final[k] = 1;		//将目前找到的最近的顶点置为1

		//修正当前最短路径及距离
		for (w = 0; w < G.numVertexes; w++) {
			//如果经过v顶点的路径比现在这条路径的长度短的话,更新
			if (!final[w] && (min + G.arc[k][w] < (*D)[w])) {
				(*D)[w] = min + G.arc[k][w];		//修改当前路径长度
				(*P)[w] = k;						//存放前驱顶点
			}
		}//这里的循环也是感觉逻辑走不通的,个人感觉很有问题。210107
	}
}
//未理解.第一步就是说不通了,因为,第一次找到0到1点连接合适,但是第一个for循环意思
//就是,选择了0到2点的连接,而不是0到1。但是,很明显第一次最佳路径是连接1点而不是
//2点,因为连接1点权值更小。。。。
//需要找个参考例子来试试。210107

6、最短路径(弗洛依德算法)

6-1)对比
迪杰斯特拉算法PK弗洛伊德算法
时间复杂度:O(n2)😮(n3)

6-2)弗洛伊德算法学习必要性:
----因为迪杰特斯拉算法求的是一个顶点到所有顶点的最短路径,但弗洛伊德算法求的是所有顶点到所有顶点的最短路径
----弗洛伊德算法非常简洁优雅
在这里插入图片描述
这里的红色表格用于保存上一次数据,不然随着点数增多,都不知道连到哪一个点了

6-3)弗洛伊德算法原理
在这里插入图片描述
跑算法前,两表如图:
在这里插入图片描述
跑完算法代码,两表:
在这里插入图片描述
需要跟着程序手算走一遍
红色的P数组很重要

#define MAXVEX 9

struct MGrap
{
	int numVertexes;
	int matirx[][15];

};

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];

void ShortestPath_Floyd(MGrap G, Pathmatirx* P, ShortPathTable* D) {
	int v, w, k;

	//初始化D和P
	for (v = 0; v < G.numVertexes; v++) {
		for (w = 0; w < G.numVertexes; w++) {
			(*D)[v][w] = G.matirx[v][w];
			(*P)[v][w] = w;
		}
	}

	//弗洛伊德算法
	for (k = 0; k < G.numVertexes; k++) {
		for (v = 0; v < G.numVertexes; v++) {
			for (w = 0; w < G.numVertexes; w++) {
				if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w]) {
					(*D)[v][w] = (*D)[v][k] + (*D)[k][w];
					(*P)[v][w] = (*P)[v][k];		//思考:这里替换为(*P)[k][w]是否可以。原因是。
				}
			}
		}
	}
}

//需要照着图思路来走一遍。

7、拓扑排序

7-1)基本知识:
一个无环的有向图称为无环图(Directed Acyclic Grap),简称DAG图

所有的工程或者某种流程都可以分为若干小的工程或者阶段,这些工程或阶段被称为“活动”。

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间得优先关系,这样的有向图是顶点表示活动的网,称之为AOV网(Active On vertex Network)

AOV网中的弧表示活动之间存在某种制约关系:AOV网不能存在回路

7-2)拓扑序列:
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,…,Vn满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在Vj之前。那么,称这样的顶点序列为一个拓扑序列。

拓扑排序:所谓的拓扑排序,其实就是对一个有向图构造拓扑序列的过程

拓扑排序例子理解:
在这里插入图片描述
将上述的列表转换为AOV网:
在这里插入图片描述
个人理解,拓扑序列:就是顶点排成列,顶点A指向的顶点B,顶点B一定位于顶点A本身后面。

7-3)拓扑排序算法

对AOV网进行拓扑排序的方法和步骤如下:
----从AOV网中选择一个没有前驱的顶点(该顶点的入度为0)并且输出它;
----从网中删除该顶点,并且删去该顶点发出的全部有向边;
----重复上述两步,直到剩余的顶点都是没有前驱为止

将上述问题使用邻接表表示如下:
在这里插入图片描述
in表示入度;first后面表示其出度连接的顶点

算法时间复杂度:
----对一个具有n个顶点,e条边的网来说,初始建立入度为0的顶点栈,要检查所有顶点一次,执行时间为O(n)
----排序中,若AOV网无回路,则每个顶点入、出栈各一次,每个表结点被检查一次,因而执行时间是O(n+e)
----所以,整个算法的时间复杂度是O(n+e)

#include <malloc.h>
#include <stdio.h>

#define MAXVEX 9
#define ERROR 0
#define OK 1


//边表结点声明
typedef struct EdgeNode {
	int adjvex;
	struct EdgeNode* next;
}EdgeNode;

//顶点表结点声明
typedef struct VertexNode {
	int in;				//顶点入度
	int data;
	EdgeNode* firstedge;
}VertexNode,AdjList[MAXVEX];

typedef struct {
	AdjList adjList;
	int numVertexes, numEdges;
}graphAdjList,*GraphAdjList;

//拓扑排序算法
//若GL无回路,则输出拓扑排序序列并返回OK,否则返回ERROR
Status TopologicalSort(GraphAdjList GL) {
	EdgeNode* e;
	int i, k, gettop;
	int top = 0;				//用于栈指针下标索引
	int count = 0;				//用于统计输出顶点的个数
	int* stack;					//用于存储入度为0的顶点

	stack = (int*)malloc(GL->numVertexes * sizeof(int));

	for (i = 0; i < GL->numVertexes; i++) {
		if (0 == GL->adjList[i].in) {
			stack[++top] = i;		//将度为0的顶点下标入栈
		}
	}

	while (0!=top)
	{
		gettop = stack[top--];		//出栈
		printf("%d->", GL->adjList[gettop].data);
		count++;

		for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
			k = e->adjvex;
			//注意:下边这个if条件是分析整个程序的要点
			//将k号顶点邻接点的入度减少1,因为它的前驱已经消除
			//接着判断减1后入度是否为0,如果为0则也入栈
			if (!(--GL->adjList[k].in)) {
				stack[++top] = k;
			}
		}

	}

	if (count < GL->numVertexes) {		//如果count小于顶点数,说明存在环
		return ERROR;
	}
	else
	{
		return OK;
	}
}

//里面有些结构需要补全

8、关键路径

8-1)AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称之为AOE网(Activity On Edge Network)

8-2)AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点
在这里插入图片描述
顶点表示事件,边表示活动,权值表示持续时间
顶点5,表示活动a4,a5的结束,活动a7,a8的开始;每个活动=后面的数值就是活动持续时间

8-3)AOV网与AOE网的比较,以造车为例:
在这里插入图片描述
红色为关键路径,该路径上面的活动称为关键活动

8-4)关键词理解:
----etv(Earliest Time of Vertex):事件最早发生时间,就是顶点的最早发生时间;

-----ltv(Latest time Of Vertex):事件最晚发生时间,就是每个顶点对应的事件最晚需要开始的事件,如果超过此时间将会延误整个工期;

----ete(Earliest Time Of Edge):活动的最早开工时间,就是弧的最早发生时间;

----lte(Latest Time Of Edge):活动的最晚发生时间,就是不推迟工期的最晚开工时间
在这里插入图片描述
Etv是同源点开始计算;ltv从汇点往回数;对于用多个影响边的顶点,取值应该最值,保证个边都不影响情况,通过该方式,获取上面图表
在这里插入图片描述
Ete,lte的计算分别从源点和汇点开始算,源点顺算,汇点逆算

事件的最早发生时间和事件最晚发生时间相等时,事件的连接线就是关键路径。

关键路径是活动的集合;etv和ltv相等时或者ete和lte相等时,存在关键路径
在这里插入图片描述
8-5)代码实现:

#include <malloc.h>
#include <stdio.h>
#define MAXVXE 9
#define ERROR 0
#define OK 1
#define Status int

//边表结点声明
typedef struct EdgeNode {
	int adjvex;
	int weight;
	struct EdgeNode* next;
}EdgeNode;

//顶点表结点声明
typedef struct VertexNode {
	int in;			//顶点入度
	int data;
	EdgeNode* firstedge;
}VertexNode,AdjList[MAXVXE];

typedef struct {
	AdjList adjList;
	int numVertexes, numEdges;
}graphAdjList,*GraphAdjList;

int* etv, * ltv;			//etv,事件最早发生时间;ltv,事件最晚发生时间
int* stack2;				//用于存储拓扑序列的栈
int top2;					//用于stack2的栈顶指针

//拓扑排序算法
//若GL无回路,则输出拓扑排序序列并返回OK,否则返回ERROR

Status TopologicalSort(GraphAdjList GL) {
	EdgeNode* e;
	int i, k, gettop;		
	int top = 0;			//用于栈指针下标索引
	int count = 0;			//用于统计输出顶点个数
	int* stack;				//用于存储入度为0的顶点

	stack = (int*)malloc(GL->numVertexes * sizeof(int));

	for (i = 0; i < GL->numVertexes; i++) {
		if (0 == GL->adjList[i].in) {
			stack[++top] = i;			//将度为0的顶点下标入栈
		}
	}

	//初始化etv都为0
	top2 = 0;
	etv = (int*)malloc(GL->numVertexes * sizeof(int));
	for (i = 0; i < GL->numVertexes; i++) {
		etv[i] = 0;
	}

	stack2 = (int*)malloc(GL->numVertexes * sizeof(int));

	while (0!=top)
	{
		gettop = stack[top--];			//出栈
		//printf("%d->", GL->adjList[gettop].data);
		stack2[++top2] = gettop;		//保存拓扑序列顺序
		count++;

		for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
			k = e->adjvex;
			//注意,下边这个if条件是分析整个程序的要点
			//将k号顶点邻接点的入度减去1,因为其前驱已经消除
			//接着判断减去1后的入度是否为0,如果为0,则也入栈
			if (!(--GL->adjList[k].in)) {
				stack[++top] = k;
			}

			if ((etv[gettop] + e->weight) > etv[k]) {
				etv[k] = etv[gettop] + e->weight;
			}

		}

	}

	if (count < GL->numVertexes) {		//如果count小于顶点,说明存在环
		return ERROR;
	}
	else
	{
		return OK;
	}

}

//求关键路径,GL为有向图,输出GL的各项关键活动
void CriticalPath(GraphAdjList GL) {
	EdgeNode* e;
	int i, gettop, k, j;
	int ete, lte;

	//调用改进后的拓扑排序,求出etv和stack2的值
	TopologicalSort(GL);

	//初始化ltv都为汇点的时间
	ltv = (int*)malloc(GL->numVertexes * sizeof(int));
	for (i = 0; i < GL->numVertexes; i++) {
		ltv[i] = etv[GL->numVertexes - 1];
	}

	//从汇点倒过来逐个计算ltv
	while (0!=top2)
	{
		gettop = stack2[top2--];		//注意,第一个出栈的是汇点
		for (e = GL->adjList[gettop].firstedge; e; e->next) {
			k = e->adjvex;
			if (ltv[k] - e->weight < ltv[gettop]) {
				ltv[gettop] = ltv[k] - e->weight;
			}
		}//这里保证里面填写最值
	}

	//通过etv和ltv求ete和lte
	for (j = 0; j < GL->numVertexes; j++) {//这里对顶点进行了遍历
		for (e = GL->adjList[j].firstedge; e; e = e->next) {//邻边表,顶点的派生连接点
			k = e->adjvex;
			ete = etv[j];
			lte = ltv[k] - e->weight;

			if (ete == lte) {//所有相等的活动组成就是关键路径
				printf("<v%d,v%d> length:%d,", GL->adjList[j].data, GL->adjList[k].data, e->weight);
			}
		}
	}

}

这节笔记很难,里面的代码这里仅是提供给大家参考。
#########################
不积硅步,无以至千里
好记性不如烂笔头
感谢授课老师
截图权利归原作者所有

问题描述:将马随机放在国际象棋的 8X8 棋盘中的某个方格中,马按走棋规则进行移动。要求每个方格上只进入一次,走遍棋盘上全部 64 个方格。编制递归程序,求出马的行走路线 ,并按求出的行走路线,将数字 1,2,…,64 依次填入 8X8 的方阵输出之。测试数据:由读者指定可自行指定一个马的初始位置。实现提示:每次在多个可走位置中选择一个进行试探,其余未曾试探过的可走位置必须用适当结构妥善管理,以备试探失败时的“回溯”悔棋使用。并探讨每次选择位置的“最佳策略”,以减少回溯的次数。 背景介绍: 国际象棋为许多令人着迷的娱乐提供了固定的框架,而这些框架常独立于游戏本身。其中的许多框架都基于骑士奇异的L型移动规则。一个经典的例子是骑士漫游问题。从十八世纪初开始,这个问题就引起了数学家和解密爱好者的注意。简单地说,这个问题要求从棋盘上任一个方格开始按规则移动骑士,使之成功的游历国际象棋棋盘的64个方格,且每个方格都接触且仅接触一次。 可以用一种简便的方法表示问题的一个解,即将数字1,……,64按骑士到达的顺序依次放入棋盘的方格中。 一种非常巧妙的解决骑士漫游地方法由J.C.Warnsdorff于1823年给出。他给出的规则是:骑士总是移向那些具有最少出口数且尚未到达的方格之一。其中出口数是指通向尚未到达方格的出口数量。在进一步的阅读之前,你可以尝试利用Warnsdorff规则手工构造出该问题的一个解。 实习任务: 编写一个程序来获得马踏棋盘即骑士漫游问题的一个解。 您的程序需要达到下面的要求: 棋盘的规模是8*8; 对于任意给定的初始化位置进行试验,得到漫游问题的解; 对每次实验,按照棋盘矩阵的方式,打印每个格被行径的顺序编号。 技术提示: 解决这类问题的关键是考虑数据在计算机中的存储表示。可能最自然的表示方法就是把棋盘存储在一个8*8的二维数组board中。以(x,y)为起点时骑士可能进行的八种移动。一般来说,位于(x,y)的骑士可能移动到以下方格之一:(x-2,y+1)、(x-1,y+2)、(x+1,y+2)、(x+2,y+1)、(x+2,y-1)、(x+1,y-2)、(x-1,y-2)、(x-2,y-1)。但请注意,如果(x,y)的位置离某一条边较近,有些可能的移动就会把骑士移到棋盘之外,而这当然是不允许的。骑士的八种可能的移动可以用一个数组MoveOffset方便地表示出来: MoveOffset[0]=(-2,1) MoveOffset[1]=(-1,2) MoveOffset[2]=(1,2) MoveOffset[3]=(2,1) MoveOffset[4]=(2,-1) MoveOffset[5]=(1,-2) MoveOffset[6]=(-1,-2) MoveOffset[7]=(-2,-1) 于是,位于(x,y)的骑士可以移动到(x+MoveOffset[k].x, y+MoveOffset[k].y),其中k是0到7之间的某个整数值,并且新方格必须仍位于棋盘上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值