国庆七天乐——第六天
20171006
【【图论】】
**********************定义*****************************
在讲这个问题之前,首先我们需要了解图论中的图是什么东西。
定义:图G是一个有序二元组(V,E),其中V称为顶集(Vertices Set),其中的元素称为顶点,E称为边集(Edges set),其中的元素称为边。E与V不相交。它们亦可写成V(G)和E(G)。
边都是二元组,用(x,y)表示,其中x,y∈V。
图G可以理解为对若干个元素之间的关系的抽象表示,其中边代表着对应的顶点之间的关系。
由于图G包含两个集合,顶集和边集,因此图G的规模有两个向度:V集的大小(顶点的数量)和E集的大小(边的数量),前者又称为图G的阶。
如果图G所表述的顶点之间的关系是不能互换的,那么对于x和y之间的关系来说,x和y的相对顺序是必须考虑的,此时边(x,y)可以用有序对(x,y)表示,而图G是有向图。
************************************路径*******************
令图G=(V,E),u到w的一条路径指的是这样的一个序列:
u,e1,v1,e2,v2,..,ek,w
其中顶点和边相间,而且边表示从左边顶点到右边顶点的关系
如果这条路径除了u和w以外其余顶点都各不相等,那么称这条路径为简单路径。
如果u=w,也就是起点和终点相等,那么这条路径称为环(回路)
路径的长度定义为路径中所有边的权值的和(可能为负)
如果u到v存在路径,v到w存在路径,那u到w也存在路径。
对于无向图,u到v的路径可以通过反转(排列反转,边变为反向边)得到v到u的路径。
*******************************分类*******************
对于图G来说,如果它所表述的顶点之间的关系都是可以互换的,也就是说这个关系不关心两个顶点的相对顺序,那么边(x,y)可以用无序对(x,y)表示,此时图G是无向图。
令无向图G=(V,E),如果∀x,y∈V,x和y之间都存在路径,那么称图G为连通图。
令有向图G=(V,E),如果∀x,y∈V,x到y都存在路径,那么称图G为强连通图。
令图G=(V,E),如果对于任意的顶点x和y,x到y之间的关系都可以用最多一条边表述,也就是说V中不含两个或以上的(x,y),那么图G称为简单图。简单图可以用矩阵表示。
令无向图G=(V,E),如果图G连通,且不包含简单环。那么称图G为无向树,此时|V|-1=|E|。
令有向图G=(V,E),如果图G在把边无向化后变为无向树,那么称图G为有向树。
令有向树G=(V,E),如果存在一个顶点x使得从x能够到达其余所有顶点,那么有向树G=(V,E)称为根在x的树形图。
令图G=(V,E),取集合V’=V,然后令E’⊆E,则图G’=(V’,E’)称为图G的生成子图。特别地,若此时G’为无向树,那么称图G’为生成树。
令图G=(V,E),取集合V’⊆V,然后令E’={ (x,y)∈E | x,y∈V’ },则图G’=(V’,E’)称为图G的导出子图。
令图G=(V,E),分别取集合V’⊆V,E’⊆E,且E’中的元素(x,y)均满足x,y∈V’,则G’=(V’,E’)称为图G的子图。
可以看到,图G的任何子图,都可以看作是图G的某个导出子图的生成子图。
************************存储***************************
1.直接列表法
直接开三个数组,记录图的每条边的起点、终点和权值。
比如右图,我们可以开三个数组u[ ],v[ ],w[ ],并存下如表所示的数值。
读入的时候就能够建立列表。
但是需要转化为其他形式才能解决和路径有关的问题。
时间复杂度O(m)
我们运用数组的静态空间,来作为链表的实际存储位置
链表和链表元素的定义
struct Edge{ int to; int data; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
加入边
for (i=0,edges=0;i<m;i++) { //第i条边起点为u[i]终点为v[i]信息为w[i]
edge[edges]=Edge(v[i],w[i],fir[u[i]]); fir[u[i]]=edge+edges; edges++; }
2.矩阵形式(邻接矩阵)
给定一个简单图G,我们可以建立一个矩阵A,使得任意两个顶点x到y之间边的有无(有时候还会有权值)都能用矩阵A的对应元素Axy表示。
这样的话,存储一个阶为n的图G,所用的时间和空间复杂度为O(n2)
优点:操作简单,在需要求所有顶点对之间最短路径的时候,或者需要使用矩阵乘法的时候有奇效(这点在讲最短路的时候详细介绍)
缺点:在面对较为稀疏的图的时候造成时间空间的大量浪费。
矩阵的定义
int edge_data[max_Vsize][max_Vsize];
加入边
for (i=0;i<m;i++) edge_data[u[i]][v[i]]=w[i];
3.链表形式(邻接链表)
对于每个顶点,我们可以用一个链表储存从它出去的边。
采取将边逐条加入链表的形式(无向边拆成2条有向边),存储具有m条边的图G所用的时间和空间复杂度为O(m)
优点:节省空间,能够应对阶更大的图
对于邻接矩阵,我们想要对一条无向边(x,y)进行操作,只要找到对应顶点的下标Axy和Ayx进行对应操作就可以了。
但是对于邻接链表,同样为了对一条无向边(x,y)进行操作,我们需要在两个链表中进行操作,如何更快地找到链表中代表这条无向边的元素呢?
最简单的方法是直接在两个链表中寻找。
但是我们也可以选择建立这两条边位置之间的数学联系,这样当找到其中一条边时,通过对位置进行数学运算就能找到反向边。
反向边的位置关系一般有2种:一种是下标±m,另一种是下标^1
******************图的遍历*****************************
1.深度优先搜索(DFS)
原则是建立一个栈,只要栈顶结点u还有相邻的点v未入过栈,就把v入栈,遍历v,继续递归地搜索,当栈顶结点u的相邻结点都入过栈时,将u出栈。
深度优先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
2. 广度优先搜索(BFS)
建立一个队列,每次从队头取出一个结点,然后将相邻的没进过队列的结点入队并遍历,然后再取出一个结点如此做。
深度优先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
【最短路】
问题1(单源最短路径问题)
性质1:给定s和t(且从s出发能够到达t),如果不存在负权回路,那么必定存在一条简单路径作为最短路。
性质2:如果从s到t的最短路se1...vk-1ekt满足k>1,那么取除最后一条边得到的se1...vk-1是从s到vk-1的最短路。(最优子结构)
性质3:若对于图G=(V,E)的起点s,s能够达到图G所有顶点,那么图G存在一个生成子图G’,并且G’是以s为根的树形图,使得对任意结点x,从s沿该树形图到x的路径均为从s到x的最短路径。
性质4:对于图G=(V,E)的任意两个顶点u,v,设从u到v的最短路径长度为duv。那么对任意三个顶点u,v,w,均有duw≤duv+dvw。(三角形不等式)
******************************实际操作********************
1.Dijkstra
每次取出未固定的结点当中,dist最小的结点nd
由前面的性质可知,nd无法被已固定的结点更新,而其余未固定的结点u也无法通过更新dist[u]使dist[u]<dist[nd],同样无法更新dist[nd],这时候dist[nd]就固定了。
固定了dist[nd]以后,为了维持前面的性质,我们拿dist[nd]去松弛与nd相邻的结点
此时对于未固定的结点v,它的临时最短路的前驱par[v]要么是nd,要么是先前固定的结点,而且那条最短路不比从其他已固定结点的最短路接续的长,这时候前面的性质又一次得到满足。
重复以上操作直到固定了dist[t]为止。
时间主要耗费在两个地方:松弛结点,找最小值
这算法的时间主要耗费在两个地方:松弛结点,找最小值。
对于找最小值的部分,如果直接枚举的话,对于固定完所有结点才能结束的最坏情况,时间复杂度可达到O(|V|2),此时松弛操作的时间复杂度为O(|E|)。总的时间复杂度为O(|V|2+|E|)。
如果我们用一个高效支持单元素修改,询问全体最小值的数据结构来记录(线段树/堆/优先队列),那么在单次修改复杂度变为O(log|V|)的前提下,单次询问的时间复杂度为O(1)。因此总的时间复杂度为O(|E|log|V|+|V|)。
不加堆优化的程序实现(使用邻接矩阵w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>dist[nd]+w[nd][i])
dist[i]=dist[nd]+w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
不加堆优化的程序实现(使用邻接链表)
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>dist[nd]+p->w)
dist[p->to]=dist[nd]+p->w;
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
加堆优化的程序实现(使用邻接链表,优先队列)
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1;
for (p=fir[nd];p!=0;p=p->next)
if (distc[p->to]>dist[nd]+p->w) {
dist[p->to]=dist[nd]+p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
这个算法无法处理负权边
2.bellman-ford
用一个数组d[ ]记录从s出发到所有结点当前的最短路径。
一开始设d[s]=0,然后枚举所有的边,利用三角形不等式更新最短距离。
不断枚举直到某一次枚举中没有结点被更新。
如果没有负权回路,算法应当在第|V|次枚举之前就求出所有结点的最短路径。
如此做的时间复杂度为O(nm)
但我们一般都不用这个算法,我们用他的儿子——SPFA
3.SPFA
用一个队列存储被更新的结点
从s出发,利用bfs每次从队列中取出一个结点对周围的结点进行松弛操作,因此而被更新d值的结点加入队列。
如此直到队列中没有元素为止。(即没有元素能够被更新时)
当队列中没有元素时,图中所有结点都找到了从起点到自身的最短路。
实现 //链表struct Edge{ int to; int dist; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
for (i=1;i<=n;i++) dist[i]=+inf, inq[i]=0; // 以1为起点,先假设不存在路径
q[0]=1; dist[1]=0; inq[1]=1; //inq表示该结点是否在队列中
for (h=0;h<t;h++) { //在n较大的时候宜改成循环队列
u=q[h]; inq[u]++; //inq为奇数表示在队内,偶数表示在队外
for (p=fir[u];p!=0;p=p->next) if (dist[v=p->to]>dist[u]+p->dist) {dist[v]=dist[u]+p->dist;
back[v]=u;
if (!(inq[v]&1)) { inq[v]++; q[t++]=v; } }
}
如果图中存在负权回路,前面的程序会死循环,这个问题后面解决。
如果图中不存在负权回路,那么任何的最短路都会是简单路径。而一个阶为n的图,到一个点的最短路所经过的路径数量最多为n-1。
我们可以证明,每求出所有某一长度的最短路径,每个点最多入队一次。
时间复杂度O(k(n+m)),其中k和最短路经过的路径条数有关。
在实际运行中,随机数据较难达到这个时间。(人品算法)
问题2(多源最短路径问题)
1floyd
这个算法的思路建立在性质1的正确性的基础之上,它考虑两个结点的最短路所经过的其它结点。
它运用动态规划的思想,从直连的边出发,逐步加入中间点,首先处理中间点只有1的最短路,然后令2也成为中间点……
最后当所有的结点都被允许成为中间点以后,任何起点和终点之间的最短路也就求出来了。
当枚举第k个中间点的时候,我们用三角形不等式,将只含有前k-1个中间点的路径u-k和v-k拼接起来,和原来的只有前k-1个中间点的u-v进行比较,记录更优的结果。
由于性质1的正确性,每个最短路不可能包含2个或以上的结点k,通过拼接可以由k-1个中间点的最短路求出k个中间点的最短路。
这个算法需要依次添加|V|个中间点,每次需要比较|V|2个结点对,因此时间复杂度为O(|V|3)。
下面是程序实现
for (mid=1;mid<=n;mid++) //每循环一次,给最短路加入一个中间点
for (le=1;le<=n;le++) //枚举拼接路径的起点和终点
for (ri=1;ri<=n;ri++)
if (dist[le][ri]>dist[le][mid]+dist[mid][ri])
dist[le][ri]=dist[le][mid]+dist[mid][ri];
******************************例题*************************
- 缺水的村子
floyd算法求出任意两间房子之间的最短路径。
- 邮递员(luogu1629)
这道题我们求出1->2,1->3,…,1->n,2->1,3->1,…,n->1的最短路径的和
对于前n-1条最短路径,直接进行dijkstra算法来求。
对于后n-1条,要求的是以1为终点的最短路径,我们可以把图中的边变为反向边,这样问题就转化为求以1为起点的最短路径了。
- poj3259
这道题只需要判断是否存在一个回路,绕着走一圈以后回到过去。
也就是是否存在负权回路的问题,可以用bellman-ford或者spfa。
【最小生成树】
- kruskal
首先证明,整个图G权值最小的边一定在最小生成树里面。
我们在将权值最小的边加入了最小生成树以后,可以将这条边所连接的两个点合成一个点考虑,然后再找下一个权值最小的连接两个不同点的边。
以此类推,我们可以把所有的边按照边权排序,先插入边权较小的边,当某条边插入时两端已经在同一个连通块,就舍弃这条边,否则就插入这条边并合并对应的连通块。
如何判断两个点所在的连通块是否相等,用并查集。
时间复杂度:排序O(|E|log|E|),并查集维护O(|E|)
程序实现(用struct Edge{ int u,v,weight; }表示边)
sort(edge,edge+m,cmp); // 按边权从小到大排序
for (i=1;i<=n;i++) fa[i]=i,rk[i]=0;
for (i=0;i<m;i++)
{
tu=top(edge[i].u);
tv=top(edge[i].v);
if (tu==tv) continue;
if (rk[tu]<rk[tv]) swap(tu,tv);
if (rk[tu]==rk[tv]) rk[tu]++;
fa[tv]=tu; ans+=weight;
}
- prim
首先证明,对于某个结点来说,以其为端点的边当中,权值最小的一条边一定在最小生成树中。(当权值最小的有多条的时候,每一条都存在某棵最小生成树包含之)
也就是说,我们可以从一个结点出发,在相邻的边当中选择一条权值最小的,加入最小生成树,然后将整个连通块当成一个结点,对外再选下一条权值最小的边。
以此类推,我们可以仿照dijkstra中对结点最短距离的维护,只是我们这次维护的是当前连通块连到这个结点的边中权值最小的一条。
不使用堆优化,时间复杂度O(|V|2)
使用堆优化,时间复杂度O(|E|log|V|+|V|)
Prim算法不加堆优化(使用邻接矩阵w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>w[nd][i])
dist[i]=w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1; ans+=dist[nd];
}
Prim算法加堆优化
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1; ans+=dist[nd];
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>p->w) {
dist[p->to]=p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
***********************应用*******************************
1poj1287
这一题,最小生成树水题,对最小生成树不熟悉的同学可以用这道题目练手。
2.在二维坐标系上有n个点,现在要给这些点之间连上线段,使得所有n个点连通的情况下,线段的长度和最小
Ans: 将每个点对之间的距离全部求出来以后,问题转换为最小生成树。
【拓扑排序】
只有有向无环图(DAG)有拓扑排序
*****************建拓扑排序的方法*********************
1.检查入度法
如果一个活动入度为0,那就表示这个活动没有前置活动,可以放在序列的最前面。将这个活动放入拓扑序列中,并且将其出度全部删除,以找到一个新的入度为0 的结点。
检查入度法的时间复杂度O(n+m)
检查入度法 //通过邻接链表组织起来的边edge[1..m]
for (i=1;i<=m;i++) rd[edge[i].to]++;
t=0; for (i=1;i<=n;i++) if (rd[i]==0) que[t++]=i;
for (h=0;h<t;h++) { //que表示拓扑顺序
u=que[h];
for (p=fir[u];p!=0;p=p->next) {
rd[p->to]--;
if (rd[p->to]==0) que[t++]=p->to;
}
}
- 深度搜索法
********************补充定义内容***********************
深度优先搜索
在深度优先搜索的过程中,所有的结点分为3类,用vis值表示
vis=0表示该结点还没访问过,
vis=1表示该结点访问过,但还没从该点返回(子孙结点还没访问完)
vis=2表示该结点已经返回了
此外,每个结点赋予一个dfn值,表示结点的入栈顺序
对于边来说,边分为树边,前向边,后向边和横叉边,其中
树边指的是从父节点找到子结点所用的边
前向边指的是从祖先结点指向其子孙结点的非树边
后向边指的是从子孙结点往回指向祖先结点的边
横叉边指的是没有祖孙关系的两个结点之间的边,起点的dfn值排在终点后面
在进行dfs的时候,我们对这些边有不同的处理方式
****************************补充完毕******************。
我们从任意的结点出发,进行深度搜索,找任意一个vis值<2的相邻结点递归下去,如果我们找到了一个vis值=1的结点,就表示我们找到一个环。
当某个结点不存在相邻结点,或者相邻结点全部vis值为2的时候,这个结点为拓扑序列的最后一位,然后返回。
说人话:先递归到最后一层,如果最后的点没有出度了,这个点就是拓扑序的最后一个,存入,然后倒叙输出。
如果出发的结点返回后仍未加入所有结点,找一个未加入的结点进行上面的操作,直到发现环或者加入所有结点为止。时间复杂度O(n+m)。
int dfs(int nd)
{
vis[nd]=1;
for(each v adjacent to nd)
{
if(vis[v]==1) return false;
if(vis[v]==0)
if(!dfs(v)) return false;
}
vis[nd]=2;
que[t++]=nd;
return true;
for(i=1;i<=n;i++)
if(vis[i]==0)
if(!dfs(i)) break;
}
************************应用***************************
1.给出一个只含有负权边的图G(|V|<2000000,|E|<3000000),求所有以任意顶点为起点的最短路径长度的最小值。
Ans:
这一题用前面提到的最短路径一定会超时。
首先如果这图有环,那么就存在负权回路,答案为-∞。
否则可以对这图进行拓扑排序,用dist[v]表示以任意起点以结点v结尾的最短路长度。
然后在进行了拓扑排序的前提下,我们发现,松弛操作只可能是前面的结点对后面的结点进行松弛。所以我们按照这个序列的先后顺序,一个个往后松弛,最后得到以所有结点结尾的最短路长度。
时间复杂度O(|V|+|E|)
2.给出一个有向无环图(|V|<2000000,|E|<3000000),顶点v处有kv元钱,现在,任意选定出发的顶点,并在任意终点结束,求最多能从该图收集到多少钱?
Ans:
首先对这一题进行拓扑排序,按照所得序列的顺序,计算到达每个顶点时能收集到的最多的钱。
用dp[v]表示走到v最多能够得多少钱,然后我们列出状态转移方程:
dp[v]=max(dp[v],dp[u]+k[v])
时间复杂度O(|V|+|E|)
3.给出一个有向图(|V|<2000000,|E|<3000000),顶点v处有kv元钱,现在,任意选定出发的顶点,并在任意终点结束,求最多能从该图收集到多少钱?
Ans:
这一题需要学习强连通分量才能解答,我们可以把这个有向图的强连通分量缩成一个点,这个图就变成DAG了。
总的时间复杂度O(|V|+|E|)。
【连通分量】
无向图G=(V,E)是连通的,当且仅当其中任意两个结点能互相到达,如果G是有向图,这种情况称G是强连通的。
对于一个连通图G来说,如果删掉了边(u,v)以后,会使得图不再连通,那么称这条边(u,v)为桥(也称割边)
如果一个连通图不包含桥的话,就意味着这个连通图无法仅靠删除一条边变为不连通图,此时称这个连通图为双连通图。
对于图G的子图G’来说,如果G’连通,那么称G’为G的连通子图。
如果图G的某个连通子图G’的顶集不是其余连通子图G’’的点集的真子集,那么称G’为G的一个连通分量。
对于强连通图和双连通图,也有类似的性质。
【tarjan】求强联通分量
tarjan算法借助dfs过程中产生的各种类型的边,旨在为每一个结点通过后向边找到dfs树下的子结点最多能够追溯到高度多低的祖先,从而确定强连通分量的范围。
我们记录dfn,表示结点的访问顺序,然后额外地记录low表示以自身为根的子树能够到达的最小高度的祖先。
如左图所示,2号结点有一个后代有直接
20171006
【【图论】】
**********************定义*****************************
在讲这个问题之前,首先我们需要了解图论中的图是什么东西。
定义:图G是一个有序二元组(V,E),其中V称为顶集(Vertices Set),其中的元素称为顶点,E称为边集(Edges set),其中的元素称为边。E与V不相交。它们亦可写成V(G)和E(G)。
边都是二元组,用(x,y)表示,其中x,y∈V。
图G可以理解为对若干个元素之间的关系的抽象表示,其中边代表着对应的顶点之间的关系。
由于图G包含两个集合,顶集和边集,因此图G的规模有两个向度:V集的大小(顶点的数量)和E集的大小(边的数量),前者又称为图G的阶。
如果图G所表述的顶点之间的关系是不能互换的,那么对于x和y之间的关系来说,x和y的相对顺序是必须考虑的,此时边(x,y)可以用有序对(x,y)表示,而图G是有向图。
************************************路径*******************
令图G=(V,E),u到w的一条路径指的是这样的一个序列:
u,e1,v1,e2,v2,..,ek,w
其中顶点和边相间,而且边表示从左边顶点到右边顶点的关系
如果这条路径除了u和w以外其余顶点都各不相等,那么称这条路径为简单路径。
如果u=w,也就是起点和终点相等,那么这条路径称为环(回路)
路径的长度定义为路径中所有边的权值的和(可能为负)
如果u到v存在路径,v到w存在路径,那u到w也存在路径。
对于无向图,u到v的路径可以通过反转(排列反转,边变为反向边)得到v到u的路径。
*******************************分类*******************
对于图G来说,如果它所表述的顶点之间的关系都是可以互换的,也就是说这个关系不关心两个顶点的相对顺序,那么边(x,y)可以用无序对(x,y)表示,此时图G是无向图。
令无向图G=(V,E),如果∀x,y∈V,x和y之间都存在路径,那么称图G为连通图。
令有向图G=(V,E),如果∀x,y∈V,x到y都存在路径,那么称图G为强连通图。
令图G=(V,E),如果对于任意的顶点x和y,x到y之间的关系都可以用最多一条边表述,也就是说V中不含两个或以上的(x,y),那么图G称为简单图。简单图可以用矩阵表示。
令无向图G=(V,E),如果图G连通,且不包含简单环。那么称图G为无向树,此时|V|-1=|E|。
令有向图G=(V,E),如果图G在把边无向化后变为无向树,那么称图G为有向树。
令有向树G=(V,E),如果存在一个顶点x使得从x能够到达其余所有顶点,那么有向树G=(V,E)称为根在x的树形图。
令图G=(V,E),取集合V’=V,然后令E’⊆E,则图G’=(V’,E’)称为图G的生成子图。特别地,若此时G’为无向树,那么称图G’为生成树。
令图G=(V,E),取集合V’⊆V,然后令E’={ (x,y)∈E | x,y∈V’ },则图G’=(V’,E’)称为图G的导出子图。
令图G=(V,E),分别取集合V’⊆V,E’⊆E,且E’中的元素(x,y)均满足x,y∈V’,则G’=(V’,E’)称为图G的子图。
可以看到,图G的任何子图,都可以看作是图G的某个导出子图的生成子图。
************************存储***************************
1.直接列表法
直接开三个数组,记录图的每条边的起点、终点和权值。
比如右图,我们可以开三个数组u[ ],v[ ],w[ ],并存下如表所示的数值。
读入的时候就能够建立列表。
但是需要转化为其他形式才能解决和路径有关的问题。
时间复杂度O(m)
我们运用数组的静态空间,来作为链表的实际存储位置
链表和链表元素的定义
struct Edge{ int to; int data; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
加入边
for (i=0,edges=0;i<m;i++) { //第i条边起点为u[i]终点为v[i]信息为w[i]
edge[edges]=Edge(v[i],w[i],fir[u[i]]); fir[u[i]]=edge+edges; edges++; }
2.矩阵形式(邻接矩阵)
给定一个简单图G,我们可以建立一个矩阵A,使得任意两个顶点x到y之间边的有无(有时候还会有权值)都能用矩阵A的对应元素Axy表示。
这样的话,存储一个阶为n的图G,所用的时间和空间复杂度为O(n2)
优点:操作简单,在需要求所有顶点对之间最短路径的时候,或者需要使用矩阵乘法的时候有奇效(这点在讲最短路的时候详细介绍)
缺点:在面对较为稀疏的图的时候造成时间空间的大量浪费。
矩阵的定义
int edge_data[max_Vsize][max_Vsize];
加入边
for (i=0;i<m;i++) edge_data[u[i]][v[i]]=w[i];
3.链表形式(邻接链表)
对于每个顶点,我们可以用一个链表储存从它出去的边。
采取将边逐条加入链表的形式(无向边拆成2条有向边),存储具有m条边的图G所用的时间和空间复杂度为O(m)
优点:节省空间,能够应对阶更大的图
对于邻接矩阵,我们想要对一条无向边(x,y)进行操作,只要找到对应顶点的下标Axy和Ayx进行对应操作就可以了。
但是对于邻接链表,同样为了对一条无向边(x,y)进行操作,我们需要在两个链表中进行操作,如何更快地找到链表中代表这条无向边的元素呢?
最简单的方法是直接在两个链表中寻找。
但是我们也可以选择建立这两条边位置之间的数学联系,这样当找到其中一条边时,通过对位置进行数学运算就能找到反向边。
反向边的位置关系一般有2种:一种是下标±m,另一种是下标^1
******************图的遍历*****************************
1.深度优先搜索(DFS)
原则是建立一个栈,只要栈顶结点u还有相邻的点v未入过栈,就把v入栈,遍历v,继续递归地搜索,当栈顶结点u的相邻结点都入过栈时,将u出栈。
深度优先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
2. 广度优先搜索(BFS)
建立一个队列,每次从队头取出一个结点,然后将相邻的没进过队列的结点入队并遍历,然后再取出一个结点如此做。
深度优先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
【最短路】
问题1(单源最短路径问题)
性质1:给定s和t(且从s出发能够到达t),如果不存在负权回路,那么必定存在一条简单路径作为最短路。
性质2:如果从s到t的最短路se1...vk-1ekt满足k>1,那么取除最后一条边得到的se1...vk-1是从s到vk-1的最短路。(最优子结构)
性质3:若对于图G=(V,E)的起点s,s能够达到图G所有顶点,那么图G存在一个生成子图G’,并且G’是以s为根的树形图,使得对任意结点x,从s沿该树形图到x的路径均为从s到x的最短路径。
性质4:对于图G=(V,E)的任意两个顶点u,v,设从u到v的最短路径长度为duv。那么对任意三个顶点u,v,w,均有duw≤duv+dvw。(三角形不等式)
******************************实际操作********************
1.Dijkstra
每次取出未固定的结点当中,dist最小的结点nd
由前面的性质可知,nd无法被已固定的结点更新,而其余未固定的结点u也无法通过更新dist[u]使dist[u]<dist[nd],同样无法更新dist[nd],这时候dist[nd]就固定了。
固定了dist[nd]以后,为了维持前面的性质,我们拿dist[nd]去松弛与nd相邻的结点
此时对于未固定的结点v,它的临时最短路的前驱par[v]要么是nd,要么是先前固定的结点,而且那条最短路不比从其他已固定结点的最短路接续的长,这时候前面的性质又一次得到满足。
重复以上操作直到固定了dist[t]为止。
时间主要耗费在两个地方:松弛结点,找最小值
这算法的时间主要耗费在两个地方:松弛结点,找最小值。
对于找最小值的部分,如果直接枚举的话,对于固定完所有结点才能结束的最坏情况,时间复杂度可达到O(|V|2),此时松弛操作的时间复杂度为O(|E|)。总的时间复杂度为O(|V|2+|E|)。
如果我们用一个高效支持单元素修改,询问全体最小值的数据结构来记录(线段树/堆/优先队列),那么在单次修改复杂度变为O(log|V|)的前提下,单次询问的时间复杂度为O(1)。因此总的时间复杂度为O(|E|log|V|+|V|)。
不加堆优化的程序实现(使用邻接矩阵w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>dist[nd]+w[nd][i])
dist[i]=dist[nd]+w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
不加堆优化的程序实现(使用邻接链表)
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>dist[nd]+p->w)
dist[p->to]=dist[nd]+p->w;
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
加堆优化的程序实现(使用邻接链表,优先队列)
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1;
for (p=fir[nd];p!=0;p=p->next)
if (distc[p->to]>dist[nd]+p->w) {
dist[p->to]=dist[nd]+p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
这个算法无法处理负权边
2.bellman-ford
用一个数组d[ ]记录从s出发到所有结点当前的最短路径。
一开始设d[s]=0,然后枚举所有的边,利用三角形不等式更新最短距离。
不断枚举直到某一次枚举中没有结点被更新。
如果没有负权回路,算法应当在第|V|次枚举之前就求出所有结点的最短路径。
如此做的时间复杂度为O(nm)
但我们一般都不用这个算法,我们用他的儿子——SPFA
3.SPFA
用一个队列存储被更新的结点
从s出发,利用bfs每次从队列中取出一个结点对周围的结点进行松弛操作,因此而被更新d值的结点加入队列。
如此直到队列中没有元素为止。(即没有元素能够被更新时)
当队列中没有元素时,图中所有结点都找到了从起点到自身的最短路。
实现 //链表struct Edge{ int to; int dist; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
for (i=1;i<=n;i++) dist[i]=+inf, inq[i]=0; // 以1为起点,先假设不存在路径
q[0]=1; dist[1]=0; inq[1]=1; //inq表示该结点是否在队列中
for (h=0;h<t;h++) { //在n较大的时候宜改成循环队列
u=q[h]; inq[u]++; //inq为奇数表示在队内,偶数表示在队外
for (p=fir[u];p!=0;p=p->next) if (dist[v=p->to]>dist[u]+p->dist) {dist[v]=dist[u]+p->dist;
back[v]=u;
if (!(inq[v]&1)) { inq[v]++; q[t++]=v; } }
}
如果图中存在负权回路,前面的程序会死循环,这个问题后面解决。
如果图中不存在负权回路,那么任何的最短路都会是简单路径。而一个阶为n的图,到一个点的最短路所经过的路径数量最多为n-1。
我们可以证明,每求出所有某一长度的最短路径,每个点最多入队一次。
时间复杂度O(k(n+m)),其中k和最短路经过的路径条数有关。
在实际运行中,随机数据较难达到这个时间。(人品算法)
问题2(多源最短路径问题)
1floyd
这个算法的思路建立在性质1的正确性的基础之上,它考虑两个结点的最短路所经过的其它结点。
它运用动态规划的思想,从直连的边出发,逐步加入中间点,首先处理中间点只有1的最短路,然后令2也成为中间点……
最后当所有的结点都被允许成为中间点以后,任何起点和终点之间的最短路也就求出来了。
当枚举第k个中间点的时候,我们用三角形不等式,将只含有前k-1个中间点的路径u-k和v-k拼接起来,和原来的只有前k-1个中间点的u-v进行比较,记录更优的结果。
由于性质1的正确性,每个最短路不可能包含2个或以上的结点k,通过拼接可以由k-1个中间点的最短路求出k个中间点的最短路。
这个算法需要依次添加|V|个中间点,每次需要比较|V|2个结点对,因此时间复杂度为O(|V|3)。
下面是程序实现
for (mid=1;mid<=n;mid++) //每循环一次,给最短路加入一个中间点
for (le=1;le<=n;le++) //枚举拼接路径的起点和终点
for (ri=1;ri<=n;ri++)
if (dist[le][ri]>dist[le][mid]+dist[mid][ri])
dist[le][ri]=dist[le][mid]+dist[mid][ri];
******************************例题*************************
- 缺水的村子
floyd算法求出任意两间房子之间的最短路径。
- 邮递员(luogu1629)
这道题我们求出1->2,1->3,…,1->n,2->1,3->1,…,n->1的最短路径的和
对于前n-1条最短路径,直接进行dijkstra算法来求。
对于后n-1条,要求的是以1为终点的最短路径,我们可以把图中的边变为反向边,这样问题就转化为求以1为起点的最短路径了。
- poj3259
这道题只需要判断是否存在一个回路,绕着走一圈以后回到过去。
也就是是否存在负权回路的问题,可以用bellman-ford或者spfa。
【最小生成树】
- kruskal
首先证明,整个图G权值最小的边一定在最小生成树里面。
我们在将权值最小的边加入了最小生成树以后,可以将这条边所连接的两个点合成一个点考虑,然后再找下一个权值最小的连接两个不同点的边。
以此类推,我们可以把所有的边按照边权排序,先插入边权较小的边,当某条边插入时两端已经在同一个连通块,就舍弃这条边,否则就插入这条边并合并对应的连通块。
如何判断两个点所在的连通块是否相等,用并查集。
时间复杂度:排序O(|E|log|E|),并查集维护O(|E|)
程序实现(用struct Edge{ int u,v,weight; }表示边)
sort(edge,edge+m,cmp); // 按边权从小到大排序
for (i=1;i<=n;i++) fa[i]=i,rk[i]=0;
for (i=0;i<m;i++)
{
tu=top(edge[i].u);
tv=top(edge[i].v);
if (tu==tv) continue;
if (rk[tu]<rk[tv]) swap(tu,tv);
if (rk[tu]==rk[tv]) rk[tu]++;
fa[tv]=tu; ans+=weight;
}
- prim
首先证明,对于某个结点来说,以其为端点的边当中,权值最小的一条边一定在最小生成树中。(当权值最小的有多条的时候,每一条都存在某棵最小生成树包含之)
也就是说,我们可以从一个结点出发,在相邻的边当中选择一条权值最小的,加入最小生成树,然后将整个连通块当成一个结点,对外再选下一条权值最小的边。
以此类推,我们可以仿照dijkstra中对结点最短距离的维护,只是我们这次维护的是当前连通块连到这个结点的边中权值最小的一条。
不使用堆优化,时间复杂度O(|V|2)
使用堆优化,时间复杂度O(|E|log|V|+|V|)
Prim算法不加堆优化(使用邻接矩阵w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>w[nd][i])
dist[i]=w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1; ans+=dist[nd];
}
Prim算法加堆优化
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1; ans+=dist[nd];
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>p->w) {
dist[p->to]=p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
***********************应用*******************************
1poj1287
这一题,最小生成树水题,对最小生成树不熟悉的同学可以用这道题目练手。
2.在二维坐标系上有n个点,现在要给这些点之间连上线段,使得所有n个点连通的情况下,线段的长度和最小
Ans: 将每个点对之间的距离全部求出来以后,问题转换为最小生成树。
【拓扑排序】
只有有向无环图(DAG)有拓扑排序
*****************建拓扑排序的方法*********************
1.检查入度法
如果一个活动入度为0,那就表示这个活动没有前置活动,可以放在序列的最前面。将这个活动放入拓扑序列中,并且将其出度全部删除,以找到一个新的入度为0 的结点。
检查入度法的时间复杂度O(n+m)
检查入度法 //通过邻接链表组织起来的边edge[1..m]
for (i=1;i<=m;i++) rd[edge[i].to]++;
t=0; for (i=1;i<=n;i++) if (rd[i]==0) que[t++]=i;
for (h=0;h<t;h++) { //que表示拓扑顺序
u=que[h];
for (p=fir[u];p!=0;p=p->next) {
rd[p->to]--;
if (rd[p->to]==0) que[t++]=p->to;
}
}
- 深度搜索法
********************补充定义内容***********************
深度优先搜索
在深度优先搜索的过程中,所有的结点分为3类,用vis值表示
vis=0表示该结点还没访问过,
vis=1表示该结点访问过,但还没从该点返回(子孙结点还没访问完)
vis=2表示该结点已经返回了
此外,每个结点赋予一个dfn值,表示结点的入栈顺序
对于边来说,边分为树边,前向边,后向边和横叉边,其中
树边指的是从父节点找到子结点所用的边
前向边指的是从祖先结点指向其子孙结点的非树边
后向边指的是从子孙结点往回指向祖先结点的边
横叉边指的是没有祖孙关系的两个结点之间的边,起点的dfn值排在终点后面
在进行dfs的时候,我们对这些边有不同的处理方式
****************************补充完毕******************。
我们从任意的结点出发,进行深度搜索,找任意一个vis值<2的相邻结点递归下去,如果我们找到了一个vis值=1的结点,就表示我们找到一个环。
当某个结点不存在相邻结点,或者相邻结点全部vis值为2的时候,这个结点为拓扑序列的最后一位,然后返回。
说人话:先递归到最后一层,如果最后的点没有出度了,这个点就是拓扑序的最后一个,存入,然后倒叙输出。
如果出发的结点返回后仍未加入所有结点,找一个未加入的结点进行上面的操作,直到发现环或者加入所有结点为止。时间复杂度O(n+m)。
int dfs(int nd)
{
vis[nd]=1;
for(each v adjacent to nd)
{
if(vis[v]==1) return false;
if(vis[v]==0)
if(!dfs(v)) return false;
}
vis[nd]=2;
que[t++]=nd;
return true;
for(i=1;i<=n;i++)
if(vis[i]==0)
if(!dfs(i)) break;
}
************************应用***************************
1.给出一个只含有负权边的图G(|V|<2000000,|E|<3000000),求所有以任意顶点为起点的最短路径长度的最小值。
Ans:
这一题用前面提到的最短路径一定会超时。
首先如果这图有环,那么就存在负权回路,答案为-∞。
否则可以对这图进行拓扑排序,用dist[v]表示以任意起点以结点v结尾的最短路长度。
然后在进行了拓扑排序的前提下,我们发现,松弛操作只可能是前面的结点对后面的结点进行松弛。所以我们按照这个序列的先后顺序,一个个往后松弛,最后得到以所有结点结尾的最短路长度。
时间复杂度O(|V|+|E|)
2.给出一个有向无环图(|V|<2000000,|E|<3000000),顶点v处有kv元钱,现在,任意选定出发的顶点,并在任意终点结束,求最多能从该图收集到多少钱?
Ans:
首先对这一题进行拓扑排序,按照所得序列的顺序,计算到达每个顶点时能收集到的最多的钱。
用dp[v]表示走到v最多能够得多少钱,然后我们列出状态转移方程:
dp[v]=max(dp[v],dp[u]+k[v])
时间复杂度O(|V|+|E|)
3.给出一个有向图(|V|<2000000,|E|<3000000),顶点v处有kv元钱,现在,任意选定出发的顶点,并在任意终点结束,求最多能从该图收集到多少钱?
Ans:
这一题需要学习强连通分量才能解答,我们可以把这个有向图的强连通分量缩成一个点,这个图就变成DAG了。
总的时间复杂度O(|V|+|E|)。
【连通分量】
无向图G=(V,E)是连通的,当且仅当其中任意两个结点能互相到达,如果G是有向图,这种情况称G是强连通的。
对于一个连通图G来说,如果删掉了边(u,v)以后,会使得图不再连通,那么称这条边(u,v)为桥(也称割边)
如果一个连通图不包含桥的话,就意味着这个连通图无法仅靠删除一条边变为不连通图,此时称这个连通图为双连通图。
对于图G的子图G’来说,如果G’连通,那么称G’为G的连通子图。
如果图G的某个连通子图G’的顶集不是其余连通子图G’’的点集的真子集,那么称G’为G的一个连通分量。
对于强连通图和双连通图,也有类似的性质。
【tarjan】求强联通分量
tarjan算法借助dfs过程中产生的各种类型的边,旨在为每一个结点通过后向边找到dfs树下的子结点最多能够追溯到高度多低的祖先,从而确定强连通分量的范围。
我们记录dfn,表示结点的访问顺序,然后额外地记录low表示以自身为根的子树能够到达的最小高度的祖先。
如左图所示,2号结点有一个后代有直接连向1号点的边
所以low[2]=1
操作:在进行dfs的时候,我们不断把和上面失去联系(dfn[nd]=low[nd])的子树从树上删除,把它们划分为独立一个强连通分量,那么怎么维护low值呢?
对于当前结点nd,枚举结点v
如果vis[v]=0,访问v,在v返回的时候,如果v没有失去联系,那么low[v]表示的v能到达的祖先,nd也能到达,我们有low[nd]=min(low[nd],low[v])
如果v访问过,而且没被切出去,那么我们有low[nd]=min(low[nd],dfn[v])
当nd访问完所有结点,而且dfn[nd]==low[nd],那么就意味着nd与上面失去了联系,删除该子树并将其结点划分为一个强连通分量
我们划分好强连通分量的时候,就意味着不同的强连通分量u和v之间不可能互相到达,这时候将所有的强连通分量缩成一个点,就能得到一个有向无环图。
然后配合拓扑排序,就能找到那些互为前提的问题,用另外的方法解决。(拓扑排序例题3)
连向1号点的边
所以low[2]=1
操作:在进行dfs的时候,我们不断把和上面失去联系(dfn[nd]=low[nd])的子树从树上删除,把它们划分为独立一个强连通分量,那么怎么维护low值呢?
对于当前结点nd,枚举结点v
如果vis[v]=0,访问v,在v返回的时候,如果v没有失去联系,那么low[v]表示的v能到达的祖先,nd也能到达,我们有low[nd]=min(low[nd],low[v])
如果v访问过,而且没被切出去,那么我们有low[nd]=min(low[nd],dfn[v])
当nd访问完所有结点,而且dfn[nd]==low[nd],那么就意味着nd与上面失去了联系,删除该子树并将其结点划分为一个强连通分量
我们划分好强连通分量的时候,就意味着不同的强连通分量u和v之间不可能互相到达,这时候将所有的强连通分量缩成一个点,就能得到一个有向无环图。
然后配合拓扑排序,就能找到那些互为前提的问题,用另外的方法解决。(拓扑排序例题3)