这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 林进源 |
0.PTA得分截图
1.本周学习总结(6分)
- 图的基本概念:
图G由两个集合V和E组成,记为G=(V,E),V是顶点的有限集合,E是连接V中两个不同顶点的边的有限集合。 - 图的基本术语
(1)一个顶点所关联的边的数量称为顶点的度,以顶点j为终点的边数目,称为该顶点的入度。以顶点i为起点的边数目称为该顶点的出度。一个顶点入度和出度的和称为该顶点的度。
所有顶点的度之和为边数的两倍
(2)若无向图的两个顶点之间存在一条边,有向图的每两个顶点之间都存在着方向相反的两条边,则称此图为完全图。无向完全图包含n(n-1)/2条边,有向完全图包含n(n-1)条边。
(3)稠密图和稀疏图:当一个图接近完全图称为稠密图,当一个图含有较少的边数称为稀疏图。
(4)在一个图中,从顶点i到顶点j的一条路径是一个顶点的序列,路径长度是指一条路径经过的边的数量。
(5)在无向图中,从顶点i到顶点j有路径则称顶点i和顶点j是连通的。若图中的任意的两个顶点是连通的,则称图为连通图。无向图中的极大连通子图称为连通分量。连通图的连通分量只有一个,而非连通图有多个连通分量。
在有向图中,图中任意两个顶点连通,则称为强连通图。
n个顶点的强连通图至少有n条边。
1.1 图的存储结构
1.1.1 邻接矩阵
图的邻接矩阵是一种采用邻接矩阵数组表示顶点之间相邻关系的存储结构。
一般有连通则用
统计第i行1的个数表示顶点i的出度
统计第j列1的个数表示顶点j的入度
- 邻接矩阵的结构体定义
#define MAXV<最大顶点数>
typedef struct {
int no;//顶点编号
INfoType info;//顶点其他信息
}VertcxRype;
typedef struct {
int edges[MAXV][MAXV];//邻接矩阵
int n, e;//顶点数,边数
VertcxRype vexs[MAXV];//存放顶点信息
}MatGraph;
- 建图函数
void CreateMGraph(MGraph &g, int n, int e)//建图
{
//n顶点,e弧数
g.n = n;
g.e = e;
int i, j;
int 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;
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
}
构建二维数组后,需要先对数组进行初始化,后判断是有向图还是无向图,若为有向图则定义有向的一条边,为无向图则定义无向的两条边。
1.1.2 邻接表
- 邻接矩阵的结构体定义
typedef struct ANode
{
int adjvex;//该边的终点编号
struct ANode *nextarc;//指向下一条边的指针
InfoType info;
}ArcNode;
typedef struct Vnode
{
Vertex data;
ArcNode *firstarc;//指向第一条边的顶点
}VNode;
typedef struct
{
VNode dajlist[MAXV];//邻接表
int n, e;
}AdjGraph;
- 建图函数
void CreateAdj(AdjGraph *&G, int n, int e)//创建图邻接表
{
int i, j, a, b;
G = new AdjGraph;
for (i = 1; i <= n; i++)//邻接表头结点置零
{
G->adjlist[i].firstarc = NULL;
}
for (j = 1; j <= e; j++)//无向图
{
cin >> a >> b;
ArcNode *p,*q;
p = new ArcNode;
q = new ArcNode;
p->adjvex = b;//用头插法进行插入
q->adjvex = a;
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
q->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = q;
}
G->n = n;
G->e = e;
}
将邻接表头的结点指向置零,后判断是为有向图还是无向图,若为有向图,则定义一边方向,用头插法进行插入。
1.1.3 邻接矩阵和邻接表表示图的区别
对于数据量较大的图类似稠密图需要用矩阵进行,而数据量较少的则需要用邻接表表示。
邻接矩阵时间复杂度为O(n的平方),邻接表的时间复杂度为o(n+e)。
1.2 图遍历
1.2.1 深度优先遍历
- 深度遍历代码
邻接矩阵:
void DFS(MGraph g, int v)//深度遍历
{
visited[v] = 1;//建立visited数组存储已访问的结点信息
if (flag == 0)//控制空格输出
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;
}
for (int i = 1; i <= g.n; i++)
{
if (g.edges[v][i] == 1 && visited[i] == 0)//未访问且两点之间连通
{
DFS(g, i);
}
}
}
邻接表:
void DFS(AdjGraph *G, int v)//v节点开始深度遍历
{
visited[v] = 1;
ArcNode *p;//新建结点储存当前信息
if (flag == 0)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;
}
p = G->adjlist[v].firstarc;
while (p != NULL)//遍历当前链
{
if (visited[p->adjvex] == 0)//判断未访问过
{
DFS(G, p->adjvex);
}
p = p->nextarc;
}
}
- 深度遍历适用哪些问题的求解。
图的深度遍历可以找到两点之间的全部路径,以此可以找到迷宫问题的全部可能答案,同时可以判断是否有简单路径,测试图的结构是否正确。
1.2.2 广度优先遍历
- 广度遍历代码:
邻接矩阵:
void BFS(MGraph g, int v)//广度遍历
{
queue<int>q;
cout << v;
visited[v] = 1;//已遍历
q.push(v);
while (!q.empty())//直到q为空
{
v = q.front();
q.pop();
for (int i = 1; i <= g.n; i++)
{
if (visited[i] == 0 && g.edges[v][i] == 1)//未遍历且连通
{
cout << " " << i;
visited[i] = 1;
q.push(i);//进队
}
}
}
}
邻接表:
void BFS(AdjGraph *G, int v)//v节点开始广度遍历
{
ArcNode *p;//新建结点储存当前信息
queue<int>q;
cout << v;
q.push(v);
visited[v] = 1;//已访问
int w;
while (!q.empty())
{
w = q.front();
q.pop();
p = G->adjlist[w].firstarc;
while (p != NULL)//遍历当前链
{
if (visited[p->adjvex] == 0)//未访问过
{
visited[p->adjvex] = 1;
cout << " " << p->adjvex;
q.push(p->adjvex);
}
p = p->nextarc;
}
}
}
- 广度遍历适用哪些问题的求解
图的广度遍历可以以层次型遍历图找到特定的第几个或者最近最远的顶点,可求解迷宫问题的最短路径。
1.3 最小生成树
一个图的极小连通子图的权值之和最小构造出的树为最小生成树,最小生成树是不唯一的,但权值之和一定一样。n个顶点的图构成n-1条边的最小生成树。
1.3.1 Prim算法求最小生成树
- 实现Prim算法的2个辅助数组,closest数组存储最小边依附的顶点编号,lowcost存储最小边的权重,通过记录比较权重找到最小边的顶点,并连通将二者改变后继顶点的权重。
- Prim算法代码:
#define INF 32767
void Peim(MGraph g, int v)
{
int lowcost[MAXV];
int min;
int closest[MAXV];
int i, j, k;
for (i = 0; i < g.n; i++)
{
lowcost[i] = g.edges[v][i];//置初值,放入顶点v和所有顶带你的权值
closest[i] = v;
}
for (i = 1; i < g.n; i++)//n-1条边,进行n-1次
{
min = INF;
for (j = 0; j < g.n; j++)//遍历找到权值最小的
{
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;//记录下标
}
}
lowcost[k] = 0;//lowcost为0表示该顶点已使用
for (j = 0; i < g.n; j++)//遍历所有顶点,比较找到的顶点与其他顶点的权值是否比原来小
{
if (lowcsost[j] != 0 && g.edges[k][j] < lowcost[j])
{
lowcost[j] = g.edges[k][j];
closest[j] = k;//改变权值和相邻的顶点
}
}
}
}
-
时间复杂的为O(n的平方),其适用于边数较多的稠密图,其是通过比较边来找顶点,每次遍历找到一个顶点,与顶点个数无关。适用于邻接矩阵,需要调用到权值,找到特定顶点间的权值。
-
1.3.2 Kruskal算法求解最小生成树
-
辅助数组vest用于记录起始点和终止点的下标,通过改变数组的值来改变顶点的所属集合。
-
Kruskal算法代码:
typedef struct {
int u;//起始点
int v;//终止点
int w;//权值
}Edge;
void Kruskal(MatGraph g)
{
int i, j, ul, vl, sn1, sn2, k;
int vest[MAXV];
Edge E[MaxSize];
k = 0;
for (i = 0; i <= g.n; i++)
{
for (j = 0; j <= i; j++)
{
if (g.edges[i][j] != 0 && e.edges[i][j] != INF)//存放数据
{
E[k].u = i;
E[k].v = j;
E[k].w = g.edges[i][j];
k++;
}
}
}
InserSort(E, g, e);//插入数据按权值大小排序
for (i = 0; i < g.n; i++)//初始化vest
{
vest[i] = i;
}
k = 1;//边的个数
j = 0;
while (k < g.n)
{
ul = E[j].u; vl = E[j].v;//记录起始点和终止点
sn1 = vest[ul];
sn2 = vest[vl];//获取顶点所在编号
if (sn1 != sn2)//为不同集合
{
k++;
for (i = 0; i < g.n; i++)
{
if (vest[i] == sn2)//集合统一编号
{
vest[i] = sn1;
}
}
j++;
}
}
}
- 时间复杂的为O(elog2e),由于其与n无关,只与e有关,适用于稀疏图。适用于邻接表遍历找到两点之间的权值,邻接表的遍历更加方便找到某点与其有连通的关系并比较权值。
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
- 该算法需要dist和path两个数组,其中dist数组用于存放最短的路径长度,通过比较其中元素找到最短路径,path数组则用于存放最短路径,通过改变path数组的值改变顶点的前继结点。
Dijkstra算法代码:
void Dijkstra(MatGraph g, int v)
{
int dist[MAXV],path[MAXV];
int s[MAXV];//判断是否访问
int mindis, i, j, u;
for (i = 0; i < g.n; i++)
{
dist[i] = g.edges[v][i];//初始化距离
s[i] = 0;
if (g.edges[v]]i] < INF)//v到i有边,初始化前继结点
{
path[i] = v;
}
else
{
path[i] = -1;
}
}
s[v] = 1;
for (i = 0; i < g.n; i++)//进行n-1次
{
mindis = INF;
for (j = 0; j < g.n; j++)//找到最小路径的长度
{
if (s[j] == 0 && dist[j] < mindis)
{
u = j;
mindis = dist[j];
}
}
s[u] = 1;
for (j = 0; j < g.n; j++)//修改改变结点后的路径长度
{
if (s[j] == 0)
{
if (g.edges[u][j] < INF&&dist[u] + g.edges[u][j] < dist[j])//修改此处可得到各种多种解法
{
dist[j] = dist[u] + g.edges[u][j];
path[j] = u;
}
}
}
}
}
- 贪心算法无法求最优解问题,在该代码中dist[u]+g.edges[u][j]<dist[j]此处的小于号将该题的解法固定唯一,若改为<=号则会出现多种解法。
- 单独一个顶点时间复杂度为O(n的平方),若是对n个顶点进行则是O(n的立方),更适用于邻接矩阵,方便通过下标找到顶点间权值并在数组中改变,若为邻接表则较难找到权值。
1.4.2 Floyd算法求解最短路径
- Floyd可以求解各个顶点到某个顶点的最短路径的长度以及路径。
Floyd算法代码:
void Floyd(MatGraph g)
{
int A[MAXV][MAXV];
int path[MAXV][MAXV];
int i, j, k;
for (i = 0; i < g.n; i++)//进行初始化
{
for (j = 0; j < g.n; j++)
{
A[i][j] = g.edges[i][j];
if (i != j && g.edgse[i][j] < INF)//存在边的关系时
{
path[i][j] = i;
}
else
{
path[i][j] = -1;
}
}
}
for (k = 0; k < g.n; k++)
{
for (i = 0; i < g.n; i++)
{
for (j = 0; j < g.n; j++)
{
if (A[i][j] > A[i][k] + A[k][j])//找到更短路径
{
A[i][j] = A[i][k] + A[i][j];//修改路径长度
path[i][j] = k;//修改顶点
}
}
}
}
}
- Floyd需要A和path两个二维数组,其中A数组是用于存放两个顶点之间的最短路径,path数组用于存放其的前继结点。
- 该算法方便找到任意顶点间的最短路径长度和路径,而Dijkstra算法只能计算特定顶点之间的最短路径,但是其时间复杂度为O(n的三次方),不适合大量的数据。
1.5 拓扑排序
拓扑排序每次找到入度为0为结点,每输出一个结点,其后续结点的入度减一,入度为0的结点进入栈或数组。
结构体:
typedef struct {
Vertex data;//顶点信息
int count;//存放入度
AreNode *firstarc;//头结点类型
}VNode;
伪代码:
while(栈不空)
{
出栈v,访问;
遍历v所有邻接点
{
所有邻接点的入度-1
当入度为0时,则入栈,以此实现入度为0时的删除操作
}
}
代码:
void TopSort(AdjGraph *G)//邻接表拓扑排序。注:需要在该函数开始计算并初始化每个节点的入度,然后再进行拓扑排序
{
int node[MAXV];
int counts = 0;
int top = -1;
int stacks[MAXV];
ArcNode *p;
int i, j, k = 0;
for (i = 0; i < G->n; i++)//初始化count
{
G->adjlist[i].count = 0;
}
for (i = 0; i < G->n; i++)
{
p = G->adjlist[i].firstarc;
while (p)//计算每个结点入度
{
G->adjlist[p->adjvex].count++;
p = p->nextarc;
}
}
for (i = 0; i < G->n; i++)
{
if (G->adjlist[i].count == 0)//结点为0入栈
{
stacks[++top] = i;
}
}
while (top > -1)
{
i = stacks[top--];
node[k++] = i;//进入数组
counts++;
p = G->adjlist[i].firstarc;
while (p)
{
j = p->adjvex;
G->adjlist[j].count--;//该节点入度-1
if (G->adjlist[j].count == 0)
{
stacks[++top] = j;
}
p = p->nextarc;
}
}
if (counts < G->n)//判断个数是否符合
{
cout << "error!";
}
else
{
for (i = 0; i < k; i++)
{
cout << node[i];
if (i != k - 1)
{
cout << " ";
}
}
}
}
- 如何用拓扑排序代码检查一个有向图是否有环路
自己的思路是在原先的拓扑代码上,由于存在有环路导致某节点的入度恒不为0,导致其输出的顶点个数不全,因此可通过判断输出的顶点个数可以判断是否有回路。
1.6 关键路径
- AOE网:带权的有向无环图,图中入度为0的顶点表示工程的开始事件,出度为0的顶点表示工程的结束事件,称这样的有向图为边表示活动的网(AOE网)。
- 通常每个工程都只有一个开始事件和结束事件,工程的AOE网都只有入度为0的顶点,称为源点,和一个出度为0的顶点,称为汇点。
- 关键路径:在AOE网中从源点到汇点的所有路径中最大路径长度的路径。
- AOE网中一条关键路径各活动持续时间的总和,把关键路径上的活动称为关键活动。
2.PTA实验作业(4分)
2.1 六度空间
从顶点i开始向四周辐射,利用广度遍历的方式,同层的结点用一样的颜色表示,逐次向外剥离,直到第6次为止,此时统计有颜色的结点个数,即为要求的个数。
2.1.1 伪代码
int main()
{
定义矩阵;
输入数据对矩阵的元素进行修改;
for (i = 1; i <= n; i++)
{
cout = BFS(i);
}
}
int BFS(int i)
{
static int visited[MAXV];//存放已访问过的结点
queue<int>q;
int level;
int last = i;//用于判断是否为该层最后一个
int tail;
int count = 0;
访问该结点并进队;
while (队不为空)
{
队头元素出;
for (遍历结点)
{
if (未访问过且边存在)
{
访问进队;
count++;
tail记录此时结点;
}
}
if (last == i)//为该层最后一个
{
level++;
last = j;//移动到下一层
}
if (达到6层)
{
return count;
}
}
}
2.1.2 提交列表
2.1.3 本题知识点
利用邻接矩阵进行广度遍历,通过广度遍历进行层数的判断,需要引入last和tail进行结点访问的层数判断以及结点层数的改变,通过比较last可以判断层数是否需要改变,并及时返回数量。
2.2 村村通
本题思路:村村通实际上是最小生成树的一种题型,要求所有顶点间存在路径且路径长度最短,因此可以用最小生成树的两种算法Prim和Kruskal进行解题。
2.2.1 伪代码
定义矩阵;
int main()
{
输入边数和顶点数;
Create(n, e);
int num=0;
num = Prim(n, e);
}
void Create(int n, int e)
{
对矩阵初始化;
修改矩阵;
}
int Prim(int n, int e)
{
int closet[];//保存顶点下标
int lowcost[];//保存权值
int cost = 0;
lowcost[1] = 0;
lowcost[1] = 0;
初始化lowcost[]和closet;
for (i = 2; i <= 2; i++)
{
初始化min,j,k;
while (j < n)
{
找到权值最小的点记录下标;
}
if (判断下标是否改变, 若有证明连通)
{
记录cost和访问顶点操作;
}
else return -1;
修改lowcost和closet;
}
}
2.2.2 提交列表
2.2.3 本题知识点
如何将村村通联想到最小二成树,最小二成树的两种算法,邻接矩阵的相关知识。