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



0.PTA得分截图

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

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

本次所有总结内容,请务必自己造一个图(不在教材或PPT出现的图),围绕这个图展开分析。建议:Python画图展示。图的结构尽量复杂,以便后续可以做最短路径、最小生成树的分析。

  • 例题


1.1 图的存储结构

1.1.1 邻接矩阵

  • 图及其对应的邻接矩阵

  • 邻接矩阵的结构体定义

#define MAXV <最大顶点个数>
#define INF 32767
typedef struct
{
	int no;//顶点编号
	InfoType info;//顶点其他信息
}VertexType;//顶点类型
typedef struct
{
	int edges[MAXV][MAXV];
	int n;//顶点
	int e;//边
	VertexType vexs[MAXV]//存放顶点信息
}MatGraph;
  • 建图函数
/*构建无向图*/
void CreateMGraph(MGraph& G, int n, int e)
{
    int i, j, a, b;
    /*初始化*/
    for (i = 1;i <= n;i++)
        for (j = 1;j <= n;j++)
            G.edges[i][j] = 0;
    for (i = 1;i <= e;i++)
    {
        cin >> a >> b;
        /*无向图,两顶点互为边,则均赋值为1*/
        G.edges[a][b] = 1;
        G.edges[b][a] = 1;
    }
    G.n = n;
    G.e = e;
}
/*构建有向图*/
void CreateMGraph(MGraph& G, int n, int e)
{
    int i, j, a, b;
    /*初始化*/
    for (i = 1;i <= n;i++)
        for (j = 1;j <= n;j++)
            Gedges[i][j] = 0;
    for (i = 1;i <= e;i++)
    {
        cin >> a >> b;
        /*有向图,a到b的边赋值为1*/
        G.edges[a][b] = 1;
    }
    G.n = n;
    G.e = e;
}

1.1.2 邻接表

  • 图及其对应邻接表

  • 邻接表的结构体定义

typedef struct ANode //边结点;
{
   int adjvex;//指向该边的终点编号
   struct ANode*nextarc;//指向下一个邻接点
   INfoType info;//储存该边的权值等信息
}ArcNode;
typedef struct Vnode //头结点
{
   int data;//顶点
   ArcNode *firstarc;//指向第一个邻接点
}VNode;
typedef struct
{
   VNode adjlist[MAX];
   int n,e;//图中顶点数n和边数e
}AdjGraph;
  • 建图函数
/*构建无向图*/
void CreatAdjGraph(AdjGraph*& g, int n, int e)
{
	int i, j, a, b;
	ArcNode* p;
	g = new AdjGraph;
	g->adjlist = new VNode[n];
	for (i = 0; i < n; i++)
		g->adjlist[i].firstarc = NULL;
	for (i = 0; i < e; i++)
	{
		cin >> a >> b;
		p = new ArcNode;
		p->adjvex = b;
		p->nextarc = g->adjlist[a].firstarc;
		g->adjlist[a].firstarc = p;
		p = new ArcNode;
		p->adjvex = a;
		p->nextarc = g->adjlist[b].firstarc;
		g->adjlist[b].firstarc = p;
	}
	g->n = n;
	g->e = e;
}
/*构建有向图*/
void CreatAdjGraph(AdjGraph*& g, int n, int e)
{
	int i, j, a, b;
	ArcNode* p;
	g = new AdjGraph;
	g->adjlist = new VNode[n];
	for (i = 0; i < n; i++)
		g->adjlist[i].firstarc = NULL;
	for (i = 0; i < e; i++)
	{
		cin >> a >> b;
		p = new ArcNode;
		p->adjvex = b;
		p->nextarc = g->adjlist[a].firstarc;
		g->adjlist[a].firstarc = p;
	}
	g->n = n;
	g->e = e;
}

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

各个结构适用什么图?时间复杂度的区别。

  • 区别
    ①对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
    ②在邻接表上容易找到任意一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi,vj)之间是否有边或弧相连,则需搜索第i个或第j个链表,还不及邻接矩阵方便。

  • 适用范围
    邻接矩阵多用于稠密图的存储(e接近n(n-1)/2),而邻接表多用于稀疏图的存储(e<<n2)。

  • 空间复杂度
    邻接矩阵的空间复杂度为0(n2),而邻接表的空间复杂度为0(n+e)。

