zoukankan      html  css  js  c++  java
  • 笔记:基础图论

    ------------------------------施工中---------------------------

    前置知识:递归、DFS、BFS、回溯、栈、队列、树、基础动态规划、链表

    (下文中,n 表示图中点的个数,m 表示图中边的个数)

    一、图的存储

      1、邻接矩阵:

        用二维矩阵 {ai,j}(n*n) 表示点 i 与点 j 之间是否有边直接相连以及边的状态。

      2、边表:

        存储图中的所有边,示例代码中 x 和 y 为边的两个端点,v 为边的状态。

    1 struct edge {
    2     int x, y, v;
    3 } edge[MAX + 10];

      邻接矩阵与边表的对比:

        邻接矩阵以矩阵的方式将图存储了起来,显然空间复杂度为 O(n2),边表则仅存储图中边的信息,显然空间复杂度为 O(m),因而稀疏图中,边表存储效率更高,稠密图中二者相差不大

        1)对于询问图中某两点之间是否有边直接相连,邻接矩阵可以直接检查 ai,j 的具体值,边表则需进行遍历。

          显然,邻接矩阵的效率更高

        2)对于查找所有与 i 直接相连的点,可以检查 ai,j(j ∈ [1, n],j ≠ 0) 的值,时间复杂度为 O(n),边表仍需遍历,时间复杂度为 O(m)

          一般情况下邻接矩阵的效率更高。

          为了提高这类问题下边表的效率,将边表中的边改为单向边,引入前向星和邻接表。

      3、前向星:

        对于边集 E,获取其对应单向边表(x 为起点(第一关键字),y 为终点(第二关键字)),将边表按照第一关键字进行排序,对于某一点 i ,记录其在排序后的边表中,以该点为起点的边所在的边表下标区间 [l, r]。

        显然,如果要查找所有与 i 直接相连的点,仅需从 l 遍历至 r 即可。

     1 int l[MAXN + 10], r[MAXN + 10];
     2 struct Edge {
     3     int x, y;
     4     friend bool operator < (const Edge a, const Edge b);
     5 } edge_list[MAXM + 10];
     6 
     7 bool operator < (const Edge a, const Edge b) {
     8     return a.x < b.x;
     9 }
    10 
    11 void prepare() {
    12     std::sort(edge_list + 1, edge_list + 1 + m * 2);
    13     l[edge_list[1].x] = 1; r[edge_list[m * 2].x] = m * 2;
    14     for (int i = 2; i <= m * 2; ++i) if (edge_list[i].x != edge_list[i - 1].x) {
    15         r[edge_list[i - 1].x] = i - 1;
    16         l[edge_list[i].x] = i;
    17     }
    18 }
    前向星

      

      4、邻接表:

        两种实现方法:

          1)将单向边表以 x 为基 准,剖分成若干条链表;

          2)利用动态数组,直接记录所有与 x 直接相连的点。

    1 int len, lin[100010];//len 为邻接表的当前长度,lin[i]为以 i 为起点的边所剖成的链表的起点
    2 struct node {
    3     int y, ne;
    4 } edge[200010];//ne 为链表的下一个元素的下标
    5 
    6 inline void addedge(int x, int y) {//在链表头部插入新边的信息
    7     edge[++len].y = y ; edge[len].ne = lin[x]; lin[x] = len;
    8 }
    邻接表

         动态数组的实现方法可以直接用vector来实现

    1 std::vector<int> link[100010];
    2 inline void addedge(int x, int y) {
    3     link[x].push_back(y);
    4 }
    vector邻接表

      基础问题:对图进行遍历:

        显然可以用floodfill解决此类问题。将当前点的所有有边直接与该点相连的点进行标记并对这些点进行floodfill即可。实现方法自然有DFS和BFS两种,这里仅给出DFS的代码。

     1 bool vis[100010];//vis[i]表示 i 是否已被访问
     2 //vector邻接表
     3 void DFS(int x) {
     4     vis[x] = true;
     5     //c++11
     6     for (auto y : link[x]) if (!vis[y]) DFS(y);
     7     //below c++11
     8     for (std::vector<int>::iterator it = link[x].begin(); it != link[x].end(); ++it)
     9         if (!vis[*it]) DFS(*it);
    10     //without pointer
    11     for (std::vector<int>::size_type i = 0; i < link[x].size(); ++i)
    12         if (!vis[link[x][i]]) DFS(link[x][i]);
    13 }
    14 //邻接表
    15 void DFS(int x) {
    16     vis[x] = true;
    17     for (int i = lin[x], y; i; i = edge[i].ne)
    18         if (!vis[y = edge[i].y]) DFS(y);
    19 }
    20 //前向星
    21 void DFS(int x) {
    22     vis[x] = true;
    23     for (int i = l[x], y; i <= r[x]; ++i)
    24         if (!vis[y = edge[i].y]) DFS(y);
    25 }
    26 //邻接矩阵
    27 void DFS(int x) {
    28     vis[x] = true;
    29     for (int i = 1; i <= n; ++i)
    30         if (!vis[i]) DFS(i);
    31 }
    DFS

    二、图的最短路问题

      1、带权图:

        已知图 G=(V,E) ,对于边集 E 中的每条边 e 都有一权值 vale ,称 G 为带权图。

        若存在vale 为负值,可称图 G 为负权图。

      2、路径和路径长度:

        对于带权图 G 中的某点对 (a,b) ,从 a 沿图中的若干条边移动至 b,得到边序列 V' ,定义 V' 为 (a,b) 的一条路径, V' 中所有边权值之和为该路径的长度,记为 V(a,b)。

      3、负环/正环:

        对于 (a,a) 的一条路径 V',若其路径长度为负值,则称 V' 为图 G 的一个负环。

        相对地,对于 (a,a) 的一条路径 V',若其路径长度为正值,则称 V' 为图 G 的一个正环。

        可以以正/负环是否存在来判断图 G 的最长/最短路是否存在。

      4、最短路/最长路:

        点对 (a,b) 之间的所有路径中,长度最短的路径 V' 为 (a,b) 的最短路。相对地,点对 (a,b) 之间的所有路径中,长度最长的路径 V' 为 (a,b) 的最长路。

        将最短路/最长路记为 min dis(a,b),

        显然负环图不存在最短路,正环图不存在最长路,因为可以通过无限次经过负/正环来增加/减少 (a,b) 间的路径长度。

      5、松弛

        在无负环图中,对于已求得的一条路径 V(a,b) ,若存在中间点 c,使得 V(a,c) + V(c,b) < V(a,b) ,则称令 V(a,b) = V(a,c) + V(c,b) 为一次松弛操作。对于正环图可以得到相似定义。通过松弛操作,可以不断缩小最短路/最长路的上界/下界

      6、最短路的 Floyd-Warshall Algorithm:

        令 A 为图 G 的邻接矩阵,其中 Ai, j 的值为 i 于 j 之间最短的边的长度,Ai, i = 0 。若 i ≠ j ,且 i 与 j 没有边直接相连,则 Ai, j = ∞

        令 F0, i, j = Ai, j ,基于动态规划思想,可得出状态转移方程:

          Fk, i, j = min(Fk-1, i, j, Fk-1, i, k + Fk-1, k, j), k > 0

        显然可以进行滚动数组优化,节省掉 F 的第一维空间。

        具体代码实现如下:

    1 int a[510][510];
    2 void floyd() {
    3     for (int i = 1; i <= n; ++i) a[i][i] = 0;
    4     for (int k = 1; k <= n; ++k) for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j)
    5         if (a[i][j] > a[i][k] + a[k][j]) a[i][j] = a[i][k] + a[k][j];
    6 }
    Floyd-Warshall Algorithm

        显然,该算法的时间复杂度为 O(n3)。有关该算法的无后效性及最优子结构属性的证明暂未给出。

        需要注意的是,进行一次 Floyd-Warshall 算法之后,对于任意 i, j ,所求得的 Fi, j 均为最短距离。称这样的算法为全源最短路径算法。相对地,进行一次单源最短路径算法,求得的则是从某个特定的点出发,到其他点的最短距离。

      7、最短路的 Dijkstra's Algorithm 及其堆优化

        在某些情况下并没有必要求全源最短路,比如询问均是从某个特定的点出发的,显然这时候仅求单源最短路即可。

        1>引入 Dijkstra's Algorithm ,该算法的正确性仅基于非负权图,其具体思想大致如下:

          1)初始化:新建一个空白图,仅将某个特定的点加入图中,视这个点为源点 s 。

          2)设 disi 为当前图中从源点到点 i 的最短距离,显然最初只有 diss 的值为0,其他点的 dis 值均为 ∞ 。

          3)基于贪心思想,寻找当前图中的所有之前没有当过中间点的所有点中,dis 值最小的一个,若不存在这样的点,则算法结束。

          4)对于找到的点,将其作为中间点,更新其他点的 dis 值(如果可能)。

          5)将该点打上“已作为中间点使用过”的标记,并重复步骤 3-5 。

        2>下面证明该算法的正确性:

          假设当前中间点比之前使用过的中间点的 dis 值更小,那么由于原图为非负权图,当前点一定在之前那个 dis 值更大的点被选择的时候, dis 值更小,因而当前点一定不会在那个 dis 值更大的点之后更新。这样就保证了所使用的中间点的 dis 值的不递减性。在这基础上,当某个点作为中间点被更新时,其 dis 值一定已经达到了最小,因为之后使用的中间点的 dis 值都大于该点的 dis 值,自然不可能通过非负权边来更新该点的 dis 值。

          因此,当某个点的 dis 值不是最短路径时,仅有两种情况,一是该点还未被某中间点更新到,二是从源点没有能够到达该点的路径。

          显然,对点 s 进行一次 Dijkstra's Algorithm,最终求得的 dis 值,若其值为 ∞ ,则 s 到这个点没有路径,其余情况下的 dis 值均为点 s 到该点的最短路径长度。

        代码如下:

     1 int dis[10010], a[10010][10010], n;
     2 bool vis[10010];
     3 void dijkstra(int s) {
     4     memset(vis, false, sizeof(vis));
     5     memset(dis, 0x3f, sizeof(dis));
     6     dis[s] = 0;
     7     int x;
     8     for (x = 0; ; x = 0) {
     9         for (int i = 1; i <= n; ++i) {
    10             if (vis[i]) continue;
    11             if (dis[i] < dis[x]) x = i;
    12         }
    13         if (!x) return;
    14         vis[x] = true;
    15         for (int i = 1; i <= n; ++i) if (dis[i] > dis[x] + a[x][i]) dis[i] = dis[x] + a[x][i];
    16     }
    17 }
    Dijkstra's Algorithm

          显然,Dijkstra's Algorithm 的时间复杂度为 O(n2

        3>堆优化

          由于每次取的点都是未作为中间点的 dis 值最小的点,可以定义一个小根堆,并将 dis 值作为第一关键字,在每次更新某个点的 dis 值时,将这个点的 dis 值和标号放入小根堆中(显然可以用 STL 中的 pair 或者自定义数据结构(需重载 <)进行存储。),这样就能保证每次取出的点在被放入堆中时, dis 值都是最小的,剩下需要做的就只剩去重了。当然,去重也很简单,如果这个点已被标记,那么再取一个即可。

          

          考虑这样进行优化的时间复杂度,因为共有 m 条边,最坏情况下 m 条边都会被用来更新 dis 值,这就意味着堆中最多只有 m 个点,时间复杂度为 O(mlogm) 每个点只被作为中间点使用 1 次,因此更新的总复杂度是 O(m)。进行一次堆优化的 Dijkstra's Algorithm 的时间复杂度则为 O(mlogm),但显然通常不会达到这样的复杂度,因为一般来说 m 条边不可能都会被用来更新 dis 值。

          堆优化后的 Dijkstra's Algorithm 代码如下:

     1 using pii = std::pair<int, int>;
     2 std::priority_queue< pii, std::vector<pii>, std::greater<pii> > q;
     3 int len = 0, lin[100010];
     4 struct node {
     5     int y, ne, v;
     6 } edge[1000010];
     7 
     8 inline void add_edge(int x, int y, int v) {
     9     edge[++len].v = v; edge[len].ne = lin[x]; edge[len].y = y; lin[x] = len;
    10 }
    11 
    12 int dis[100010];
    13 bool vis[100010];
    14 void dijkstra(int s) {
    15     memset(dis, 0x3f, sizeof(dis));
    16     memset(vis, false, sizeof(vis));
    17     dis[s] = 0;
    18     q.push(std::make_pair(0, s));
    19     while (!q.empty()) {
    20         int x;
    21         pii tmp = q.top();
    22         q.pop();
    23         x = tmp.second;
    24         if (vis[x]) continue;
    25         vis[x] = true;
    26         for (int i = lin[x], y; i; i = edge[i].ne) if (dis[y = edge[i].y] > dis[x] + edge[i].v) {
    27             dis[y] = dis[x] + edge[i].v;
    28             q.push(std::make_pair(dis[y], y));
    29         }
    30     }
    31 }
    Dijkstra's Algorithm With priority_queue

        但是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=head[i],y;j!=-1;j=edge[j].ne){
    64         if (id[edge[j].y]==0&&!used[y=edge[j].y]){
    65             used[y]=true;
    66             q[sum+1]=y;
    67             dfs(y,sum+1);
    68             used[y]=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中出现了环了呢?

        显然我们可以证明出现自环的情况下会做无限松弛,一个特判就好了(其实也可以tarjan求强连通分量个数,前提是不存在==的约束边)。

        于此同时我们一般上要建立一个超级源点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一样的,但是这种方法莫名地比建立源点快= =所以对于查分约束系统来说我们可以直接采用这一种方法来求解。此外还需要判断题意,即题目要求解出最大k值还是最小k值,然后根据题意建立适当的不等式图。当我们要求最小k值时,以<=为基准建立有向图,SPFA的松弛操作中以大于为基准,求最大k值时则反过来。

  • 相关阅读:
    JSP基础语法:注释、Scriptlet、编译指令
    JDBC的LIKE书写规范
    AWT回顾篇
    1.五子棋预备知识
    对象的生命周期回顾篇
    学习activemq(2)写个简单的程序
    activemq in action(3)剖析JMS消息(转)
    activemq in action(1)
    学习activemq(3)
    hadhoop安装
  • 原文地址:https://www.cnblogs.com/hinanawitenshi/p/6639433.html
Copyright © 2011-2022 走看看