图
0.PTA得分截图
1.本周学习总结(6分)
1.1 图的存储结构
图主要分为无向图、有向图和网。存储方式主要是邻接矩阵和邻接表。
1.1.1 邻接矩阵
- 带/无权图:如果图的边的长度不完全相同,则图为带权图。有/无向图:如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分,而与一个有向边关联的两个点也有始点和终点之分。相反,边没有方向的图称为无向图。
- 有/无向图:如果给图的每条边规定一个方向,那么得到的图称 为有向图。在有向图中,与一个节点相关联的边有出边和入边之分,而与一个有向边关联的两个点也有始点和终点之分。相反,边没有方向的图称为无向图。
- 无向图的邻接矩阵
图的顶点数为n,边数为m
. 无向图的邻接矩阵
. 利用一个一维数组V[n-1]存储顶点下标
. 利用一个二维数组A[n][n],其中A[i][j]中存放的是顶点i到顶点j的距离
. 若i=j则距离为0(邻接矩阵的对焦新为0)
. 若i与j之间不存在边,则设为∞(后文代码中设为0),否则设为边长
该图可以得到如下的邻接矩阵,v1->v2为2,因此A[0][1]=2.A[1][0]=2,V2与V3之间没有边,A[1][2]=A[2][1]=∞其余顶点同理。有向图的同理。
- 邻接矩阵的结构体定义
#define MAX 20 //边和顶点的最大数量
typedef char ElemType;
typedef struct Graph{
int vexNum; //顶点数
int arcNum; //边的数量
ElemType vexs[MAX]; //顶点信息
int arcs[MAX][MAX]; //边的信息
}Graph,*myGraph;
- 建图函数
void createGraph(myGraph &G)
{
G=(Graph*)malloc(sizeof(Graph)); //结构体的指针要初始化
int i,j,k;
int vexNum,arcNum;
char v1,v2; //边的两个顶点
printf("请输入顶点的数量:");
scanf("%d",&G->vexNum);
printf("请输入边的数量:");
scanf("%d",&G->arcNum);
printf("请依次将顶点数据输入进来\n");
for(i=0;i<G->vexNum;i++)
{
getchar();
scanf("%c",&G->vexs[i]);
}
for(i=0;i<G->vexNum;i++)
{
for(j=0;j<G->vexNum;j++)
{
G->arcs[i][j]=0;//初始化矩阵
}
}
printf("请依次将边输入进来\n");
for(i=0;i<G->arcNum;i++)
{
getchar();
scanf("%c%c",&v1,&v2);
j=getLocate(G,v1);
k=getLocate(G,v2);
G->arcs[j][k]=1;
G->arcs[k][j]=1;
}
}
1.1.2 邻接表
. 顶点表也就是个结构体数组,是存放顶点的结构,顶点表中有data元素,存放顶点信息 firstarc是一个边结构体表指针,存放邻接点的信息。
. 边表也是一个结构体,内有adivex元素,存放邻接点的下标,weight存放顶点与邻接点之间线的权重,next是边表结构体指针,存放该顶点的下一个邻接点,next就是负责将顶点的邻接点连起来。
*邻接表的结构体定义
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 CreateAdj(AdjGraph*& G, int n, int e)
{
ArcNode* p;
int i, j, a, b;
G = new AdjGraph;
/*邻接表初始化*/
for (i = 0;i <= n;i++)
{
G->adjlist[i].firstarc = NULL;
}
/*建立邻接表*/
for (i = 0;i < e;i++)
{
//无向图,a,b有边互连
cin >> a >> b;
p = new ArcNode;
p->adjvex = a;
p->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = p;
p = new ArcNode;
p->adjvex = b;
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
}
G->n = n;G->e = e;
}
- 有向图
void CreateAdj(AdjGraph*& G, int n, int e)
{
ArcNode* p;
int i, j, a, b;
G = new AdjGraph;
/*邻接表初始化*/
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 邻接矩阵和邻接表表示图的区别
- 邻接矩阵:图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息。
- 邻接表:邻接矩阵是不错的一种图存储结构,但是,对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。因此,找到一种数组与链表相结合的存储方法称为邻接表。
邻接表的处理方法是这样的:
(1). 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。
(2). 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。 - 区别
对于一个具有n个顶点e条边的无向图,它的邻接表表示有n个顶点表结点2e个边表结点;
对于一个具有n个顶点e条边的有向图,它的邻接表表示有n个顶点表结点e个边表结点;
如果图中边的数目远远小于n2称作稀疏图,这是用邻接表表示比用邻接矩阵表示节省空间;
如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。
1.2 图遍历
1.2.1 深度优先遍历
深度遍历就是图遍历的一种,深度遍历是一直遍历为遍历过的邻边,如果遇到邻边都是已遍历过的就要回溯寻找为遍历过的顶点,知道全部顶点都遍历过,所以我们深度遍历用的是递归的方法。
- 深度优先遍历图的方法是,从图中某顶点v出发:
a. 访问顶点v;
b. 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
c. 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
用一副图来表达这个流程如下:
- 从v = 顶点1开始出发,先访问顶点1。
- 深度遍历代码
矩阵
void DFS(MGraph g, int v)//深度遍历
{
int flag = 0;
visited[v] = 1;
for (int j = 0; j <g.n; j++)
{
if (visited[j] == 1)
{
flag++;
}
}
if (flag == 1)
cout << v;
else
cout << " " << v;
for (int i = 0; i < g.n; i++)
{
if (g.edges[v][i] != 0 && visited[i] == 0)
{
DFS(g, i);
}
}
}
链表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
int flag = 0;
visited[v] = 1;
for (int j = 1; j <= G->n; j++)
{
if (visited[j] == 1)
{
flag++;
}
}
if (flag == 1)
cout << v;
else
cout << " " << v;
ArcNode* p;
for (int i = 0; i < G->n; i++)
{
p = G->adjlist[v].firstarc;
while (p)
{
if (visited[p->adjvex] == 0&&p!=NULL)
{
DFS(G, p->adjvex);
}
p = p->nextarc;
}
}
}
深度遍历适用哪些问题的求解。(可百度搜索)
- 深度遍历算法例题应用
1: 全排列问题
2: ABC+DEF=GHI问题
3: 二维数组寻找点到点的最短路径
4: 求岛屿的面积
5: 求岛屿的个数
6: 地板的埔法有多少种
7: 二叉树的深度遍历
8: 图的深度遍历
9: 图的最短路径求解
10: 找子集等于某给定的数
1.2.2 广度优先遍历
广度优先遍历是按层来处理顶点,距离开始点最近的那些顶点首先被访问,而最远的那些顶点则最后被访问.
- 初始状态,从顶点1开始,队列={1}
- 访问1的邻接顶点,1出队变黑,2,3入队,队列={2,3,}
- 访问2的邻接顶点,2出队,4入队,队列={3,4}
- 访问3的邻接顶点,3出队,队列={4}
- 访问4的邻接顶点,4出队,队列={ 空}
*广度遍历代码
矩阵
void BFS(MGraph g, int v)//广度遍历
{
int front;
queue<int>q;
q.push(v);
visited[v] = 1;
cout << v;
while (!q.empty())
{
front = q.front();
q.pop();
for (int i = 0; i < g.n; i++)
{
if (g.edges[front][i] == 1 && visited[i] == 0)
{
q.push(i);
visited[i] = 1;
cout << " " << i +;
}
}
}
}
- 链表
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
int i, j;
int front;
queue<int>q;
ArcNode* p;
q.push(v);
visited[v] = 1;
cout << v;
while (!q.empty())
{
front = q.front();
q.pop();
p = G->adjlist[front].firstarc;
do
{
if (p != NULL&&visited[p->adjvex]==0)
{
q.push(p->adjvex);
visited[p->adjvex ] = 1;
cout << " " << p->adjvex ;
}
p = p->nextarc;
}while (p != NULL);
}
}
- 广度遍历适用哪些问题的求解。
- 最短路径;
- 最远顶点
1.3 最小生成树。
- 生成树从前述的深度优先和广度优先遍历算法知,对于一个拥有n个顶点的无向连通图,它的边数一般都大于n-1。生成树是指在连通图中,由n个顶点和不构成回路的n-1条边构成的树。若由深度优先遍历得到的生成树称为深度优先生成树,则由广度优先遍历得到的生成树称为广度优先生成树。再进一步分析可知,对于满足条件,连通图的n个顶点和不构成回路的n-1条边构成的生成树有多棵,换言之,图的生成树不唯一。
- 最小生成树对于带权的图,其生成树的边也带权,在这些带权的生成树中必有一棵边的权值之和最小的生成树,这棵生成树就是最小(代价)生成树。
1.3.1 Prim算法求最小生成树
基于上述图结构求Prim算法生成的最小生成树的边序列
- Prim算法思想:
假设G=(V,E)是连通无向网,T=(V,TE)是求得的G的最小生成树中边的集合,U是求得的G的最小生成树所含的顶点集。初态时,任取一个顶点u加入U,使得U={u},TE=Ø。重复下述操作:找出U和V-U之间的一条最小权的边(u,v),将v并入U,即U=U∪{v},边(u,v)并入集合TE,即TE=TE∪{(u,v)}。直到V=U为止。最后得到的T=(V,TE)就是G的一棵最小生成树。也就是说,用Prim求最小生成树是从一个顶点开始,每次加入一条最小权的边和对应的顶点,逐渐扩张生成的。
1). 初始化U={v0},TE=Ø
2). U={v0,v2},TE={(v0,v2)}
3). U={v0,v2,v5},TE={(v0,v2),(v2,v5)}
4). U={v0,v2,v5,v3},TE={(v0,v2),(v2,v5),(v5,v3)}
5). U={v0,v2,v5,v3,v1},TE={(v0,v2),(v2,v5),(v5,v3),(v2,v1)}
6). U={v0,v2,v5,v3,v1,v4},TE={(v0,v2),(v2,v5),(v5,v3),(v2,v1),(v2,v4)}
实现Prim算法的2个辅助数组是什么?其作用是什么?Prim算法代码。
一个是lowcost[]:存放候选边,每个顶点到u中最小边;
另一个是closet[]:U中顶点的邻边顶点
代码:
#define INF 32767
//INF表示oo
void Prim(MGraph g, int v)
{
int lowcost[MAXV], min, closest[MAXV], i, j, k;
for (i = 0; i < g.n; i++)//给lowcost[]和lclosest[]置初
{
lowcost[i] = g.edges[v][i; closest[i] = v;
}
for (i = 1; i < g.n; i++)
//找出(n - 1)个顶点
{
min = INF;
for (j = 0; j < g.n; j++)//在(V - U)中我出离U最近的项点k
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j]; k = j; //k记录最近顶点的编号
printf("边(%d,%d)权为:%d\n", closest[k], k, min);
lowocost[k] = 0;// 记已经加入U
for (j = 0; j < g.n; j++)
//修改数组lowcost和closest
if (g.edges[k][j] != 0 && g.edges[k][j] < lowcost[j])
{
lowcost[j] = g.edges[k][j];
{
closest[j] = k;
}
}
}
}
时间复杂度为O(n^2).
1.3.2 Kruskal算法求解最小生成树
- Kruskal算法用到的数据结构有:
- 边顶点与权值存储结构
- 并查集
- Kruskal算法的步骤包括:
- 对所有权值进行从小到大排序(这里对边排序时还需要记录边的索引,这样以边的权值排完序后只改变了权值的索引位置)
- 然后每次选取最小的权值,如果和已有点集构成环则跳过,否则加到该点集中。最终有所有的点集构成的树就是最佳的
- 代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//并查集实现最小生成树
vector<int> u, v, weights, w_r, father;
int mycmp(int i, int j)
{
return weights[i] < weights[j];
}
int find(int x)
{
return father[x] == x ? x : father[x] = find(father[x]);
}
void kruskal_test()
{
int n;
cin >> n;
vector<vector<int> > A(n, vector<int>(n));
for(int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> A[i][j];
}
}
int edges = 0;
// 共计n*(n - 1)/2条边
for (int i = 0; i < n - 1; ++i) {
for (int j = i + 1; j < n; ++j) {
u.push_back(i);
v.push_back(j);
weights.push_back(A[i][j]);
w_r.push_back(edges++);
}
}
for (int i = 0; i < n; ++i) {
father.push_back(i); // 记录n个节点的根节点,初始化为各自本身
}
sort(w_r.begin(), w_r.end(), mycmp); //以weight的大小来对索引值进行排序
int min_tree = 0, cnt = 0;
for (int i = 0; i < edges; ++i) {
int e = w_r[i]; //e代表排序后的权值的索引
int x = find(u[e]), y = find(v[e]);
//x不等于y表示u[e]和v[e]两个节点没有公共根节点,可以合并
if (x != y) {
min_tree += weights[e];
father[x] = y;
++cnt;
}
}
if (cnt < n - 1) min_tree = 0;
cout << min_tree << endl;
}
int main(void)
{
kruskal_test();
return 0;
}
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
- 算法特点:
迪科斯彻算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。 - 算法的思路
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中包含了图的所有顶点。
Dijkstra算法需要哪些辅助数据结构
dis[] path[]
Dijkstra算法的时间复杂度,使用什么图结构,为什么
- Dijkstra算法最简单的实现方法是用一个链表或者数组来存储所有顶点的集合Q,所以搜索Q中最小元素的运算(Extract-Min(Q))只需要线性搜索Q中的所有元素。算法的运行时间是O(n2)。
- 对于边数少于n2稀疏图来说,我们可以用邻接表来更有效的实现Dijkstra算法。同时需要将一个二叉堆或者斐波纳契堆用作优先队列来寻找最小的顶点(Extract-Min)。当用到二叉堆的时候,算法所需的时间为O((m+n)log n).
1.4.2 Floyd算法求解最短路径
Floyd算法解决什么问题?
Floyd算法需要哪些辅助数据结构
Floyd算法优势,举例说明。
最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。
1.5 拓扑排序
拓扑排序指的是将有向无环图(又称“DAG”图)中的顶点按照图中指定的先后顺序进行排序。
- 对有向无环图进行拓扑排序,只需要遵循两个原则:
- 在图中选择一个没有前驱的顶点 V;
- 从图中删除顶点 V 和所有以该顶点为尾的弧
上面左图拓扑排序如下:
- 结构体
typedef struct//表头节点类型
{
vertex data;//顶点信息
int count; // 存放顶点入度
ArcNode* firstarc;//指向第一条弧
}VNode;
伪代码:
遍历邻接表
把每个顶点的入度算出来
遍历图顶点
度为0,入栈st
while (st不为空)
{
出栈v,访问
遍历v的所有邻接点
{
所有邻接点的度 - 1;
邻接点的度若为0,进栈st
}
}
1.6 关键路径
什么叫AOE-网?
在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网
- AOE网的性质:
⑴ 只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;
⑵ 只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。
什么是关键路径概念?
在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。
什么是关键活动?
键路径上的活动称为关键活动。关键活动:e[i]=l[i]的活动
由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。关键路径长度是整个工程所需的最短工期。
- 代码实现
Status TopologicalOrder(ALGraph G, Stack &T)
{
// 有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)。
// T为拓扑序列定点栈,S为零入度顶点栈。
// 若G无回路,则用栈T返回G的一个拓扑序列,且函数值为OK,否则为ERROR。
Stack S;
intcount=0,k;
charindegree[40];
ArcNode *p;
InitStack(S);
FindInDegree(G, indegree); // 对各顶点求入度indegree[0..vernum-1]
for(int j=0; j<G.vexnum; ++j) // 建零入度顶点栈S
if(indegree[j]==0)
Push(S, j); // 入度为0者进栈
InitStack(T);//建拓扑序列顶点栈T
count = 0;
for(inti=0; i<G.vexnum; i++)
ve[i] = 0; // 初始化
while(!StackEmpty(S))
{
Pop(S, j); Push(T, j); ++count; // j号顶点入T栈并计数
for(p=G.vertices[j].firstarc; p; p=p->nextarc)
{
k = p->adjvex; // 对j号顶点的每个邻接点的入度减1
if(--indegree[k] == 0) Push(S, k); // 若入度减为0,则入栈
if(ve[j]+p->info > ve[k]) ve[k] = ve[j]+p->info;
}//for *(p->info)=dut(<j,k>)
}
if(count<G.vexnum)
returnERROR; // 该有向网有回路
else
returnOK;
}
2.PTA实验作业(4分)
2.1 六度空间(2分)
2.1.1 伪代码(贴代码,本题0分)
定义一个结构体来存放节点编号和深度遍历是节点所在的层次
typedef struct Element {
VertexType v; /* 结点编号 */
int Layer; /* BFS的层次 */
} QElementType;
从第一个节点作为开始·标记为已访问过
将开始节点存放到结构体中并将层数记为0
将其带有节点信息的结构体入队
while (当队中非空时 )
取队头元素为qe并出队
for 从该点的第一条边开始 to 没有边截止
若edge->Adjv是v的尚未访问的邻接顶点,将其记为六度以内的顶点并将层数加1
如果该节点的层数小于6
将其入队
恢复qe的层数
计算符合“六度空间”理论的结点占结点总数的百分比。
2.1.2 提交列表
2.1.3 本题知识点
- 图的广度搜索原理解析类似二叉树的层序遍历
(1). 从所需要的顶点进入图(类似利用此顶点为树的根结点,构建一棵树)
(2). 根结点入队列
(3). 寻找与此顶点相连的所有顶点并放入队列中(图的临接矩阵中,储存的是每个顶点间的边的关系,而且无向图的临接矩阵一定为对称矩阵)
(4). 当顶点所在行遍历结束,取出队列的下一个顶点,重复。直至遍历所有的顶点 - 实现动态分配二维数组注意:
(1). 动态分配必须按照顺序分配
(2). 同时数组释放内存的时候要按照先后顺序释放
(3). 否则会出现野指针
(4). 内存泄漏导致程序崩溃
2.2 村村通或通信网络设计或旅游规划(2分)
2.2.1 伪代码
本题运用prime算法解决
int edges[MAXV][MAXV];
int n,e; //图结构
void prime()
定义lowcost[MAXV]数组表示某个顶点当前最低花费,并在确定该顶点的closest后将其赋值为0
定义closest[MAXV]数组表示某顶点最低花费情况下的前一个顶点
定义min存储最小值
for i = 1 to n
lowcost[i] = edges[1][i];
closest[i] = 1; 初始化最低花费和最近顶点
end for
for i = 1 to n
min = INF (INF为无限大,其值自定义一个较大的数)
for j = 1 to n
若 lowcost[j] != 0&&lowcost[j] < min
min = lowcost[j]
k = j
end for
end for
令k = j即令k 等于 离i最近的那个顶点
lowcost[k] = 0表示该顶点已被访问
for j = 1 to n 加入k后寻找新的最低花费
if(edges[k][j] != 0&&edges[k][j] < lowcost[j])
lowcost[j] = edges[k][j];
closest[j] = k
end for;
for i = 0 to n
累加各个最低花费并且输出
若出现某个顶点的最低距离为INF,表示无法畅通输出 -1
2.2.2 提交列表
2.2.3 本题知识点
prime算法解决