1.2 图遍历


1.2.1 深度优先遍历

选上述的图,继续介绍深度优先遍历结果

  • 无向图

第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

  • 有向图
第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
  • 深度遍历代码
void DFS(MGraph g, int v)
{
    int j;
    if (visited[v] == 0)
    {
        if (flag == 0)
        {
            cout << v;
            flag = 1;
        }
        else
            cout << " " << v;
        visited[v] = 1;
    }
    for (j = 1; j <= g.n; j++)
    {
        if (g.edges[v][j] && visited[j] == 0)
            DFS(g, j);
    }
}
  • 深度遍历适用哪些问题的求解。(可百度搜索)

①DFS由于有回溯的步骤,所以我们可以对其进行更新,比如并查集合并时的状态压缩都是使用DFS,还有图论中的tarjan算法,lca等等。

②搜索有状态约束的问题

在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
Sample Input
4 4
…#
…#.
.#…
#…
Sample Output
1

题目要求:棋子只能放在#上并且k个棋子必须在不同行不同列,所以说这里 进行了状态的限制,我们如果用BFS做肯定不太好做,但是我们刚好可以运用DFS的回溯特点做这个题目;
因为这个题目是二维的题目我们无法判断 该行该列到底有没有放棋子,我们只可以判断我们当前到的这一行放棋子了没有,于是我们在外面加一个数组VIS,它标记着第i列有没有放棋子,如果第i列放了棋子,那么我们就令vis[i]==true。然后我们dfs他的每一行
bool vis[50];
int n,k;
char mp[50][50];//标记每一列
/*AshGuang的版权*/
ll cnt=0;
void dfs(int x,int way)//用way记录我们放了多少棋子
{
    if(way==k)
    {
        cnt++;//cnt记录方案数
        return;//一定记得要return
    }
    if(x>=n) return;//这是判界 因为我们按行遍历,一共有n行不能多出去
    for(int i=0;i<n;i++)//判断这一行的每一列
    {
        if(mp[x][i]=='#'&&!vis[i])//如果说这mp[x][i]刚好是#而且 第i列没有放棋子 
        {
            vis[i]=1;//我们就放上
            dfs(x+1,way+1);//在下一行放,这一行已经无法放了,不同行不同列
            vis[i]=0;//这个地方比较关键,回溯的重点//注释1
        }
    }
    dfs(x+1,way);//这一行找不到的话就直接进行下一行//注释2
}
int main()
{
    while(~scanf("%d%d",&n,&k))
    {
        if(n==-1&&k==-1) break;
        cnt=0;
        memset(mp,0,sizeof(mp));
        for(int i=0;i<n;i++)
            scanf("%s",mp[i]);
        dfs(0,0);
        printf("%d\n",cnt);
    }
    return 0;
}

③搜索联通块问题

比如说题目要求 一个矩阵中 只有1与0两种数字 ,问有几块儿1,对于块儿的定义:如果1的 九宫格范围内也有1,那么这个1与九宫格范围内的1是一块儿,比如说:

1 0 1
0 1 0
1 0 1
输出:1

1 0 1 0 0
0 1 0 0 0
1 0 1 0 1
输出:2

这个问题是可以用BFS来写的,但是容易超时,所以用DFS写最好不过了,根据DFS的特点,深度优先搜索嘛。我们对一个起点开始遍历他就会把他所有能按照要求到达的点都遍历一遍。
伪代码:
从一个起点开始:
		该起点标记为1
		遍历该起点的每一个方向,如果是1,继续进行扩展延申
		结束之后,与该起点通过一定规则可以相连的都变成了1
		遍历下一个起点,遍历之前看看这个节点是否被标记过,没有cnt++;
具体代码:
void dfs(int x,int y,int id)
{
    if(x<1||x>m||y<1||y>n) return;
    if(num[x][y]||str[x][y]=='*') return;
    num[x][y]=id;
    for(int i=0;i<8;i++)
    {
        int mx=x+dx[i],my=y+dy[i];
        dfs(mx,my,id);
    }
}
  for(int i=1;i<=m;i++)
         for(int k=1;k<=n;k++)
               if(str[i][k]==1&&num[i][k]==0) dfs(i,k,++cnt);
  printf("%lld\n",cnt);
  最后cnt的值就是联通块个数

1.2.2 广度优先遍历

