DS博客作业04--图
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 总结图内容
图存储结构
邻接矩阵:适合稠密图
邻接矩阵的解析:
不带权的图:如果i,j之间有边,则G.edge[i][j]=1,如果没有边,则G.edge[i][j]=0
带权图:如果i,j之间有边,则G.edge[i][j]=权值。 如果没有边,且i=j,则G.edge[i][j]=0;若i!=j的话,G.edge[i][j]=正无穷
且根据邻接矩阵建图,时间复杂度为:O(n2),与顶点个数有关
对于需要用邻接矩阵的建图,且顶点个数较多,可能超过了栈空间。可以用指针做法,动态申请空间。也可以使用全局变量或静态变量,但是强烈不建议,在多文件编程中非常不方便
邻接矩阵结构体及建图代码:
带权节点图的代码由以下代码稍作修改便可以
结构体:
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph;
建图代码:
void CreateMGraph(MGraph& g, int n, int e)//建图
{
g.n = n; g.e = e;
int x, y;//x,y对应两条边
for (int i = 0; i < n; i++)//初始化图结构
for (int j = 0; j < n; j++)
g.edges[i][j] = 0;
for (int i = 0; i < e; i++)
{
cin >> x >> y;
g.edges[x][y] = 1;
g.edges[y][x] = 1;//有向图删掉这句
}
}
1邻接表:适合稀疏图
邻接表结构体及其代码:
typedef struct ANode
{
int adjvex;//该节点的编号
struct ANode* nextarc;//指向下一节点的指针
}ANode;
typedef struct VNode
{
int data;//顶点信息
ANode* firstarc;//指向第一个节点
}VNode;
typedef struct AdjGraph
{
VNode adjvex[MAXSIZE];
int N, M;//N表示顶点个数,M表示边数
}AdjGraph,*Graph;
建图代码:
void CreatGraph(Graph& G, int N, int M)//建立图结构
{
int x, y;//保存两条邻边
ANode* p;
G->N = N; G->M = M;
for (int i = 1; i <= N; i++) //初始化链表的next
{
G->adjvex[i].firstarc = NULL;
}
for (int i = 1; i <= M; i++)
{
cin >> x >> y;
p = new ANode;
p->adjvex = x;
p->nextarc = G->adjvex[y].firstarc;
G->adjvex[y].firstarc = p;
//无向图,有向图只用写入一个就ok
p = new ANode;
p->adjvex = y;
p->nextarc = G->adjvex[x].firstarc;
G->adjvex[x].firstarc = p;
}
}
图遍历及应用。包括DFS,BFS.如何判断图是否连通、如何查找图路径、如何找最短路径。
深度优先遍历(DFS)
深度优先搜索遍历的过程
(1)从图中某个初始顶点v出发,首先访问初始顶点v。
(2)选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
具体图解:
具体代码:
void DFSTraverse(Graph G) { //对于非连通图的情况,需要加上这个函数来遍历图
for (v=0; v<G.vexnum; ++v)
visited[v] = 0; // 访问标志数组初始化
for (v=0; v<G.vexnum; ++v)
if (!visited[v]) DFS(G,v); // 对尚未访问的顶点调用DFS
}
void DFS(ALGraph *G,int v) //深度遍历DFS
{ ArcNode *p;
visited[v]=1; //将该节点置为已访问
printf("%d ",v);
p=G->adjlist[v].firstarc;
while (p!=NULL)
{
if (visited[p->adjvex]==0) DFS(G,p->adjvex); //如果该节点未访问,则遍历该节点
p=p->nextarc; //如果该节点已访问,则遍历下一个节点
}
}
广度优先遍历(BFS)
广度优先搜索遍历的过程是:
(1)访问初始点v,接着访问v的所有未被访问过的邻接点。
(2)按照次序访问每一个顶点的所有未被访问过的邻接点。
(3)依次类推,直到图中所有顶点都被访问过为止。
广度遍历图解:
具体代码:
void DFSTraverse(Graph G) { //对于非连通图的情况,需要加上这个函数来遍历图
for (v=0; v<G.vexnum; ++v)
visited[v] = 0; // 访问标志数组初始化
for (v=0; v<G.vexnum; ++v)
if (!visited[v]) BFS(G,v); // 对尚未访问的顶点调用DFS
}
void BFS(Graph G, int i)//从第i个节点开始广度遍历
{
ANode* p;
p = new ANode;
queue<int> queue;
queue.push(i);
visited[i] = 1; //置为已访问
while (!queue.empty())
{
i = queue.front();
queue.pop();
p = G->adjvex[i].firstarc;
cout<<i;//输出节点
while (p != NULL)
{
if (visited[p->adjvex] == 0)//如果该节点未访问
{
queue.push(p->adjvex);
visited[p->adjvex] = 1; //置为已访问
}
p = p->nextarc;
}
}
}
判断图是否连通
判断图是否连通思路:
由于DFS,BFS都是由一个顶点出发,遍历整个连通图的。
所以只要由一个顶点遍历之后,检查visited[]是否全部为1,如果全为1,说明为连通图,否则是非连通图
求不带权无向连通图的最短路径
伪代码:
队列结构体
typedef struct
{ int data; //顶点编号
int parent; //前一个顶点的位置
} QUERE;
void ShortPath(AdjGraph *G,int u,int v)
{
qu[rear].data=u;//第一个顶点u进队
while队不空循环
{ front++;
w=qu[front].data; //出队顶点w
if (w==v) 根据parent关系输出路径break;
while遍历邻接表
{
rear++;//将w的未访问过的邻接点进队
qu[rear].data=p->adjvex;
qu[rear].parent=front;
}
}
}
//还可以用pta上的解法来做:
代码如下:
struct GNode{
int Nv;
int Ne;
AdjList List;
}Graph,*PtrToGNode;
//运用path[]保存前一个顶点,dish[]数组保存最短路径,类似于迪杰斯特拉算法
void Unweighted( Graph G, Queue Q, int dist[], int path[], Vertex S )
{
Vertex V, W;
NodePtr ptr;
dist[S] = 0;
Enqueue(S, Q);
while ( !IsEmpty(Q) )
{
V = Dequeue( Q );//出队列一个顶点
for ( ptr=G->List[V].FirstEdge; ptr; ptr=ptr->Next) //遍历邻接表
{
W = ptr->AdjV;
if ( dist[W] == INFINITY ) {
dist[w]=dist[v]+1;;
path[W] = V;
Enqueue(W,Q);;
}
}
}
}
最小生成树相关算法及应用
生成树的概念
一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不能回路。一个图的生成树不唯一
深度优先生成树:由深度优先遍历得到的生成树称为深度优先生成树;
广度优先生成树:由广度优先遍历得到的生成树称为广度优先生成树;
最小生成树:生成树中其中权值之和最小的生成树称为图的最小生成树,且最小生成树不唯一,但其权值一定相同
普里姆算法(prim)
图解:
算法思想:
初始化开始顶点,到其他顶点为侯选边,选出最小边,加入集合,再次修改侯选边,选出最小边,如此循环,直到所有顶点选中
代码层面用closest[i]数组表示顶点i的前一个顶点,lowcost[i]表示顶点i到集合的最小边,lowcost[i]==0表示已经访问过
代码详细解析:
void Prim(MGraph G)//prim算法
{
int closest[1000];//记录顶点i的前一个顶点
int lowcost[1000];//记录顶点i到集合的最小边
int k;
for (int i = 1; i <= G.N; i++)//初始化数组closest[i],lowcost[i]
{
closest[i] = 1; lowcost[i] = G.deges[1][i];
}
for (int i = 1; i < G.N; i++)//访问N-1个节点
{
int min = INT_MAX;//初始化最小值
for (int j = 1; j <= G.N; j++)//找出lowcost[i]数组中的最小值,加入集合
{
if (lowcost[j] < min && lowcost[j] != 0)
{
min = lowcost[j]; k = j;
}
}
lowcost[k] = 0;//将该节点置为已访问
for (int j = 1; j <= G.N; j++)//修改lowcost数组,closest数组的值
{
if (lowcost[j] != 0 && G.deges[k][j] < lowcost[j])//如果新加入的顶点到j顶点的权重 小于 原来顶点到j顶点的权重,则修改其最小值
{
lowcost[j] = G.deges[k][j];//修改lowcost[j]的最小值
closest[j] = k;//修改j顶点的前一个顶点为k
}
}
}
}
prim算法应用
适用于稠密图,且算法复杂度只关于顶点,图存储结构用邻接矩阵,算法复杂度为O(n2)
克鲁斯卡尔算法(kruskal)
图解:
算法思想:
将所有边起始顶点和终止顶点以及权重保存下来,用快排或堆排根据权重排序,选出最小边加入集合,假若邻边都在同一个集合内,则直接选下一条边,直到边完
代码层面则用vets[i]数组表示该顶点的集合,若相同,则属于同一个集合
代码详细解析:
void Kruskal(Graph G)//普鲁斯卡尔算法
{
ANode* p;
Edge E[MAXSIZE];
int vets[MAXSIZE];//集合辅助数组
int k = 1;
for (int i = 1; i <= G->N; i++)//将所有边放入数组中
{
p = new ANode;
p = G->adjvex[i].firstarc;//遍历每个顶点
while (p != NULL)//将每条边的起点终点权值写入数组中
{
E[k].start = i;
E[k].tail = p->adjvex;
E[k].w = p->weight;
p = p->nextarc;
k++;
}
}
sort(E+1, E + G->M, cmp);//排序
for (int i = 1; i <= G->N; i++)//初始化辅助数组,使它本身成一个集合
{
vets[i] = i;
}
int start, tail;//保存起始终止节点
for (int j = 1, k = 1; k < G->N; j++)//k表示构造第k条边,j表示数组E从j开始
{
start = E[j].start; tail = E[j].tail;
int sn1, sn2;//保存两个顶点所属的集合编号
sn1 = vets[start]; sn2 = vets[tail];//必须用sn1,sn2保存,直接用vets[start],vets[tail]不行
if (sn1 != sn2)//如果两顶点属于不同集合
{
k++;//将构造边数加一
for (int i = 1; i <= G->N; i++)
{
if (vets[i] == sn2)//将节点所属集合合并
{
vets[i] = sn1;
}
}
}
}
}
算法应用:
公路村村通等等关于最小生成数的,适用于稀疏,且算法复杂度只关于边,图存储结构用邻接表,,可以用并查集改进该算法,时间复杂度为elog2e
最短路径相关算法及应用,可适当拓展最短路径算法
最短路径概念及相关介绍
最短路径概念:在带权有向图中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。
最短路径和最小生成树不同:最小生成树是经过所有顶点的,而最短路径不一定经过所有顶点
求最短路径一般有两种算法:求一顶点到其他各点的最短路径一般用 Dijkstra(迪杰斯特拉)算法,而求任意两点间的最短路径时,一般选用用Floyd(弗洛伊德)算法
Dijkstra(迪杰斯特拉算法)
图解:
算法思想:
从起始顶点选出一条距离起始顶点最小距离边V1, 若起始顶点直接到Vj顶点的距离大于起始顶点经过V1到Vj的距离的话,则修改其距离值。重复上述步骤,直到所有顶点全被包含
代码层面:用数组dist[]表示源点V0到每个终点的最短路径长度,path[]表示短路径序列的前一顶点的序号。-1表示没有路径。用一个数组s[]表示该顶点是否被选中,1代表选中,0代表还未选中
具体代码解析如下:
void Dijkstra(MGraph g, int v)//源点v到其他顶点最短路径
{
int dish[MAXV], path[MAXV];//定义dish[],path[]数组
int s[MAXV];//辅助,用来看该节点是否访问过
for (int i = 0; i < g.n; i++)//初始化s[],dish[],path[]数组的
{
s[i] = 0;
dish[i] = g.edges[v][i];
if (g.edges[v][i] < INF)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;//表示v顶点已经访问过
int min;
int u=0;//保存最小路径顶点
for (int i = 0; i < g.n-1; i++)
{
min = INF;
for (int j = 0; j < g.n; j++)
{
if (s[j] == 0 && dish[j] < min)//找出dish[]数组中的最小值
{
u = j;
min = dish[j];
}
}
s[u] = 1;//表示该顶点已访问
for (int j = 0; j < g.n; j++)
{
if (s[j] == 0)//表示该顶点未访问过
{
if (g.edges[u][j] < INF && dish[u] + g.edges[u][j] < dish[j])//j顶点与u有邻边,且小于原来的值
{
dish[j] = dish[u] + g.edges[u][j];//修改j的dish
path[j] = u;//前驱顶点改变
}
}
}
}
Dispath(dish,path,s,g.n,v);//输出路径
}
Dijkstra(迪杰斯特拉算法)应用:
计算机网络路由,旅游规划等等关于最小路径的应用
时间复杂度为:O(n2)
弗洛伊德(Floyd)算法
图解:
算法思想:
用二维数组A[][[]表示各点的最短路径,path[][]表示前一个顶点,分别遍历每个顶点,在遍历每个顶点的每条边,找出最短路径的路径,写入A[][],path[][];遍历完成后,即得到每两个顶点间的最短路径;
时间复杂度为O(n3)。求每两个顶点间的最短路径,用迪杰斯特拉算法也可以完成,在它的外面加上以每个顶点为起点遍历就可以完成,时间复杂度也为O(n3)
算法具体代码解析:
void Floyd(MGraph g)
{
int A[MAXV][MAXV];
int path[MAXV][MAXV];
int i,j,k,n=g.n;
for(i=0;i<n;i++) //A[][],path[]数组初始化
for(j=0;j<n;j++)
{
A[i][j]=g.edges[i][j];
path[i][j]=-1;
}
for(k=0;k<n;k++) //三重循环,遍历每个顶点
{
for(i=0;i<n;i++) //再遍历每个顶点的每条边
for(j=0;j<n;j++)
if(A[i][j]>(A[i][k]+A[k][j])) //如果存在最短路径,则修改A,Path数组的值
{
A[i][j]=A[i][k]+A[k][j];
path[i][j]=k;
}
}
//循环完成后,即得出每两个顶点间的最短路径,保存在两个二维数组里
}
弗洛伊德(Floyd)算法应用:
计算机网络路由等等,与迪杰特斯拉算法差不多,只是这样求两个顶点间的最短路径更加简洁,方便
拓扑排序、关键路径
拓扑排序
拓扑排序介绍:
拓扑排序的图必须是有向无环图,在这个图中寻找拓扑序列的过程叫做拓扑排序
拓扑排序可以用来检测图中是否有回路
图解:
拓扑排序伪代码:
void TopSort(AdjGraph *G)
{
初始化count=0
while 遍历邻接表,计算出每个顶点的入度,即count
for将入度为0(count==0)的顶点入栈或队列
while栈不为空
出栈一个顶点,输出
while遍历该顶点的领接表
修改每个顶点的入度,即减一
将入度为0的顶点入栈
}
拓扑排序应用:
可以用于排课等需要先后顺序的事件,还可以用来检验图中是否有回路
关键路径
概念介绍:
AOE-网(带权有向无环图) 概念:
用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是一个带权的有向无环图
源点:入度为0的顶点 汇点:出度为0的点
关键路径:从源点到汇点的最长路径
关键活动:关键路径中的边
图解:
1.2.谈谈你对图的认识及学习体会。
学习完了图和树这两个章节,也算学完了非线性结构的基本概念及知识。
在学习树结构时,最大的难点便是递归的设计,以及代码比较难调试,并不能直观的看到结果
而在学习图结构时,感觉概念不难理解,但是求各类问题好多算法,且自己现在根本没有能力写出具体代码,都是哪里不会看哪里,但起码的算法理解我是明白的,问题就是具体代码的问题
图这个结构,解决的是多对多问题,仔细看起来树的一对多结构和一对一结构都是它的子集。而图结构主要解决的便是多对多关系中的最小生成树(经过全部顶点),最短路径(不用经过全部顶点),关键路径,遍历全部顶点的问题等等
2.阅读代码(0--5分)
2.1 题目及解题代码
解题代码:
2.1.1 该题的设计思路
如果节点属于第一个集合,将其着为蓝色,否则着为红色。只有在二分图的情况下,可以给图着色:一个节点为蓝色,说明它的所有邻接点为红色,它的邻接点的所有邻接点为蓝色,依此类推。
代码层面中:使用数组(或者哈希表)记录每个节点的颜色: color[node]。颜色可以是 0, 1,或者未着色(-1 或者 null)。
使用栈完成深度优先搜索,存储着下一个要访问节点的顺序。在 graph[node] 中,对每个未着色邻接点,着色该节点并将其放入到栈中。
2.1.2 该题的伪代码
2.1.3 运行结果
2.1.4分析该题目解题优势及难点。
该解法将一个我感觉摸不着头脑的题目转化成了类似于pta上图着色的问题,只不过这里是给它上色,pta是判断上色是否正确。转化成上色后就很好理解了该题的基本思路。
还有就是该题使用栈完成了深度优先搜索,存储着下一个要访问节点的顺序,不同于以前的递归调用深度遍历
2.2 题目及解题代码
解题代码:
2.2.1 该题的设计思路
使用深度优先搜索的方法判断图中的每个节点是否能走到环中。对于每个节点,我们有三种数组表示的方法:
用visited[0]表示该节点没有出度,是终点;用visited[1]表示该节点已访问;用visited[2]表示该节点安全
当我们第一次访问一个节点时,我们把它从 visited[该顶点]为0 变成 visited[该顶点]为1 ,并继续搜索与它相连的节点。
如果在搜索过程中我们遇到一个 visited[该顶点]为1 的节点,那么说明找到了一个环,此时退出搜索,所有的 visited[该顶点]为1 节点保持不变(即从任意一个 visited[该顶点]为1 的节点开始,都能走到环中)
如果搜索过程中,我们没有遇到 visited[该顶点]为1 的顶点,那么在回溯到当前节点时,我们把它从 visited[该顶点]为1 变成 visited[该顶点]为2 ,即表示它是一个安全的节点。
2.2.2 该题的伪代码
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
优势:用visited[]数组巧妙地表示了该节点的状态情况,运用了DFS来看它有没有环
主要这题就是看它每个顶点是不是连通的,不是连通的就是安全节点。难点就是递归的的设计及如何处理每个顶点的状态比较难弄
2.3 题目及解题代码
解题代码:
2.3.1 该题的设计思路
先将每个节点度为1的节点保存,然后一层层地删除,直到图的节点为1个或者两个为止(由题意可知,最小高度树的根节点一定是一个或者两个)
类似于拓扑排序,只不过像他的变式,换了一种方式来出题
2.3.2 该题的伪代码
2.3.3 运行结果
2.3.4分析该题目解题优势及难点。
这题的思路刚开始我想的是用广度遍每个节点,保存高度和根节点。最后比较高度,最小的根节点即为答案,但这种做法可能会超时
而题目的这种做法,可以说是很巧妙了像拨洋葱一样,层层剥开,剩下的心便是高度最小的根节点,效率时间也可以说是最小化了