知识点梳理:数据结构与算法——图
省略了容易理解的部分,适合复习,预习困难
基本概念:
- 稀疏图:边条数<完全图边条数x5%
- 稀疏因子:在mxn的矩阵中,有t个非零元素,则稀疏因子 δ = 1 m × n \delta=\frac{1}{m\times n} δ=m×n1
- 简单路径:除起点和终点外同一结点不能出现2次
- 根:一个有向图中,若存在一个顶点 V 0 V_0 V0,从此顶点有路径可以到达图中其他所有顶点,则 V 0 V_0 V0称作图的根
- 连通分量(连通分支):无向图的极大连通子图
- 强连通:任意两顶点间双向路径
- 强连通分量:有向图的极大强连通子图
- 网络:带权的连通图
- 自由树:不带有简单回路的无向图,连通的,|V|-1条边
图的储存结构:
- 无向图的邻接表表示注意同一条边出现两次,修改的时候要改两处
- 十字链表:
图的遍历:
- DFS和BFS的时间复杂度:
- 邻接表:有向图 Θ ( n + e ) \Theta(n+e) Θ(n+e),无向图 Θ ( n + 2 e ) \Theta(n+2e) Θ(n+2e)
- 相邻矩阵: Θ ( n + n 2 ) = Θ ( n 2 ) \Theta(n+n^2)=\Theta(n^2) Θ(n+n2)=Θ(n2)
拓扑排序:
- BFS:选择入度为0输出,删掉其出边,对应顶点indegree–;有环:队列为空时仍有点未访问
- DFS:逆序序列反向输出;遍历相邻点,调用递归函数,最后再将该点存入result数组中
- 有环的判断:
void DFS(Graph& G,int V)
{
A.push(V);S.insert(V);
visit(G,V);//访问V并进行一些操作
G.mark[V]=VISITED;
for(Edge e=G.FirstEdge(V);G.isEdge(e);e=G.nextEdge(e))//访问所有与V相邻的点
{
if(G.mark[e.To]==UNVISITED)
DFS(G,e.To);//递归
else if(S.count(e.To)>0)//之前已经在S出现过,则说明有环
cout<<"yes"<<endl;
int x=A.top();A.pop();
S.erase(x);//将最后加入的那个点去掉,然后访问下一个相邻结点(换一条路)
}
}
实际做题中使用:(原理一致)
bool dfs(int v) {
visit[v] = -1;
for (int j = rem[v]; j; j = e[j].next) {
int t = e[j].to;
if (visit[t] < 0)
return true;//之前已经在路径上出现过
else if (!visit[t] && !dfs(t))
return true;
}
visit[i] = 1;
return false;//no circle
}
最短路径问题:
Dijkstra:
- 单源最短路径,边权不能为负(解决思路(第4题))
- 从已生成最短路径的结点集选择距源点 V 0 V_0 V0最近的边延伸(最终生成以 V 0 V_0 V0为根的有向树
- 时间复杂度(用最小堆且不删除旧值):
Θ
(
(
∣
V
∣
+
∣
E
∣
)
l
o
g
∣
E
∣
)
\Theta((|V|+|E|)log|E|)
Θ((∣V∣+∣E∣)log∣E∣)
- 可以不删除旧值的原因:如果访问到旧值的话对应顶点一定是被标记VISITED的,不可能出现
class Dist{
public:
int index;
int length;
int pre;
}//记录最短路径的相关选择
void Dijkstra(Graph &G,int s,Dist *&D)//s表示从标号为s的顶点开始
{
D=new Dist[G.VerticesNum()];
for(int i=0;i<G.VerticesNum();i++)
{
G.mark[i]=UNVISITED;
D[i].length=INFINITY;
D[i].index=i;
D[i].pre=s;
}//初始化
D[s].length=0;
MinHeap<Dist>H(G.EdgesNum());//实际做题中多用priority_queue
H.insert(D[s]);
for(int i=0;i<G.VerticesNum();i++)
{
bool Found=false;
Dist d;
while(!H.empty())
{
d=H.Min();
H.RemoveMin();
if(G.mark[d.index]=UNVISITED)
{Found=true;break;}
if(!Found)break;
int v=d.index;G.mark[v]=VISITED;Visit(v);
for(Edge e=G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))
{
if(D[G.ToVertex(e)].length>(D[v].length()+G.weight(e)))
{
D[G.ToVertex(e)].length=(D[v].length()+G.weight(e));
D[G.ToVertex(e)].pre=v;
H.insert(D[G.ToVertex(e)]);
}
}
}
}
}
Floyd:
- 动态规划,允许边权为负
- a d j ( k ) [ i , j ] adj^{(k)}[i,j] adj(k)[i,j]: V i V_i Vi到 V j V_j Vj中间序号不大于k时的最短路径->一直迭代到 a d j ( n ) adj^{(n)} adj(n)
- 记录最短路径:path[i,j]: V i V_i Vi到 V j V_j Vj最短路径上 V j V_j Vj前面那个顶点
void Floyd(Graph &G)
{
for(int i=0;i<G.VerticesNum();i++)
for(int j=0;j<G.VerticesNum();j++)
if(i==j)
{
D[i][j].length=0;
D[i][j].pre=i;
}
else
{
D[i][j].length=INFINITY;
D[i][j].pre=-1;//初始化表示不能到达
}
for(int i=0;i<G.VerticesNum();i++)
for(Edge e=G.FirstEdge(i);G.IsEdge(e);e=G.NextEdge(e))
{
D[i][G.ToVertex(e)].length=G.weight(e);
D[i][G.ToVertex(e)].pre=i;
}
for(int v=0;v<G.VerticesNum();v++)
for(int i=0;i<G.VerticesNum();i++)
for(int j=0;j<G.VerticesNum();j++)
if((D[i][v].length+D[v][j].length)<D[i][j].length)
{
D[i][j].length=D[i][v].length+D[v][j].length;
D[i][j].pre=D[v][j].pre;//注意这里不一定是v
}
}
最小生成树:
不唯一但是最小权值确定
Prim:
- 任意顶点开始,包括在MST中->一端在MST一端在外面的边中,找权最小的一条边->端点包括进MST
- 合理性证明:
void Prim(Graph& G,int s)
{
int MSTcnt=0;//计算已经有多少条边入MST
MinHeap<Edge>H;
for(int i=0;i<G.VerticesNum();i++)
G.mark[i]=UNVISITED;
G.mark[s]=VISITED;
int v=s;
while(MSTcnt<G.VerticesNum()-1)
{
for(Edge e=G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))
if(G.mark[G.ToVertex(e)]==UNVISITED)
H.insert(e);
bool Found=false;
while(!H.empty())
{
e=H.Min();
H.removeMin();
if(G.mark[G.ToVertex(e)]]==UNVISITED)
{ Found=true;break;}
}
if(!Found)//非连通图的情况,没有MST,所以前面要有while(MSTcnt<G.VerticesNum()-1)的判断
return;
v=G.ToVertex(e);
G.mark[v]=VISITED;
AddEdgeToMST(e,MST,MSTcnt++);//这一步就是把e记录进去,这里操作可自行调整,但是MSTcnt++一定要
}
}
Kruskal:
- 开始时划分为|V|个等价类,以权的大小为顺序处理各边,某边连接两个不同等价类的顶点,则加入MST,两顶点的等价类合并->最终全部称为一个等价类
- 时间复杂度(使用路径压缩):
O
(
∣
E
∣
l
o
g
∣
E
∣
)
O(|E|log|E|)
O(∣E∣log∣E∣)
(使用路径压缩后对等价类的操作为O(1)) - 补充说明:如果等价类的数量>1但是堆已空,则说明是非连通图,没有MST