选上述的图,继续介绍广度优先遍历结果

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

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

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

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

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

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

  • 广度遍历代码
void BFS(MGraph g, int v)
{
    int p;
    queue<int>q;
    if (visited[v] == 0)
    {
        cout << v;
        visited[v] = 1;
        q.push(v);
    }
    while (!q.empty())
    {
        p = q.front();
        q.pop();
        for (int j = 1; j <= g.n; j++)
        {
            if (g.edges[p][j] == 1 && visited[j] == 0)
            {
                cout << " " << j;
                visited[j] = 1;
                q.push(j);
            }
        }
    }
}
  • 广度遍历适用哪些问题的求解。(可百度搜索)

①层序遍历
②最短路径问题

/*求迷宫最短路*/
这种题目用DFS也是可以解的,用 DFS的思路:有许多条路可以到达终点我们求一下 到达终点的最小值。这样是非常耗时间的 而BFS不一样因为BFS是按层遍历,所以 每一层对于根节点来说距离一定是最小的,不需要再次更新,我们到达终点时这个距离一定一定是最小的,比如说:
还是拿这张图来说 让我们求到达的C的最小距离,按照BFS的思路,第二层就可以到C我们直接break就可以了,为什么?因为如果a->b-f->c一定不会比直接到c近,这就是BFS最特殊的性质,在搜索过程中保留了最短路。所以我们用一个辅助数组来标记记录 到当前节点的 最小距离,如果 f还要访问c时,判断一下 如果 c这个地方的距离已经被更新,说明它已经确定了最小值,后来的无法更新,我们就不让c进入队列。这样一来最小值也就确定了。
拿一个例题来说,1代表墙壁,0代表可通路每次可以向四个方向出发,起点在左上方,终点在右下方,问最短距离:
伪代码:(用num[i][j]表示第i行第j个位置走过没有)
因为初始起点需要走0步,我们把num数组 初始-1,mem(num,-1);
左上角为起点:
	num[1][1]=0;
	1 1进入队列
			取出当前队列第一个元素(因为二维通常需要使用结构体)
			判断它是否为终点位置?break:continue;
			遍历该点四个方向可行方向
				if(num[mx][my]==-1)
					num[mx][my]=num[i][k]+1
				把这个点进入队列。
	结束
具体代码:
int bfs()
{
	queue<node>q;
	q.push({1,1});
	num[1][1]=0;
	while(!q.empty)
	{
		node u=q.front();q.pop();//扔掉首元素
		for(int i=0;i<4;i++)
		{
			int mx=u.x+dx[i],my=u.y+dy[i];
			if(u.x==ex&&u.y==ey) return num[u.x][u.y];//放队列之前我们已经把距离更新了,只要找到这个点绝对是最小距离了。
			if(num[mx][my]==-1&&str[mx][my]==0)//这里还需要加一个判界,我就不手写了有点累了。
			{
				num[mx][my]=num[u.x][u.y]+1;
				q.push({mx,my});
			}
		}
	}
}

③同时移动性题目问题
④路径还原

1.3 最小生成树

  • 用自己语言描述什么是最小生成树。
    在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即),而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集(即)且为无循环图,使得
    的 w(T) 最小,则此 T 为 G 的最小生成树。
    即,令到图中所有节点都连通的最小代价

1.3.1 Prim算法求最小生成树

  • 基于上述图结构求Prim算法生成的最小生成树的边序列
设置2个数据结构:

lowcost[i]:表示以i为终点的边的最小权值,当lowcost[i]=0说明以i为终点的边的最小权值=0,也就是表示i点加入了MST

mst[i]:表示对应lowcost[i]的起点,即说明边<mst[i],i>是MST的一条边,当mst[i]=0表示起点i加入MST



我们假设V1是起始点,进行初始化(*代表无限大,即无通路):



lowcost[2]=6,lowcost[3]=1,lowcost[4]=5,lowcost[5]=*,lowcost[6]=*

mst[2]=1,mst[3]=1,mst[4]=1,mst[5]=1,mst[6]=1,(所有点默认起点是V1)



明显看出,以V3为终点的边的权值最小=1,所以边<mst[3],3>=1加入MST

此时,因为点V3的加入,需要更新lowcost数组和mst数组:

lowcost[2]=5,lowcost[3]=0,lowcost[4]=5,lowcost[5]=6,lowcost[6]=4

