0.展示PTA总分
1.本章学习总结
- 举一个栗子,咱们在用百度地图的时候,常常会使用导航功能。比如你在地铁站A附近,你想去的地点在地铁站F附近,那么导航会告诉你一个最佳的地铁线路换乘方案。
1.1 总结树内容
图的基本概念
- 图,是一种比树更为复杂的数据结构。树的节点之间是一对多的关系,并且存在父与子的层级划分;而图的顶点(注意,这里不叫节点)之间是多对多的关系,并且所有顶点都是平等的,无所谓谁是父谁是子。
- 在图中,最基本的单元是顶点(vertex),相当于树中的节点。顶点之间的关联关系,被称为边(edge)。
在有些图中,每一条边并不是完全等同的......这样就引入一个新概念:边的权重(Weight)。涉及到权重的图,被称为带权图(Weighted Graph)。
- 顶点之间的边就有了方向的区分,这种带有方向的图被称为有向图。
邻接矩阵
- 拥有n个顶点的图,它所包含的连接数量最多是n(n-1)个。因此,要表达各个顶点之间的关联关系,最清晰易懂的方式是使用二维数组(矩阵)。
- 如图所示,顶点0和顶点1之间有边关联,那么矩阵中的元素A[0][1]与A[1][0]的值就是1;顶点1和顶点2之间没有边关联,那么矩阵中的元素A[1][2]与A[2][1]的值就是0。
- 有向图的邻接矩阵又是什么样子呢?从图中可以看出,有向图不再是一个对称矩阵。从V0可以到达V1,从V1却未必能到达V0,因此A[0][1]和A[1][0]的值不一定相等。
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
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; //无权图矩阵赋值0或1,有权图赋值0,∞,权
for (i = 0; i < e; i++) //输入边
{
cin >> a >> b;
g.edges[a][b] = 1;
g.edges[b][a] = 1; //无向图需要对称赋值
}
g.e = e;
g.n = n;
}
缺点
- 占用了太多的空间。试想,如果一个图有1000个顶点,其中只有10个顶点之间有关联(这种情况叫做稀疏图),却不得不建立一个1000X1000的二维数组,实在太浪费了。
优点
- 简单直观,可以快速查到一个顶点和另一顶点之间的关联关系。
邻接表
- 为了解决邻接矩阵占用空间的问题,人们想到了另一种图的表示方法:邻接表。在邻接表中,图的每一个顶点都是一个链表的头节点,其后连接着该顶点能够直接达到的相邻顶点。
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
typedef struct Vnode
{ Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef VNode AdjList[MAXV];
typedef struct
{ AdjList adjlist; //邻接表
int n,e; //图中顶点数n和边数e
} AdjGraph;
void CreateAdj(AdjGraph*& G, int n, int e)
{
ArcNode* p, * q;
int i,a,b;
G = new AdjGraph;
for (i = 1; i <= n; i++)
{
G->adjlist[i].firstarc = NULL; //头结点初始化
}
for (i = 1; i <= e; i++)
{
cin >> a >> b; //输入边
/*无向图需要双向赋值*/
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;
}
优点
- 很明显,这种邻接表的存储方式,占用的空间比邻接矩阵要小得多。
DFS
- 从顶点v出发深度遍历图G的算法
① 访问v
② 依次从顶点v未被访问的邻接点出发深度遍历。 - 当以邻接矩阵表示图时,DFS时间复杂度为O(n^2);当以邻接表表示图时,DFS时间复杂度为O(n+e),n为顶点数e为边数。
邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
static int n = 0;
int j;
if (visited[v] == 0)
{
if (n == 0)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
visited[v] = 1;
}
for (j = 1; j <= g.n; j++)
{
if(g.edges[v][j]&&visited[j]==0)
DFS(g, j);
}
}
邻接表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
static int n = 0;
ArcNode* p;
visited[v] = 1;
if (!n)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
p = G->adjlist[v].firstarc;
while (p != NULL&&n<G->n)
{
if (visited[p->adjvex] == 0)DFS(G, p->adjvex);
p = p->nextarc;
}
}
BFS
- 从顶点v出发遍历图G的算法买描述如下:
①访问v
②假设最近一层的访问顶点依次为vi1,vi2,vi3…vik,则依次访问vi1,vi2,vi3…vik的未被访问的邻接点
③重复②知道没有未被访问的邻接点为止 - 当以邻接矩阵表示图时,BFS时间复杂度为O(n^2);当以邻接表表示图时,BFS时间复杂度为O(n+e),n为顶点数e为边数。
/*邻接矩阵*/
void BFS(MGraph g, int v)
{
queue<int>q; //定义队列q
int i, j;
cout << v; //输出起始顶点
visited[v] = 1; //已访问顶点
q.push(v); //顶点加入队列
while (!q.empty()) //队列不空时循环
{
i = q.front(); //出队顶点i
q.pop();
for (j = 1; j <= g.n; j++)
{
if (g.edges[i][j] && !visited[j]) //顶点i的邻接点入队并输出
{
cout << " " << j;
visited[j] = 1;
q.push(j);
}
}
}
}
/*邻接表*/
void BFS(AdjGraph* G, int v)
{
queue<int>q; //定义队列q
ArcNode* p;
int d;
cout << v; //输出起始顶点
visited[v] = 1; //已访问顶点
q.push(v); //顶点加入队列
while (!q.empty()) //队列不空时循环
{
d = q.front(); //出队顶点d
q.pop();
p = G->adjlist[d].firstarc; //顶点d的边结点
while (p)
{
if (visited[p->adjvex] == 0) //每个边结点入队并输出
{
cout << " " << p->adjvex;
visited[p->adjvex] = 1;
q.push(p->adjvex);
}
p = p->nextarc;
}
}
}
判断图是否连通
- 通过DFS、BFS或拓扑排序判断图的连通性,若是连通图则所有顶点都会被访问。
int visited[MAXV];
bool Connect(AdjGraph *G) //判断无向图G的连通性
{ int i;
bool flag=true;
for (i=0;i<G->n;i++) //visited数组置初值
visited[i]=0;
DFS(G,0); //调用前面的中DSF算法,从顶点0开始深度优先遍历
for (i=0;i<G->n;i++)
if (visited[i]==0)
{ flag=false;
break;
}
return flag;
}
判断顶点u->v是否有简单路径
void FindPath(AdjGraph G,int u,int v,int path[],int d) //d表示路径长度初始为-1
{
int w,i;
ArcNode *p;
visited[u]=1;
d++; //路径长度d增1,顶点u加入路径
path[d]=u;
if(u==v&&d>=0) //找到一条路径
{
for(i=0;i<=d;i++)
cout<<path[i];
cout<<endl;
return;
}
p=G->adjlist[u].firstarc; //p指向u的第一个邻接点
while(p)
{
w=p->adjvex;
if(!visited[w]) //w未被访问则递归访问
FindPath(G,w,v,path,d)
p=p->nextarc;
}
/*删除return,加上visited[u]=0恢复环境可以找出图中所有最短路径*/
}
最小生成树
- 对于带权连通图,n个顶点,n-1条边
- 根据DFS或BFS生成的树
- 其中权值之和最小的生成树称为最小生成树,生成的树可能不一样,但权值相同
普里姆算法(Prim): O(n2)、 适用于稠密图,选邻接矩阵
- 此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
1.图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
2.在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
3.重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
for (i=1;i<g.n;i++) //找出n-1个顶点,初始顶点默认已选
{
min=INF;
for (j=0;j<g.n;j++) // 找未选顶点到已选顶点最小值
if (lowcost[j]!=0 && lowcost[j]<min) //j未选并且边最小
{
min=lowcost[j]; k=j; /k记录最近顶点的编号
}
cout>>k;
lowcost[k]=0; //k已选
for (j=0;j<g.n;j++) //修正数组lowcost和closest
if (lowcost[j]!=0 && g.edges[k][j]<lowcost[j])
{
lowcost[j]=g.edges[k][j];
closest[j]=k;
}
}
克鲁斯卡尔算法(Kruskal): O(eloge)、 适用于稀疏图,选邻接表
- 此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
- 把图中的所有边按代价从小到大排序;
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
- 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
//边结构体
typedef struct
{ int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
void Kruskal(AdjGraph *g)
{
初始化边数组Edge E[MAX];
将E数组对边的权值进行排序;
初始化并查集树t;
j=1; //E中边的下标,初值为1
while 生成的边数小于n-1
{
取sn1跟sn2为E[j].u跟E[j].v的集合;
if sn1!=sn2
输出该边,并合并两顶点所在集合;
}
最短路径(与最小生成树不同,路径上不一定包含n个顶点)
- 问题描述:给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径,并限定各边上的权值大于或等于0。
单源最短路径一用Dijkstra ( 迪杰斯特拉)算法(用邻接矩阵方便)
- 图存储结构:
数组dist[:源点Vg到每个终点的最短路径长度。
数组path[]:最短路径序列的前一顶点的序号;初值或无路径用-1表示.
数组s[]:表示最短路径顶点集合
时间复杂度:O(n^2)
不适用带负权值的带权图求单源最短路径
不适用求最长路径长度
void Dijkstra(MatGraph g,int v)
{
int dist[MAXV],path[MAXV];
int visited[MAXV];
int mindis,j,ul
for(i=0;i<g.n;i++)
{
dist[i]=g.edges[v][i]; //距离初始化
visited[i]=0;
if(g.edges[v][i]<INF)
{
path[i]=v; //顶点v到i有边
}
else
{
path[i]=-1; //顶点v到i边
}
}
visited[v]=1;
for(i=0;i<g.n;i++)
{
mindis=INF;
for(j=0;j<g.n;j++)
{
if(s[j]==0&&dist[j]<mindis) //找最小路径长度顶点u
{
u=j;
mindis=dist[j];
}
}
visited[u]=1; //顶点u加入s中
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;
}
}
}
Dispath(dist,path,s,g.n,v);
}
所有顶点间的最短路径-用Floyd ( 弗洛伊德)算法
- 两个数组的含义,A[i] [j]存放的是从i到j之间的的最短距离,A[i] [j]>A[i] [k]+A[k] [j],表示的是从i到j之间经过k会更短,所以把A[i] [j]的长度改为A[i] [k]+A[k] [j],path[i] [j]=i,表示的是,全局最短路径中,j顶点的上一个顶点是i。
- 时间复杂度:O(n^3)
void Floyd(MatGraph g)
{
int A[MAXVEX][MAXVEX];//建立A数组
int path[MAXVEX][MAXVEX];
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.edges[i][j]<INF)
{
path[i][j]=i; //i和顶点j之间有一条边
}
else
path[i][j]=-1; //i和j顶点之间没有一条边
}
}
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[k][j]; //修改路径长度
path[i][j]=k; //修改经过顶点k
}
}
}
}
}
拓扑排序
- 在一个有向无环图(Directed Acycline Graph,DAG)中找一个拓扑序列的过程称为拓扑排序。
- 如何进行拓扑排序?
1.从有向图中选取- -个没有前驱的顶点,并输出之;
2.从有向图中删去此顶点以及所有以它为尾的弧;
3.重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。 - 特点:
1.在输出顶底序号的地方加入一个初始化为0的cnt,进行cnt++,如果最后cnt<n,则说明有回路
2.时间复杂度:O(n+e)
3.一个AOV-网的拓扑序列不是唯一的
4.应用:可以检测是否有环
typedef struct
{
vertex data;
int count;
ArcNode *firstarc;
}VNode;
void TopSort(ADjGraph *G)
{
int i,j;
int St[MAXV];
int top=-1;
ArcNode *p;
for(i=0;i<G->n;i++) //求所有顶点的入度
{
p=G>adjlist[i].firstarc;
while(p!=NULL)
{
G->adjlist[p->adjvex].count++;
p=p->nextarc;
}
}
for(i=0;i<G->n;i++) //将入度为0的顶点入栈
{
if(G->adjlist[i].count==0)
{
top++;
St[top]=i;
}
}
while(top>-1)
{
i=St[top];
top--;
cout<<i;
p=G->adjlist[j].firstarc;
while(P!=NULL)
{
j=p->adjvex;
G->adjlist[j].count--;
if(G->adjlist[j].count==0)
{
top++;
St[top]=j;
}
p=p->nextarc;
}
}
}
关键路径
- 整个工程完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径(critical path)
- 在AOE网中,从源点到汇点的所有路径中具有最大路径长度的路径称为关键路径。
- 算法过程:
(1) 从开始顶点V0出发,假设ve(0)=0,然后按照拓扑有序求出其他各顶点i的最早开始时间ve(i),如果得到拓扑序列中顶点数目小于图中的顶点数,则表示图中存在回路,算法结束,否则继续执行。
(2)从结束顶点Vn出发,假设vl(n-1) = ve(n-1);然后求出各顶点i的最晚发生时间。
(3)根据顶点的最早发生时间,和最晚发生时间,依次求出出每条弧的最早开始时间和最晚开始时间,如果两只相等,则为关键活动。关键活动组成的路径则为关键路径。
void CriticalPath(GraphAdjList GL)
{
EdgeNode *e;
int i,gettop,k,j;
int ete,lte; /*声明活动最早发生时间和最迟发生时间*/
TopoLogicalSort(GL); /*求拓扑序列,计算数组etv和stack2的值*/
ltv = (int*) malloc(GL->numVertexes*sizeof(int)); /*时间的最晚发生时间*/
for(i= 0; i<GL->numVertexes;i++)
ltv[i]=etv[GL->numVertexes-1]; /*初始化ltv[i] 为工程完成的最早时间,etv[i]初始化为0*/
while(top2!=0) /*计算ltv*/
{
gettop = stack2[top2--];
for(e=GL->adjList[gettop].firstedge;e!=NUll;e=e->next)
{/*求各定点事件的最迟发生时间ltv值*/
k=e->adjvex;
if(ltv[k]-e->weight<ltv[gettop])
ltv[gettop]= ltv[k]-e->weight; /*求最晚发生时间,是从拓扑序列的最后一个顶点逆着推导*/
}
}
for(j=0;j<GL->numVertexes;j++) /*求关键活动*/
{
for(e=GL->adjList[j].firstedge;e!=NULL;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);
}
}
}
1.2.谈谈你对图的认识及学习体会
刚开始学习图,我就感觉到了它的不平凡,老师教完的知识点听的懵懵懂懂,只有不断地去编程,去调试,才有机会掌握图的奥义,然后又学了很多很多的算法,说真的到现在还是挺懵的,做题目靠一边看课件一边写,不然真的会乱,会漏很多东西。感觉理解还是很浅陋,之后还要继续学习相关内容。
2.阅读代码
2.1阈值距离内邻居最少的城市
class Solution {
public:
int findTheCity(int n, vector <vector<int>> &edges, int distanceThreshold) {
// 定义二维D向量,并初始化各个城市间距离为INT_MAX(无穷)
vector <vector<int>> D(n, vector<int>(n, INT_MAX));
// 根据edges[][]初始化D[][]
for (auto &e : edges) {
// 无向图两个城市间的两个方向距离相同
D[e[0]][e[1]] = e[2];
D[e[1]][e[0]] = e[2];
}
// Floyd算法
for (int k = 0; k < n; k++) {
// n个顶点依次作为插入点
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j || D[i][k] == INT_MAX || D[k][j] == INT_MAX) {
// 这些情况都不符合下一行的if条件,
// 单独拿出来只是为了防止两个INT_MAX相加导致溢出
continue;
}
D[i][j] = min(D[i][k] + D[k][j], D[i][j]);
}
}
}
// 选择出能到达其它城市最少的城市ret
int ret;
int minNum = INT_MAX;
for (int i = 0; i < n; i++) {
int cnt = 0;
for (int j = 0; j < n; j++) {
if (i != j && D[i][j] <= distanceThreshold) {
cnt++;
}
}
if (cnt <= minNum) {
minNum = cnt;
ret = i;
}
}
return ret;
}
};
2.1.1 该题的设计思路
- 通过Floyd算法求出各个城市间的距离,保存在二维数组A[n][n]中;遍历二维数组A[n][n],统计各个城市在距离不超过阈值的情况下能到达的城市数量;输出能到达其他城市数量最少的城市。
2.1.2 该题的伪代码
输入顶点数N和边数M;
根据顶点数和边数创建邻接矩阵g.edge[][];
用Floyd算法计算各个城市之间距离不超过阈值的城市之和;
{
定义二维数组A[n][n]用来存放各个城市之间的最短距离;
for(i=0 to N-1)
for(j=0 to N-1)
初始化A[i][j]=g.edge[i][j];
end for
end for
for(k=0 to N-1)
for(i=0 to N-1)
for(j=0 to N-1)
if(经过城市k后,i到j的距离比不经过k小)
修改i到j的最短距离为i到k的距离+k到j的距离:A[i][j]=A[i][k]+A[k][j];
end for
end for
end for
定义minNum=∞用来存储一个城市到其他城市的距离不超过阈值的数量;
定义ret用来记录城市编号;
for(i=0 to N-1)
定义距离小于阈值的城市数量cnt=0;
for(j=0 to N-1)
if(i!=j&&i到j的最短距离小于等于阈值) cnt++;
end for
if(cnt<=minNum) minNum=cnt,ret=i;
end for
输出ret;
}
2.1.3 运行结果
2.1.4分析该题目解题优势及难点
- 解题优势:可以在Floyd算法的基础上进行运算,事半功倍,正所谓前人种树后人乘凉;由于这题是无向图,所以采用邻接矩阵来存储图结构比较不会造成内存上的浪费,且邻接矩阵比较容易判断两个城市间是否连通。
- 难点:使用邻接矩阵存储图结构的话一定要注意先初始化邻接矩阵后再对输入的边进行存储
2.2接雨水
int trap(vector<int>& height)
{
int left = 0, right = height.size() - 1;
int ans = 0;
int left_max = 0, right_max = 0;
while (left < right) {
if (height[left] < height[right]) {
height[left] >= left_max ? (left_max = height[left]) : ans += (left_max - height[left]);
++left;
}
else {
height[right] >= right_max ? (right_max = height[right]) : ans += (right_max - height[right]);
--right;
}
}
return ans;
}
2.2.1 该题的设计思路
left_max:左边的最大值,它是从左往右遍历找到的
right_max:右边的最大值,它是从右往左遍历找到的
left:从左往右处理的当前下标
right:从右往左处理的当前下标
1.在某个位置i处,它能存的水,取决于它左右两边的最大值中较小的一个
2.只要 right_max[i]>left_max[i] ,积水高度将由 left_max 决定,类似地left_max[i]>right_max[i]
3.对于位置left而言,它左边最大值一定是left_max,这时候,如果left_max<right_max成立,那么它就知道自己能存多少水了,无论右边将来会不会出现更大的right_max,都不影响这个结果;所以当left_max<right_max时,就去处理left下标,反之,就去处理right下标
时间复杂度:O(n)
空间复杂度:O(1)
2.2.2 该题的伪代码
初始化left指针为0 right指针为size-1;
while(left<right)
{
if(height[left]<height[right])
if(height[left]>=left_max) 更新left_max;
else 累加left_max-height[left]到ans;
left=left+1;
else
if(height[right]>=right_max) 更新right_max;
else 累加right_max-height[right]到ans;
right=right-1;
}
2.2.4分析该题目解题优势及难点
优势:
1.运用双指针的做法,只需要一次遍历,而动态规划需要分左和右两次,减少了空间复杂度
难点:
1.对于一个位置能容下的雨水量来说,能接的雨水量是等于它左右两边柱子高度最小值减去它的高度,所以要思考如何找所有位置的左右两边的柱子的最大值
2.当初看题解的时候我以为是要通过比较left_max和right_max来判断哪个指针动,但是思考了一会才发现其实不用,因为每次移动的都是小的一边,除非出现更大的,因此hight[left]和height[right]中的较大值始终是所扫描的所有数的最大值
2.3判断二分图
class Solution {
public boolean isBipartite(int[][] graph) {
int n = graph.length;
int[] color = new int[n];
Arrays.fill(color, -1);
for (int start = 0; start < n; ++start) {
if (color[start] == -1) {
Stack<Integer> stack = new Stack();
stack.push(start);
color[start] = 0;
while (!stack.empty()) {
Integer node = stack.pop();
for (int nei: graph[node]) {
if (color[nei] == -1) {
stack.push(nei);
color[nei] = color[node] ^ 1;
} else if (color[nei] == color[node]) {
return false;
}
}
}
}
}
return true;
}
}
2.3.1 该题的设计思路
将二分图问题转化为着色问题,分为2种颜色。通过DFS进行着色,若在DFS过程中发现邻接点已经着色且颜色相同,说明不构成二分图。 时间复杂度O(n+e)n为顶点数e为边数; 空间复杂度O(n)color数组存顶点颜色。
2.3.2 该题的伪代码
for 遍历顶点
{
if 顶点v未被着色
{
将v着色成color
for 遍历v的邻接点
{
if 邻接点color与v的相同 return false;
else if 邻接点未着色
递归将邻接点着色成-color
}
}
}
return true;
2.3.3 运行结果
2.3.4分析该题目解题优势及难点
难点:就在于能不能将二分图的判别方法转化为图的着色问题。
优势:在于题解是在DFS过程中同时着色跟判断,这会提升不少效率。