基础图论总结
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础图论总结相关的知识,希望对你有一定的参考价值。
在此之前需要先学会基本数据结构,递归以及搜索、回溯
用了半个月的时间终于搞完了全部的基础图论。。。仅介绍到差分约束
图的定义自己百度= =这里直接开始说图的存储。
图的存储有N种写法,我所知道的有邻接矩阵、边表、邻接表、前向星、边集数组、十字链表、邻接多重表,这里只讨论较容易实现的3种也是最为普遍的3种:邻接矩阵,边表和邻接表。
先说说邻接矩阵吧。
邻接矩阵是一种图的直接的储存方式,对于稠密图来说,邻接矩阵的效率要高于邻接表以及边表,但是如其名,我们需要一个n*n的二维数组,所以其空间复杂度是很大的(相对于稀疏图中的邻接表和边表),因此我们仅在某些情况下使用邻接矩阵。如:Floyed(本身时间复杂度就是n*n*n),Dijkstra(堆优化则需要用邻接表),Prim(同Dijkstra),Topsort(可用邻接表写法),这些在之后的算法中会具体讲到。邻接矩阵大概是最简单的存储图的方式了,对于a[i][j](i,j∈V)来说,其代表的就是点i与点j之间的连通性,当然,a[i][j]的值本身也可代表i与j之间的边的权值(需要注意的是邻接矩阵是无法存重边的,因此对于某些题来说对于重边需要特殊处理);
然后是边表。边表也是一种直接的储存方式,但其只是单纯地储存边的信息,无法像邻接表和邻接矩阵一样能够很快地遍历整张图,但是Kruskal和Bellman-Ford中需要用到边表,我们会在最小生成树中提到该算法。其储存方法如下(在此只介绍结构体的储存方法,其实还可以用vector来储存):
1 struct edge{ 2 int x,y,v; 3 }edge[MAX+10];//x为始端,y为末端,v为权值
之后是邻接表。邻接表在稀疏图中是一种比较高效的储存方法,因为它仅仅储存边,而不是将所有点与点之间的关系用二维数组来表示,因此在稀疏图中的效率要高于邻接矩阵。但邻接表与边表又有不同,邻接表采用的是链表的形式,表中不存起始点,仅仅存储终点,权值以及一个指针,该指针指向的是以相同的点为起点的下一条边的位置(下一条边指的是在这条边之前读入的边),同时我们还需要一个head数组,表示以每个点为起点的从后往前的第一条边的位置,和边表一样,这里只介绍结构体的储存方法,不介绍vector的储存。
1 struct edge{ 2 int y,v,ne; 3 }edge[MAX+10];//y为边的末端,v为权值,ne为以当前边为起点记录的下一条边的位置 4 int head[MAX+10],len=0;//head[i]指向以i为起点的第一条边,len为邻接表的总长度 5 void addedge(int x,int y,int v)//加入一条以x为起点,终点为y的边,类似链表的操作 6 { 7 edge[++len].v=v; 8 edge[len].ne=head[x]; 9 edge[len].y=y; 10 head[x]=len; 11 } 12 13 void init()//读入 14 { 15 memset(head,0,sizeof(head)); 16 scanf("%d%d",&n,&m); 17 int x,y,v 18 for (int i=1;i<=n;i++) 19 { 20 scanf("%d%d%d",&x,&y,&v); 21 addedge(x,y,v); 22 addedge(y,x,v);//无向图 23 } 24 }
现在我们知道图的存储了,那么我们应该如何遍历这个图呢?
同搜索一样,我们可以采用DFS和BFS的方法对图进行遍历,DFS是以深度为优先进行搜索的,而BFS是以广度为优先进行搜索的。对于DFS来说,我们从一个点开始遍历,当出现连通时,我们就从下一个点开始继续遍历,因此DFS是“一条道走到黑”的,由于其用的是递归的方法进行搜索的,所以我们可以对遍历到的点进行回溯(在判断欧拉回路的时候需要使用)。而BFS就不一样了。它采用的是用队列的方式进行存储,以队列的头元素为起点,将其直接连向的,且没有被访问过的点全部加入到数组中。 这两种算法的效率大概是相同的,但需要注意的是如果不用栈进行模拟DFS的话,在递归层数较多的情况下是会爆栈的,BFS则不会。 还有一点:DFS可以进行回溯,而BFS则不能。
下面给出DFS和BFS的模板
1 //遍历——邻接矩阵DFS(数据较小时使用,较大时需用栈模拟) 2 void dfs(int p) 3 { 4 for (int i=1;i<=n;i++) 5 if (a[p][i]&&!f[i]) 6 { 7 f[i]=false; 8 dfs(i); 9 f[i]=true;//根据情况加上回溯 10 } 11 } 12 13 //遍历——邻接表DFS 14 void dfs(int p) 15 { 16 for (int i=head[p];i;i=edge[i].ne) 17 if (!f[edge[i].y]) 18 { 19 f[edge[i].y]=true; 20 dfs(edge[i].y); 21 f[edge[i].y]=false;//根据情况加上回溯 22 } 23 } 24 25 //遍历——邻接矩阵BFS 26 int queue[MAX+10]; 27 void bfs(int p) 28 { 29 int h=0;tail=1; 30 queue[1]=p; f[p]=true; 31 while (h<tail) 32 { 33 i=queue[++h]; 34 for (int j=1;i<=n;j++) 35 if (a[i][j]&&!f[j]) 36 { 37 f[j]=true; 38 queue[++tail]=j; 39 } 40 } 41 } 42 43 //遍历——邻接表BFS 44 void bfs(int p) 45 { 46 int h=0;tail=1; 47 queue[1]=p; f[p]=true; 48 while (h++<tail) 49 for (int i=head[queue[h]];i;i=edge[i].ne)//将以当前起点为起点的所有边的终点加入到队列中 50 if (!f[edge[i].y]) 51 { 52 f[edge[i].y]=true; 53 queue[++tail]=edge[i].y; 54 } 55 }
接下来是最短路问题。
稍有常识的OIer都会知道,一个三角形的任意两边之和是大于第三边的,但对于图来说并非不会出现两遍之和小于第三边的情况。这既是最短路的来源。因为不遵循三角形的性质,所以需要我们对i到j的边进行判断,如果说以k为中间点,i->k->j的耗费要大于i->j,那么我们需要对i到j的边进行更新,这就是图论中代码实现最简单的代码——Floyd的思路。代码如下:
1 void floyd() 2 { 3 for (int i=1;i<=n;i++) 4 a[i][i]=0;//如果没有负边的话需要将i到i的权值初始化为1 5 for (int k=1;k<=n;k++) 6 for (int i=1;i<=n;i++) 7 for (int j=1;j<=n;j++) 8 if (a[i][k]+a[k][j]<a[i][j]) 9 a[i][j]=a[i][k]+a[k][j]; 10 }
需要注意的是k是放在最外层的,而且k作为中间点时只能放在最外层。想一想为什么?这里我们采用反证法:
如果将i,j放在外层的话,i,j关于中间点k会进行更新,这样的话最终得到的i,j之间的最短路,我们实际上只更新了一遍,显而易见用这样的方法所找到的最短路不一定是最短的。
那么将k放在外层呢? 基于每个点i,j来说,都会关于其他的所有点更新一次,这样我们就将这个点更新了n次,所求得的点一定是最大值。
这样的话Floyd其实采用的是动态规划的思想,k所代表的是当前的状态,不能将其放入内层。
(PS:Floyd求得的是全源最短路,且允许出现负权路)
Floyd的三层循环注定会耗费很长的时间,其复杂度为V*V*V,所以这需要我们找一个更加简便的方法。
如果说我每次询问i到j的最短路,那么是否需要将全源最短路求出来?事实上是不需要的,我们只需要基于源点对源点到其他点的最短路算出来就好了。仔细想想的话,我们需要以除了i之外的其它点作为中间点,将i到其它所有点的最短路更新n-1次即可。但策略并没有这么简单,我们是不是要按照点的顺序来求最短路?用贪心的思想来解的话,其实是必须找到当前i到其它点的最短距离,然后将这个最短距离作为中间点,对其它的点进行更新,我们称这样的算法为点i基于点j的一次松弛。这样的话当前已经松弛过的点一定是已经求出来最短路的点。这就是Dijkstra的思想。代码如下:
1 int d[MAX];//d为从源点到其他所有点的距离 2 bool vis[MAX];//记录某个点是否已经松弛过 3 void dijkstra(int s) 4 { 5 for (int i=1;i<=n;i++) dis[i]=a[s][i]; 6 memset(vis,false,sizeof(vis)); 7 for (int i=1;i<=n;i++) 8 { 9 int minn=MAX; 10 int k=0; 11 for (int j=1;j<=n;j++) 12 if (!vis[j]&&dis[j]<minn) 13 { 14 minn=dis[j]; 15 k=j; 16 } 17 if (k==0) return;//找不到可松弛的点 18 vis[k]=false; 19 for (int j=1;j<=n;j++) 20 if (!vis[j]&&d[k]+a[k][j]<d[j]) 21 d[j]=d[k]+a[k][j]; 22 } 23 }
这样的算法,时间复杂度为V*V,如果要搜索全源最短路的话,其实效率是不如Floyd的。
另外想一下:是否有什么能够优化的算法?注意:我们每次找的都是最短的边。
我们学过一种数据结构,这种数据结构的顶端是所有数据中的最小/最大数据。没错就是堆,我们可以用堆,对Dijkstra进行优化,将所能枚举到的边放入小根堆中,每次取堆顶,如果这条边的末端的点还未被松弛,那么我们就关于这个点进行松弛。但是堆中只存储一个值的话显而易见是得不到边的具体信息的。
需要进行适当的拓展:C++的stl中内置了一种只含两个元素的结构体——pair,在没有进行重定义的情况下,会自动比较pair中的第一个元素,再比较第二个元素,这样的话我们可以定义一个pair量,其中存储的第一个元素是边的权值,第二个元素是边的下标,这样我们就可以进行Dijkstra了。代码如下:
1 #include<utility> 2 #include<queue> 3 typedef pair<int,int> pii;//第一个元素储存值,第二个元素储存下标 4 void dijkstra(int s) 5 { 6 priority_queue <pii,vector<pii>,greater<pii> > q; 7 memset(d,0x3f,sizeof(d)); 8 d[s]=0; 9 memset(vis,false,sizeof(vis)); 10 q.push(make_pair(d[s],s)); 11 while (!q.empty()) 12 { 13 pii tmp=q.top(); 14 q.pop(); 15 int x=tmp.second; 16 if (vis[x]) continue; 17 vis[x]=true; 18 for (int i=head[x];i;i=edge[x].ne) 19 if (d[edge[i].y]>d[x]+edge[i].v) 20 { 21 d[edge[i].y]=d[x]+edge[i].v; 22 q.push(make_pair(d[edge[i].y],edge[i].y)); 23 } 24 } 25 }
注意要用邻接表。时间效率是V*logE,显然快了许多。(不用结构体的原因其实就是懒得重定义(逃))
但是Dijkstra是不能求带有负权路的最短路径的,因为当出现负权路时,已经松弛过的点还能继续进行松弛,而Dijkstra是基于每个点只松弛一次,所以求不出带负权边的最短路。
这时候我们还是只需要更新n次,每次基于所有边进行松弛,如果不能松弛则退出。这就是Bellman-Ford的思路。代码如下:
1 void bellmanford(int s) 2 { 3 memset(dis,0X3f,sizeof(dis)); 4 dis[s]=0; 5 bool rel; 6 for (int i=1;i<=n;i++) 7 { 8 rel=false; 9 for (int j=1;j<=len;j++) 10 if (dis[edge[j].y]>dis[edge[j].x]+edge[j].v) 11 { 12 dis[edge[j].y]=dis[edge[j].x]+edge[j].v; 13 rel=true; 14 } 15 if (!rel) return; 16 } 17 }
需要注意的是Bellman-Ford使用的是边表,时间复杂度为V*E。
因为Bellman的特性,我们可以对其进行优化,我们用队列维护所有更新过的点,每次取队头的点进行更新,当更新到最短路时就将最短路的终点加入队列中(前提是这个点不在队列中),这就是SPFA的大致思路。代码如下:
1 int queue[MAX]; 2 void spfa(int s) 3 { 4 int Head=0,tail=1; 5 memset(vis,false,sizeof(vis)); 6 memset(dis,0x3f,sizeof(dis)); 7 queue[1]=s; dis[s]=0; vis[s]=true; 8 while (Head<tail) 9 { 10 int tn=queue[++Head]; 11 vis[tn]=false; 12 int te=head[tn]; 13 for (int i=te;i;i=edge[i].ne) 14 { 15 int tmp=edge[i].y; 16 if (dis[tmp]>dis[tn]+edge[i].v) 17 { 18 dis[tmp]=dis[tn]+edge[i].v; 19 if (!vis[tmp]) 20 { 21 vis[tmp]=true; 22 queue[++tail]=tmp; 23 } 24 } 25 } 26 } 27 }
注意:邻接表
我们可以发现,无论是哪种算法,都是严格按照三角形定律进行更新的,即:如果两边之和小于第三边,那么这两边之和就是起点到终点的新的最短路。
接下来是图的最小生成树问题。
所谓的生成树,就是通过点与点之间的关系,将某个点作为整个生成树的根,连接图中的所有点。最小生成树就是求生成树中用到的边权值的最小和。
需要注意的几点:1、我们求得的生成树连接了所有的点,因此图必须保证是连通的。2、已经连接到的点是否需要再次进行连接?我们可以运用树的特性来证明这一点:在一个树中,根节点没有父节点,除了根节点之外的所有点的父节点只有一个,因此不需要连接重复的点。这样的话我们就可以建立一个bool数组,记录每个点是否已经在最小生成树中。3、每次应该如何取边?同样是贪心的方法:我们用一个dis数组记录当前已经搜索到的能到达某个点的最短边,每次取出最短的边,然后将这个边的末端的点进行更新,即如果这个点能够连接到的边权值小于当前已知的最小边权值时,用这个边权值来替代它。这就是Prim算法的具体思想,代码实现和Dijkstra很相似,如下:
1 void prim(int s) 2 { 3 memset(dis,0x3f,sizeof(dis)); 4 memset(vis,false,sizeof(vis)); 5 for (int i=1;i<=n;i++) dis[i]=a[s][i]; 6 vis[s]=true; sumn=0;//只有点s已做过松弛 7 for (int i=2;i<=n;i++) 8 { 9 int minn=MAX,c=0; 10 for (int j=1;j<=n;j++)//搜索能到达的最短的边 11 if (!vis[j]&&dis[j]<minn) 12 { 13 minn=dis[j]; 14 c=j; 15 } 16 vis[c]=true; 17 sumn+=minn; 18 for (int j=1;j<=n;j++)//基于这个点进行松弛 19 if (a[c][j]<dis[j]&&!vis[j]) 20 dis[j]=a[c][j]; 21 } 22 }
注意这里使用了邻接矩阵,因此复杂度是V*V的。因为其思想与Dijkstra相似,所以同样也可以进行堆优化,堆优化的思路与Dijkstra的堆优化思路相似,这里不作证明,只给出代码:
1 typedef pair <int,int> pii; 2 void prim(int s) 3 { 4 priority_queue<pii,vector<pii>,greater<pii> > q; 5 memset(vis,false,sizeof(vis)); 6 memset(dis,0,sizeof(dis)); 7 vis[s]=true; sumn=0; 8 for (int i=head[s];i;i=edge[i].ne){ 9 q.push(make_pair(edge[i].v,edge[i].y)); 10 dis[edge[i].y]=edge[i].v; 11 } 12 for (int i=2;i<=n;i++){ 13 pii a=q.top(); q.pop(); 14 int minn=a.first,p=a.second; 15 while (vis[p]){ 16 pii a=q.top(); q.pop(); 17 minn=a.first,p=a.second; 18 } 19 vis[p]=true; 20 sumn+=minn; 21 for (int i=head[p];i;i=edge[i].ne) 22 if (!vis[edge[i].y]) q.push(make_pair(edge[i].v,edge[i].y)); 23 } 24 }
这个代码未经证明,使用的时候需要注意一下。堆优化的Prim同样也是使用了邻接表,时间复杂度也是V*logE。
由Prim算法的证明我们可以得知每次取的都是最短的边,这样的话我们可以想到另外一种算法,既然取最短的边的话,我们可以使用边表,将边的权值按照从小到大进行排列,这样的话我们只需要一次遍历就能求出来最小生成树了,问题又来了:我们如何判断是否将当前边所连接的点已经在生成树中?同样根据树的特性我们可以采用并查集的方法将点进行合并,如果当前边所连接的点不在生成树中就将其加入生成树中,这就是Kruskal算法的大致思路,代码如下:
1 #include<algorithm> 2 struct edges{ 3 int x,y,v; 4 }edge[MAX]; 5 int father[x]; 6 int getfather(int x) 7 {return (father[x]==x)? x:father[x]=getfather(father[x]);} 8 9 bool mycmp(edges x,edges y) 10 {return x.v<y.v;} 11 12 void kruskal() 13 { 14 for (int i=1;i<=n;i++) father[i]=i; 15 sort(edge+1,edge+1+len,mycmp); 16 int cnt=0; 17 for (int i=1;i<=len;i++){ 18 int v=getfather(edge[i].x); 19 int u=getfather(edge[i].y); 20 if (v!=u){ 21 father[v]=u; 22 if (++cal==n-1) 23 return; 24 } 25 } 26 }
我们可以发现这种算法的复杂度基本是由排序算法的复杂度决定的,我们使用了快排,因此复杂度为E*logE。
接下来是拓扑排序(Topsort),Topsort维护的是图中的先后顺序,因此当图中出现环的时候是无法求出拓扑序的。那么当图中没有环时应该如何进行拓扑排序?仔细想想,我们可以记录所有点的入度,然后将所有入度为0的点入队,每次枚举队头,将其能到达的点的入度减一,我们称这个操作为删边,当某个点的入度为0时就将其进入队列,这是BFS求拓扑序的思路,DFS思路和图的遍历的DFS方法是差不多的,注意删边。
仔细想想:我们根据BFS的算法思路可以得知一次BFS只能求出一种拓扑序,如果要求出所有的拓扑序的话,我们需要使用BFS,并在BFS上加入回溯即可。
Topsort算法代码如下:
1 //Topsort(邻接矩阵,队列,BFS) 2 void topsort() 3 { 4 int head=0,tail=0; 5 for (int i=1;i<=n;i++)//初始化队列,使队列中所有入度为0的点入队 6 if (id[i]==0) queue[++tail]=i;//id为i的入度 7 while (head<tail){ 8 int i=queue[++head]; 9 for (int j=1;j<=n;j++) 10 if (a[i][j]){ 11 id[j]--; 12 if (id[j]==0) queue[++tail]=j; 13 } 14 } 15 } 16 17 //Topsort(邻接表,队列,BFS) 18 void topsort() 19 { 20 int Head=0,tail=0; 21 for (int i=1;i<=n;i++) 22 if (id[i]==0) queue[++tail]=i; 23 while (Head<tail){ 24 int te=queue[++Head]; 25 int tn=head[te]; 26 for (;tn!=-1;tn=edge[tn].ne){ 27 id[edge[tn].y]--; 28 if (id[edge[tn].y]==0) queue[++tail]=edge[tn].y; 29 } 30 } 31 } 32 33 //Topsort(邻接矩阵,DFS,可求出所有拓扑序) 34 void topsort(int i,int sum)//i为当前元素的位置,sum为队列中的元素个数 35 { 36 if (sum==n){ 37 flag=true; return; 38 } 39 for (int j=1;j<=n;j++) 40 if (a[i][j]) 41 id[j]--; 42 for (int j=1;j<=n;j++){ 43 if (!used[j]&&id[j]==0){ 44 used[j]=true; 45 q[sum+1]=j;//将点加入队列中 46 dfs(j,sum+1); 47 used[j]=false; 48 } 49 if (flag) return;//不加上则可求出所有的拓扑序,但需要特殊处理 50 } 51 for (int j=1;j<=n;j++) 52 if (a[i][j]) 53 id[j]++;//回溯,可求出所有拓扑序 54 } 55 56 //Topsort(邻接表,DFS,可求出所有拓扑序) 57 void topsort(int i,int sum) 58 { 59 if (sum==n){ 60 flag=true; return; 61 } 62 for (int j=head[i];j!=-1;j=edge[j].ne) id[edge[j].y]--; 63 for (int j=j;j<=n;j++){ 64 if (id[j]==0&&!used[j]){ 65 used[j]=true; 66 q[sum+1]=j; 67 dfs(j,sum+1); 68 used[j]=false; 69 } 70 if (flag) return; 71 } 72 for (int j=head[i];j!=-1;j=edge[j].ne) id[edge[j].y]++; 73 }
接下来是图的割边,割点以及强连通分量,因为在这里仅讨论Tarjan算法,所以我们将这三者同时进行讨论:
对于割点来说,我们可以用N次DFS来判断,每次删除一个点,然后判断图是否连通,这样的算法效率显然是极低的。我们知道DFS算法会形成一颗树,对于每一棵DFS树来说,其子节点不会通向任意一个根节点,我们称这个子节点到其之前的边为返祖边,那么如果我们要判断一个点是否为割点,在它形成的DFS树中,不会有任何一个点能够通向其根节点之前的点,根据这样的思路,我们可以建立一个dfn数组和一个low数组,dfn记录的是点当前的DFS层数,low记录点的最小的DFS层数,我们在DFS的同时对这两个数组进行更新,对于当前点i,如果其能够通向其之前的任意一个节点j,那么用dfn[j]来更新low[i],如果这个节点的子节点j的low值小于low[i],用low[j]来更新low[i],并将更新后的low[i]与dfn[j]进行比较,如果dfn[j]>=low[i],那么将i的子节点加一,这样我们判断割点的情况就很简单了,一个点是割点,如果这个点存在父节点,那么它的子节点的个数一定大于等于1,如果不存在父节点,那么它的子节点个数一定大于等于2,这样的点就是要求的割点。代码如下:
int dfn[MAX],low[MAX],ind=0; void tarjan(int x,int par=0){ dfn[x]=low[x]=++ind; son=0; for (int i=head[x],y;i;i=edge[i].ne) if ((y=edge[i].y)!=par){//防止访问父节点 if (!dfn[y]){ tarjan(y,x); if (low[y]<low[x]) low[x]=low[y];//更新low[x] if (low[y]>=dfn[x]) son++;//如果low[y]>dfn[x],则此子树上没有返祖边 } else if (dfn[y]<low[x]) low[x]=dfn[y]; } if (son==2||(son==1&&par)) ans[++tot]=x; }
割边的思路与割点相似,其父边需要用一个反向边的下标来注释,一条边是割边,当且仅当其起点的dfn值等于low值,证明思路与求割点相似。代码如下:
1 void tarjan(int x,int par=0){ 2 dfn[x]=low[x]=++ind; 3 for (int i=head[x],y;i;i=edge[i].ne) 4 if (i!=par){ 5 if (!dfn[y=edge[i].y]){ 6 tarjan(y,rev[i]);//rev[i]为i的反向边 7 if (low[y]<low[x]) low[x]=low[y]; 8 } 9 else if (dfn[y]<low[x]) low[x]=dfn[y]; 10 } 11 if (low[x]==dfn[x]) ans[++tot]=par; 12 }
强连通分量与割边和割边不太相似,后两者是无向图,而强连通分量则是存在于有向图中的,它指的是无向图中的极大连通子图,我的理解就是无向图中的不被任何其他环所包括的环。如何判断环呢?其实很简单,我们只需要判断一个点i所连接的一个点j是否能够相互连通就好了,但是我们不能保证这个环不被其他环所包括,且无法确定某个点处于哪个强连通分量中。
为了解决这个问题,我们需要引入栈,将所有访问过的点压入栈中,然后和割边割点一样的方法对dfnlow进行更新,如果出现dfn[i]==low[i]时,将i之后能访问到的所有点弹出栈,并将其记录在同一个强连通分量中,这样的操作我们也称为缩点,代码如下:
1 int stack[MAX],top=0; 2 void tarjan(int x){ 3 dfn[x]=low[x]=++ind; 4 vis[stack[++top]=x]=true; 5 for (int i=head[x],y;i;i=edge[i].ne){ 6 if (!dfn[y=edge[i].y]){ 7 tarjan(y); 8 if (low[y]<low[x]) low[x]=low[y]; 9 } 10 else if (vis[y]&&dfn[y]<low[x]) low[x]=dfn[y]; 11 } 12 if (dfn[x]==low[x]){ 13 int k; tot++; 14 do{ 15 k=stack[top--]; 16 vis[k]=false; 17 bel[k]=tot; 18 }while (k!=x); 19 } 20 }
接下来是最后一环——差分约束系统。
差分约束系统只是一种建图的方法。
我们先来看一些不等式组:a>b; b=a; c>=a; b<c,我们可以将其转化为:b+1<=a; b+0<=a; a+0<=b; a+0<=c; b+1<=c;
对于这样的不等式组,我们可以想到SPFA中的松弛操作:
if (dis[tn]+edge[i].v<dis[tmp]) dis[tmp]=dis[tn]+edge[i].v;
那么我们就可以将这个不等式组转化为图的形式:
对于一个不等式组b+1<=a来说,我们可以将b看做一条边的起点,将a看做该边的终点,1为边权值,这样我们就可以建立条从b到a的有向边,这条边的边权值为1.
这就是差分约束的具体建图方法。
我们来列出对于所有不等式的建图方法:
1、a>b+n -> b+1+n<=a -> b到a有一条边权值为1+n的边
2、a>=b+n -> b+n<=a -> b到a有一条边权值为n的边
3、a==b+n -> a>=b+n&&a<=b+n -> b到a有一条边权值为0的双向边
4、a<=b+n -> a到b有一条边权值为-n的边
5、a<b+n -> a+1-n<=b -> a到b有一条边权值为1-n的边
建图的方法有了,那么我们如何求最小或最大的k值,使得对于任意一个点都有一个值v,使得0<=v<=k,并让图中的所有不等式都成立呢?都有边了直接SPFA不就好了嘛= =
当然方法不止SPFA。
如果当一个不等式中不存在2、3、4条件时,显而易见我们可以进行Topsort来求k值,这样的效率大概是快于SPFA的,代码如下:
1 bool spfa() 2 { 3 memset(dis,0,sizeof(dis)); 4 int Head=0,tail=0; 5 for (int i=1;i<=n;i++) 6 if (id[i]==0) queue[++tail]=i; 7 while (Head<tail){ 8 int tn=queue[++tail]; 9 for (int i=head[tn],y;i;i=edge[i].ne){ 10 id[y=edge[i].ne]--; 11 dis[y]=max(dis[y],dis[tn]+edge[i].v); 12 if (id[y]==0) queue[++tail]=y; 13 } 14 } 15 if (tail<n) return true;//如果访问的点的个数小于当前点的个数,返回真,否则返回假 16 return false; 17 }
3条件存在时我们需要将双向边改为单向边,据不完全测试,Topsort可以求出最小k值= =
注意在这些不等式组建立成的图中是可以出现环的,但无论是自环还是其它环(前提是这个环中存在一个边的边权值e[i].v!=0)都不能使这个不等式组成立,我们在Topsort中采用了删边,所以出现环的时候我们访问不到所有的边,这时候只需要将队列中元素的个数与点的个数比较就好,但是如果SPFA中出现了环了呢?
显然我们可以证明出现自环的情况下会做无限松弛,一个特判就好了。
于此同时我们一般上要建立一个超级源点s,这个源点到其它所有的点都存在边,我们只需要关于s做SPFA就好了,代码如下
bool spfa() { while (Head<tail){ int tn=queue[++Head]; for (int i=head[tn],y;i;i=edge[i].ne) if (dis[y=edge[i].y]<dis[tn]+edge[i].v){ dis[y]=dis[tn]+edge[i].v; if (dis[y]>n) return true; if (!vis[y]){ vis[y]=true; queue[++tail]=y; } } vis[tn]=false; } return false; }
当然我们还可以采用另外一种方法:先将所有点入队后再开始做SPFA,实际上是和建立源点S一样的,但是这种方法莫名地比建立源点快= =所以对于查分约束系统来说我们可以直接采用这一种方法来求解
就这样吧。
以上是关于基础图论总结的主要内容,如果未能解决你的问题,请参考以下文章