mst[2]=3,mst[3]=0,mst[4]=1,mst[5]=3,mst[6]=3


明显看出,以V6为终点的边的权值最小=4,所以边<mst[6],6>=4加入MST

此时,因为点V6的加入,需要更新lowcost数组和mst数组:

lowcost[2]=5,lowcost[3]=0,lowcost[4]=2,lowcost[5]=6,lowcost[6]=0

mst[2]=3,mst[3]=0,mst[4]=6,mst[5]=3,mst[6]=0


明显看出,以V4为终点的边的权值最小=2,所以边<mst[4],4>=4加入MST

此时,因为点V4的加入,需要更新lowcost数组和mst数组:

lowcost[2]=5,lowcost[3]=0,lowcost[4]=0,lowcost[5]=6,lowcost[6]=0

mst[2]=3,mst[3]=0,mst[4]=0,mst[5]=3,mst[6]=0


明显看出,以V2为终点的边的权值最小=5,所以边<mst[2],2>=5加入MST

此时,因为点V2的加入,需要更新lowcost数组和mst数组:

lowcost[2]=0,lowcost[3]=0,lowcost[4]=0,lowcost[5]=3,lowcost[6]=0

mst[2]=0,mst[3]=0,mst[4]=0,mst[5]=2,mst[6]=0


很明显,以V5为终点的边的权值最小=3,所以边<mst[5],5>=3加入MST


lowcost[2]=0,lowcost[3]=0,lowcost[4]=0,lowcost[5]=0,lowcost[6]=0

mst[2]=0,mst[3]=0,mst[4]=0,mst[5]=0,mst[6]=0

至此,MST构建成功,如图所示:

  • Prim算法的2个辅助数组是什么?其作用是什么?Prim算法代码。
    • 辅助数据结构及其作用
      ①lowcost数组:lowcost[i]存储U-V中 i 顶点到其邻边中的最小边权值
      ②closest数组:记录最小权值的边对应的顶点

    • Prim算法代码

#include<iostream>
#include<fstream>
using  namespace std;
 
#define MAX 100
#define MAXCOST 0x7fffffff
 
int graph[MAX][MAX];
 
int prim(int graph[][MAX], int n)
{
	int lowcost[MAX];
	int mst[MAX];
	int i, j, min, minid, sum = 0;
	for (i = 2; i <= n; i++)
	{
		lowcost[i] = graph[1][i];
		mst[i] = 1;
	}
	mst[1] = 0;
	for (i = 2; i <= n; i++)
	{
		min = MAXCOST;
		minid = 0;
		for (j = 2; j <= n; j++)
		{
			if (lowcost[j] < min && lowcost[j] != 0)
			{
				min = lowcost[j];
				minid = j;
			}
		}
		cout << "V" << mst[minid] << "-V" << minid << "=" << min << endl;
		sum += min;
		lowcost[minid] = 0;
		for (j = 2; j <= n; j++)
		{
			if (graph[minid][j] < lowcost[j])
			{
				lowcost[j] = graph[minid][j];
				mst[j] = minid;
			}
		}
	}
	return sum;
}
 
int main()
{
	int i, j, k, m, n;
	int x, y, cost;
	ifstream in("input.txt");
	in >> m >> n;//m=顶点的个数,n=边的个数
	//初始化图G
	for (i = 1; i <= m; i++)
	{
		for (j = 1; j <= m; j++)
		{
			graph[i][j] = MAXCOST;
		}
	}
	//构建图G
	for (k = 1; k <= n; k++)
	{
		in >> i >> j >> cost;
		graph[i][j] = cost;
		graph[j][i] = cost;
	}
	//求解最小生成树
	cost = prim(graph, m);
	//输出最小权值和
	cout << "最小权值和=" << cost << endl;
	system("pause");
	return 0;
}

  • 分析Prim算法时间复杂度,适用什么图结构,为什么?
    • 时间复杂度
      O(n^2)
    • 适用范围
      因为执行时间和图中的边数e无关,所以适用于稠密图

1.3.2 Kruskal算法求解最小生成树

  • 基于上述图结构求Kruskal算法生成的最小生成树的边序列

首先,在初始状态下,对各顶点赋予不同的标记(用颜色区别),如下图所示:


