基础图论总结

  在此之前需要先学会基本数据结构,递归以及搜索、回溯

  用了半个月的时间终于搞完了全部的基础图论。。。仅介绍到差分约束

  图的定义自己百度= =这里直接开始说图的存储。

  图的存储有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一样的,但是这种方法莫名地比建立源点快= =所以对于查分约束系统来说我们可以直接采用这一种方法来求解

  就这样吧。

时间: 2024-11-08 20:51:24

基础图论总结的相关文章

基础图论算法导引

ACM中常用图论算法 1. 拓扑排序 -> 拓扑排序的原理及其实现 2. 最短路径算法 -> 最短路算法总结 差分约束系统 -> 差分约束 前k短路 -> 前K短路径问题 3. 最小生成树问题扩展 -> 最?小?生?成?树?问?题?的?拓?展  最优比率生成树 -> 最优比率生成树 最小k度限制生成树 -> IOI2004国家集训队论文,由汪汀所著(网盘内有) 或者刘汝佳的黑书内有 裸题 poj1639 题解 4. 二分图匹配 -> 二分图的最大匹配.完美匹

基础图论--存图

图论蛮好玩的呢  比起数论真是有趣多了 有空整理一下下 首先,图是个什么鬼东东呢 graph, 一堆点集,一堆边集,可以把各种事物抽象成点,事物之间的联系用边来表示,边上还可有权值,表示距离费用等 e.g. 把各个城市抽象成点,城市之间可以由高铁直达的称作有联系(边), 边上还可附加权值,俩城市间距离等 至于一些基本概念, 有向无向,是否成环,入度出度等就不多讲啦 基本概念理解就好 现在看看存图,看不同情况来选适合的存图方式 邻接矩阵---二维矩阵 a[ n ][ m ], 可用a[i][j]=

暑假集训-基础图论

5 / 37 Problem A HUST 1019 A dangerous trip 10 / 71 Problem B HUST 1631 Road System     Problem C UVALive 3523 Knights of the Round Table   1 / 5 Problem D UVALive 5135 Mining Your Own Business 1 / 1 Problem E UVALive 4287 Proving Equivalences 0 / 2

基础图论3

题目:没有网址 题意: 题解: 建立图搜索ok 代码: #include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<queue> #include<set> #include<algorithm> #include<map> #define maxn 10005 using namespace std; vecto

BDFZOI 树的直径

提交次数:2 涉及知识:基础图论/BFS 描述 一棵树T的"直径"定义为结点两两间距离的最大值.给定带权树T,求T的直径长度. 输入 第一行包含2个整数N.M,表示图中共有N个结点和M条无向边.(N <= 5000,M<n)接下来M行,每行包含3个整数{u,v,w},表示有一条无向边连接结点u.v*输入保证是无环图输出一个整数,代表直径长度 样例输入 4 31 2 12 3 22 4 3 样例输出 5 代码: 1 #include<iostream> 2 #in

静态频繁子图挖掘算法用于动态网络——gSpan算法研究

摘要 随着信息技术的不断发展,人类可以很容易地收集和储存大量的数据,然而,如何在海量的数据中提取对用户有用的信息逐渐地成为巨大挑战.为了应对这种挑战,数据挖掘技术应运而生,成为了最近一段时期数据科学的和人工智能领域内的研究热点.数据集中的频繁模式作为一种有价值的信息,受到了人们的广泛关注,成为了数据挖掘技术研究领域内的热门话题和研究重点. 传统的频繁模式挖掘技术被用来在事务数据集中发现频繁项集,然而随着数据挖掘技术应用到非传统领域,单纯的事务数据结构很难对新的领域的数据进行有效的建模.因此,频繁

寒假日记

2.9 好像快要放假了--即使是Cu滚粗也要回家过年.提前开始想念同学们?有一点吧--一年里从来没有分开超过三天的一群人呐.整个鸡年一共在家里待了:初一到初六.高考6.7-6.9.联赛后11.18-11.20.元旦12.31,即将到来的2.10-2.15,一共19天,比自己想象中还是要多吧.这一年:寒假集训学基础图论,高一下学期搞高考,4月跟着学长学姐HEOI2017打个酱油,暑假集训大概慢慢会考试了,小学期依然搞高考,高二上学期文化课全盘弃疗,联赛前集训成为熟练暴力选手,NOIP2017水个暴

学习算法思想 修炼编程内功

第1章 当我们谈论算法的时候,我们在谈论什么? 无论是BAT,还是FLAG,但凡有点儿水平的技术公司,面试都要面算法.为什么算法这么重要?在工作中,真的会使用算法吗?学了算法到底有什么用?当我们谈论算法的时候,我们在谈论什么? 1-1 我们究竟为什么要学习算法 1-2 课程介绍 第2章 排序基础 O(n^2)的算法虽然简单,但也实用!让我们从最简单的基础排序算法开始,打开我们的算法大门! 2-1 选择排序法 - Selection Sort 2-2 使用模板(泛型)编写算法 2-3 随机生成算法

学习算法思想,修炼编程内功

第1章 当我们谈论算法的时候,我们在谈论什么?无论是BAT,还是FLAG,但凡有点儿水平的技术公司,面试都要面算法.为什么算法这么重要?在工作中,真的会使用算法吗?学了算法到底有什么用?当我们谈论算法的时候,我们在谈论什么? 第2章 排序基础O(n^2)的算法虽然简单,但也实用!让我们从最简单的基础排序算法开始,打开我们的算法大门! 第3章 高级排序算法虽然很多同学都听说过归并排序和快速排序,但优化归并排序可以有哪些角度?快速排序能逐渐迭代四个版本?在这一章,让我们真正理解这些高级排序算法.面试