对所有边按照权值的大小进行排序,按照从小到大的顺序进行判断,首先是(1,3),由于顶点 1 和顶点 3 标记不同,所以可以构成生成树的一部分,遍历所有顶点,将与顶点 3 标记相同的全部更改为顶点 1 的标记,如下图所示:

其次是(4,6)边,两顶点标记不同,所以可以构成生成树的一部分,更新所有顶点的标记为:

其次是(2,5)边,两顶点标记不同,可以构成生成树的一部分,更新所有顶点的标记为:

然后最小的是(3,6)边,两者标记不同,可以连接,遍历所有顶点,将与顶点 6 标记相同的所有顶点的标记更改为顶点 1 的标记:

继续选择权值最小的边,此时会发现,权值为 5 的边有 3 个,其中(1,4)和(3,4)各自两顶点的标记一样,如果连接会产生回路,所以舍去,而(2,3)标记不一样,可以选择,将所有与顶点 2 标记相同的顶点的标记全部改为同顶点 3 相同的标记:

当选取的边的数量相比与顶点的数量小 1 时,说明最小生成树已经生成。所以最终采用克鲁斯卡尔算法得到的最小生成树为上图所示。

  • 实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
    • 辅助数据结构及其作用
      vest[]数组:判断该条边加入后是否会形成回路
    • Kruskal算法代码
#include <stdio.h>
#include <stdlib.h>
#define Max 50
typedef struct road *Road;
typedef struct road
{
	int a , b;
	int w;
}road;
 
typedef struct graph *Graph;
typedef struct graph
{
	int e , n;
	Road data;
}graph;
 
Graph initGraph(int m , int n)
{
	Graph g = (Graph)malloc(sizeof(graph));
	g->n = m;
	g->e = n;
	g->data = (Road)malloc(sizeof(road) * (g->e));
	return g;
}
 
void create(Graph g)
{
	int i;
	for(i = 1 ; i <= g->e ; i++)
	{
		int x , y, w;
		scanf("%d %d %d",&x,&y,&w);
		if(x < y)
		{
			g->data[i].a = x;
			g->data[i].b = y;
		}
		else
		{
			g->data[i].a = y;
			g->data[i].b = x;
		}
		g->data[i].w = w;
	}
}
 
int getRoot(int v[], int x)
{
	while(v[x] != x)
	{
		x = v[x];
	}
	return x;
}
 
void sort(Road data, int n)
{
	int i , j;
	for(i = 1 ; i <= n-1 ; i++)
	{
		for(j = 1 ; j <= n-i ; j++)
		{
			if(data[j].w > data[j+1].w)
			{
				road t = data[j];
				data[j] = data[j+1];
				data[j+1] = t;
			}
		}
	}
}
 
int Kruskal(Graph g)
{
	int sum = 0;
	//并查集
	int v[Max];
	int i;
	//init
	for(i = 1 ; i <= g->n ; i++)
	{
		v[i] = i;
	}
	sort(g->data , g->e);
	//main
	for(i = 1 ; i <= g->e ; i++)
	{
		int a , b;
		a = getRoot(v,g->data[i].a);
		b = getRoot(v,g->data[i].b);
		if(a != b)
		{
			v[a] = b;
			sum += g->data[i].w;
		}
	}
	return sum;
}
 
int main()
{
	int m , n , id = 1;
	while(scanf("%d %d",&m,&n) != EOF)
	{
		int r , i;
		Graph g = initGraph(m,n);
		create(g);
		r = Kruskal(g);
		printf("Case %d:%d\n",id++,r);
		free(g);
	}
	return 0;
}

  • 分析Kruskal算法时间复杂度,适用什么图结构,为什么?
    • 时间复杂度为
      O(eloge)
    • 适用范围
      更适合于求边稀疏的网的最小生成树


1.4 最短路径

1.4.1 Dijkstra算法求解最短路径

  • 基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)

    • 一般从选定点开始抛入优先队列。(路径一般为0),boolean数组标记0的位置(最短为0) , 然后0周围连通的点抛入优先队列中(可能是node类),并把各个点的距离记录到对应数组内(如果小于就更新,大于就不动,初始第一次是无穷肯定会更新),第一次就结束了

    • 从队列中抛出距离最近的那个点B(第一次就是0周围邻居)。这个点距离一定是最近的(所有权值都是正的,点的距离只能越来越长。)标记这个点为true,并且将这个点的邻居加入队列(下一次确定的最短点在前面未确定和这个点邻居中产生),并更新通过B点计算各个位置的长度,如果小于则更新!

    • 重复二的操作,直到所有点都确定。

  • Dijkstra算法需要哪些辅助数据结构

    • s[]数组:存储被选中的结点
    • dist[]数组:源点到每个顶点的权值
    • path[]数组:存放源点v->j的最短路径上顶点j的前一个顶点编号
  • Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。、

typedef struct node
{
    int matrix[N][M];      //邻接矩阵
    int n;                 //顶点数
    int e;                 //边数
}MGraph;
 
void DijkstraPath(MGraph g,int *dist,int *path,int v0)   //v0表示源顶点
{
    int i,j,k;
    bool *visited=(bool *)malloc(sizeof(bool)*g.n);
    for(i=0;i<g.n;i++)     //初始化
    {
        if(g.matrix[v0][i]>0&&i!=v0)
        {
            dist[i]=g.matrix[v0][i];
            path[i]=v0;     //path记录最短路径上从v0到i的前一个顶点
        }
        else
        {
            dist[i]=INT_MAX;    //若i不与v0直接相邻,则权值置为无穷大
            path[i]=-1;
        }
        visited[i]=false;
        path[v0]=v0;
        dist[v0]=0;
    }
    visited[v0]=true;
    for(i=1;i<g.n;i++)     //循环扩展n-1次
    {
        int min=INT_MAX;
        int u;
        for(j=0;j<g.n;j++)    //寻找未被扩展的权值最小的顶点
        {
            if(visited[j]==false&&dist[j]<min)
            {
                min=dist[j];
                u=j;
            }
        }
        visited[u]=true;    //加入已经访问过的集合,被访问过的集合里的点的距离都是到原点距离最近的
        for(k=0;k<g.n;k++)   //更新dist数组的值和路径的值
        {
            //当访问过集合变化的时候,看一下这个集合中新顶点相邻点到原点的距离是否缩短了
            if(visited[k]==false&&g.matrix[u][k]>0&&(min+g.matrix[u][k])<dist[k])
            {
                dist[k]=min+g.matrix[u][k];
                //路径有更新就记录一下这一步加入访问集合的顶点~~
                path[k]=u;
            }
        }
    }
}

  • Dijkstra算法的时间复杂度,使用什么图结构,为什么。
    *时间复杂度
    O(n^2)
    *适用范围
    不适用于负权值的带权图求单源最短路径

1.4.2 Floyd算法求解最短路径

  • Floyd算法解决什么问题?
    解决给定的加权图中顶点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。

  • Floyd算法需要哪些辅助数据结构
    map(i,j)表示节点i到j最短路径的距离

  • Floyd算法优势

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

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

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

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

    • 适用范围:无负权回路即可,边权可正可负,运行一次算法即可求得任意两点间最短路。

  • 最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。

    • Bellman-Ford算法(解决负权边,解决单源最短路径)
      主要思想:对所有的边进行n-1轮松弛操作,因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边。换句话说,第1轮在对所有的边进行松弛后,得到的是从1号顶点只能经过一条边到达其余各定点的最短路径长度。第2轮在对所有的边进行松弛后,得到的是从1号顶点只能经过两条边到达其余各定点的最短路径长度

      此外,Bellman_Ford还可以检测一个图是否含有负权回路:如果在进行n-1轮松弛后仍然存在dst[e[i]] > dst[s[i]]+w[i]。算法核心代码如下:

1.5 拓扑排序

  • 找一个有向图,并求其对要的拓扑排序序列

    所以,拓扑排序为:acbfde
  • 实现拓扑排序代码,结构体如何设计?
typedef struct {
	Vertex data;//顶点信息
	int count;//增加数据域:存放顶点入度
	AreNode *firstarc;//指向第一个邻接点
}VNode;
  • 书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?

/*拓扑排序伪代码*/

Status TopologicalSort{
    邻接表构图
    if(图G没有形成回路)
则输出图G的顶点的一个拓扑序列并返回OK,否则返回ERROR
 FindInDegree(G, indegree); /*对各顶点求入度*/
 InitStack(S);/*对栈进行初始化*/
 for i:0~G.vernum
 if(入度为0)
则进栈Push(S, i)
对输出顶点计数count = 0
 while当栈不为空
{
 Pop(S,i);   
 输出i号顶点
 计数++count

 for(p=G.vertices[i].firstarc; p; p=p->nextarc)
{
            k = p->adjvex; 
对顶点i的每个邻接点的入度-1
 if(入度减为 0)
      入栈Push(S, k)
}
}
}

/*删除入度为0的结点*/

while(栈不为空)
{
   出栈v,访问;
   while//遍历v所有的邻接点
   {
      将所有邻接点的入度-1
      当入度为0时,则入栈
   }
}
  • 如何用拓扑排序代码检查一个有向图是否有环路?
    可以根据最后程序输出的顶点个数来判断,若有环路,则无法将入度减为0后再输出,即输出的顶点数会减少。

1.6 关键路径

  • 什么叫AOE-网?

在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。
其中:
·只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。
·只有在进入某点的各有向边所代表的活动都已结束,该顶点所代表的时事件才能发生。

  • 什么是关键路径概念?

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

  • 什么是关键活动?

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

2.PTA实验作业(4分)

2.1 六度空间(2分)

选一题,介绍伪代码,不要贴代码。请结合图形展开分析思路。

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

伪代码为思路总结,不是简单翻译代码。

用邻接矩阵构图
初始化,对每条边赋值为1
 while (!qu.empty() && level < 6)
    {
        循环遍历顶点
        {
            若该顶点未被访问过,且该顶点与i之前存在边
            {
                count++;//满足六度空间理论的结点数+1
                i出栈
                visited[i] = 1;//标记结点i为已访问
                tail = i;
            }
        }
        if (last == temp)
        {
            该层遍历完成,level++
            last = tail;
        }
    }
循环遍历每个顶点
广度优先遍历
cout<<(和所求顶点的距离小于或等于6的顶点数) * 100.00 / 总顶点数

2.1.2 提交列表

2.1.3 本题知识点

  • 用二维数组edges[a][b],在输入结点信息时,令其为1,来表示a,b间存在边关系。
  • 广度优先遍历,通过已找到和未找到顶点之间的边界向外扩展,即其首先搜索和s距离为k的所有顶点,然后再去搜索和S距离为k+l的其他顶点。
  • 队列的相关库函数,如
    ①判断队列是否为空qu.empty()
    ②返回队列中第一个元素,即最后插入到队列中的那个元素qu.front()
    ③移除队首元素qu.pop()
    ④插入一个新元素在队尾qu.push(i)
    ⑤常用的库函数还有:返回队列中元素个数qu.size();返回队列中最后一个元素,即最先入队的那个元素qu.back();插入一个新的元素在队尾qu.emplace(i);交换q1,q1两个队列的内容q1.swap(q2)

2.2 村村通或通信网络设计或旅游规划(2分)

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

伪代码为思路总结,不是简单翻译代码。

邻接表建图
给各边赋权值
若n>e
cout<<−1,表示需要建设更多公路

int Connect
{
初始化cosest数组
初始化lowcost数组
for i:1->n
{
	for j++ :2~g->n {
            if (lowcost[j] && lowcost[j] < minn) {
                记录最小值为lowcost[j]
                记录最小值下标j
            }
        }
    花费cost叠加
    令lowcost[k] = 0;
        for j++ :1~g->n {
            if (lowcost[j] && lowcost[j] > g->edges[k][j]) {
                cosest[j]为先前记录的最小值下标
                lowcost[j]为最小值下标到j所对应的边的权值
            }
        }
     循环遍历,
     若lowcost[i]均不为0
     则return -1;
}	
return cost
}

2.2.2 提交列表

2.2.3 本题知识点

  • 邻接矩阵法建图:在构建图时,需要一个结构体,存储图中边信息,及所求城镇数目和候选道路数目,同时用一个二维数组存放顶点间关系(边或弧)的数据
  • 指针的运用:在邻接矩阵构建中,有大量对于结构体定义数据的运用,我们需要注意指针的指向性,避免储存错误信息。
  • 一维、二维数组的运用:用二维数组edges[a][b]来存储道路a,b改建的预算成本,用一维数组lowcost[i]存储最低费用,closest[i]存储最近路程。
posted @ 2021-05-23 18:47  noyiie  阅读(106)  评论(1编辑  收藏  举报
// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css