zoukankan      html  css  js  c++  java
  • 五一清北学堂培训之图论

    dalao曰:图论千万步,放弃第一步

    存储:

    1.邻接矩阵

          缺点:1.有重边时很麻烦

                     2.空间复杂度太高

         优点:写起来简单(当然还有其他优点辣)

      

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    int ideg[N], odeg[N], n, m, edg[N][N];
    bool visited[N];
    
    void travel(int u, int distance)
    {
        cout << u << " " << distance << endl; visited[u] = true;
        for (int v = 1; v <= n; v++)
            if (edg[u][v] != -1 && !visited[v])
                travel(v, distance + edg[u][v]); //if there is an edge (u, v) and v has not been visited, then travel(v)
    }
    int main()
    {
        cin >> n >> m;
        memset(edg, -1, sizeof edg);
        memset(visited, false, sizeof visited);
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, edg[u][v] = w, odeg[u]++, ideg[v]++;//加出度和入度 
        for (int i = 1; i <= n; i++)
            cout << ideg[i] << " " << odeg[i] << endl;
        for (int i = 1; i <= n; i++)
            if (!visited[i]) travel(i, 0);
    }

    2.邻接表

       我们发现邻接矩阵中有好多位置是空的,所以我们可以只记录一个点的出边,就形成了邻接表

    类似一个链表的形式,遍历时,只要把list[i]后面的链遍历就行了。

      指针版本

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    struct edge {
        int u, v, w; edge *next;
        edge(int _u, int _v, int _w, edge *_next):
            u(_u), v(_v), w(_w), next(_next) {}
    };
    edge *head[N]; //List[u] stores all edges start from u
    int ideg[N], odeg[N], n, m;
    bool visited[N];
    
    void add(int u, int v, int w)
    {
        edge *e = new edge(u, v, w, head[u]);
        head[u] = e;
    }
    void travel(int u, int distance)
    {
        cout << u << " " << distance << endl; visited[u] = true;
        for (edge *e = head[u]; e ; e = e -> next)
            if (!visited[e -> v])
                travel(e -> v, distance + e -> w); //if there is an edge (u, v) and v has not been visited, then travel(v)
    }
    int main()
    {
        cin >> n >> m;
        memset(visited, false, sizeof visited);
        memset(head, 0, sizeof head);
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++;
        for (int i = 1; i <= n; i++)
            cout << ideg[i] << " " << odeg[i] << endl;
        for (int i = 1; i <= n; i++)
            if (!visited[i]) travel(i, 0);
    }

    来我们看看好理解的数组版本

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    struct edge {
        int u, v, w, next;
    }edg[N];
    int head[N]; //记录每个点的最后一条出边的编号,方便记录next
    int ideg[N], odeg[N], n, m, cnt; //cnt: 边的编号
    bool visited[N];
    void add(int u, int v, int w)
    {
        int e = ++cnt;//编号加以
        edg[e] = (edge){u, v, w, head[u]};//e的next是当前的head[u](u是起点)
        head[u] = e;
    }
    void travel(int u, int distance)//遍历
    {
        cout << u << " " << distance << endl; visited[u] = true;
        for (int e = head[u]; e ; e = edg[e].next)//终止条件:next不存在
            if (!visited[edg[e].v])
                travel(edg[e].v, distance + edg[e].w); //如果没有遍历过,就遍历它
    }
    int main()
    {
        cin >> n >> m; cnt = 0;
        memset(visited, false, sizeof visited);
        memset(head, 0, sizeof head);
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++;
        for (int i = 1; i <= n; i++)
            cout << ideg[i] << " " << odeg[i] << endl;
        for (int i = 1; i <= n; i++)
            if (!visited[i]) travel(i, 0);
    }

    举个例子

     

    二维数组模拟邻接表

    edg[u][k]记录从u出发的第k条边,不过还是n^2的。因为在一开始,我们就确定了数组的长度,所以还是有浪费。

    怎么办?vector。

    vector是用多少,有多少(变长数组)

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    struct edge {
        int u, v, w;
    };
    vector<edge> edg[N]; //长度为n,每个元素是vector的东西(变长数组)这里edg[0]是边长数组,edg[1]也是变长数组.....以此类推
    int ideg[N], odeg[N], n, m, cnt; //cnt: numbers of edges
    bool visited[N];
    void add(int u, int v, int w)
    {
        edg[u].push_back((edge){u, v, w});
    }
    void travel(int u, int distance)//distance:当前的路径总长度
    {
        cout << u << " " << distance << endl; visited[u] = true;
        for (int e = 0; e < edg[u].size(); e++)//访问u的所有出边:上限:u的出边数量 
            if (!visited[edg[u][e].v])//edg[u][e]:以u出发的第e条出边 
                travel(edg[u][e].v, distance + edg[u][e].w); //如果没有遍历过,就遍历
    }
    int main()
    {
        cin >> n >> m; cnt = 0;
        memset(visited, false, sizeof visited);
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++;//记录入度,出度
        for (int i = 1; i <= n; i++)
            cout << ideg[i] << " " << odeg[i] << endl;
        for (int i = 1; i <= n; i++)
            if (!visited[i]) travel(i, 0);
    }

    dalao 曰:代码比开机要简单

    生成树:

     

    就是从一个无向连通图中,选一个子图,使这个子图构成一棵树。这个子图就是生成树

    树:无环

    举个简单的例子

    红色部分就是生成树

    蓝色也是生成树

    一个图的生成树的数量是指数级别的

     瓶颈生成树

     

    就是求所有边中,最大的边权最小的那棵生成树

    这里红色比蓝色更优

    和最小生成树的区别:

     

     求瓶颈生成树,不需要全部学这些算法

    直接讲kruskal

     

    判断联通:并查集。

    重复过程直到加入了n-1条边

    为什么是对的?

    最开始u,v在不同的两个联通块里,现在有一条边w0,但到最后肯定会处于同一个联通块里,所以最后两个连通块中肯定有一条边,假设最后那条边是w1,但是现在w1没有加入,按这个算法,w1一定是比w0小,所以是对的。

     上面的证明并不严谨

    下面是严谨的证明。

    我们先讲一下闲圈算法消圈算法

    把一个环的边从大到小排序。然后把大的边去掉,最后就是最小生成树。kruskal可以用消圈算法证明,不过是把消圈算法反过来。不是去掉边,而是从小到大,加入边。

    为什么消圈算法不行?找环太麻烦了。

    dalao曰:手速不行就去多玩点游戏,比如扫雷什么的

    代码:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int maxn = 1000005;
    struct edge {
        int u, v, w;
    }edg[maxn];
    int n, m, p[maxn], ans = 0;
    
    bool cmp(edge a, edge b)
        {return a.w < b.w;}
    int findp(int t) 
        {return p[t] ? p[t] = findp(p[t]) : t;}
    bool merge(int u, int v)
    {
        u = findp(u); v = findp(v);
        if (u == v) return false;
        p[u] = v; return true;
    }
    int main()
    {
        cin >> n >> m;
        for (int i = 1, u, v, w; i <= m; i++)
            cin >> u >> v >> w, edg[i] = (edge){u, v, w};
        sort(edg + 1, edg + m + 1, cmp);//排序 
        
        for (int i = 1; i <= m; i++)
            if (merge(edg[i].u, edg[i].v))//将两个点放进同一个连通块里 
                ans = max(ans, edg[i]. w);
        cout << ans << endl;
    }

     路径:

     举个例子:

    上面就是从1到6的路径

    但是路径可以有多条。

    简单路径:不经过重复的点

    路径长度:路径上所有边的边权之和

    最短路径问题:

    每个都要掌握 qwq,当然spfa是bellman_ford的一个优化

    (松弛过后)

    其实就是一个取min的操作,所有求最短路径的算法都是这个思想。

    floyd:

     似乎就是个DP qwq

    这里我们举个例子

    k=1:d[4][2]=d[4][1]+d[1][2]=8,d[4][3]=d[4][1]+d[1][3]=26

    k=2:可以更新:d[1][4]=2+3=5,d[1][3]=d[1][2]+d[2][3]=11,

    k=3:什么都做不了

    k=4:d[3][1]=d[3][4]+d[4][1]=7,d[2][1]=d[2][4]+d[4][1]=9

    证明:

    举个例子

       设是3到5的最短路径,那么4到7的最短路径一定是4->1->7.在k=1时,d[4][7]成为真实值。

    k=2:d[3][4]完成

    k=3:无影响

    k=4:d[3][7]完成(d[3][4]正确,且d[4][7]z正确)

    k=5,6:无影响

    k=7:d[3][5]完成

    在任意一个图里,不断枚举k,就不断完成一些点的真实值。最后全部完成

    思想:不断枚举中间点

    代码:

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    int n,d[101][101],m;
    const int inf=0x3ffff;
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)//初始化
         {for(int j=1;j<=n;j++)
           d[i][j]=inf;
          d[i][i]=0; 
         }
         for(int i=1;i<=m;i++)
         {int x,y,z;
           scanf("%d%d%d",&x,&y,&z);
           d[x][y]=min(d[x][y],z);//无向图的话就是d[y][x]=d[x]y]
         }
         for(int k=1;k<=n;k++)//注意枚举顺序
         {for(int i=1;i<=n;i++)
           {for(int j=1;j<=n;j++)
            {d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
             if(d[i][j]<0)//注意负权环
              {printf("有负权环!");return 0; 
              }
             } 
           }
         }
         for(int i=1;i<=n;i++)
          for(int j=1;j<=n;j++)
           printf("d[%d][%d]=%d
    ",i,j,d[i][j]);
    }

     复杂度:O(n^3) (最多跑n=500)

    优势:全部跑完一遍之后,任意两点最短路O(1)询问

    有什么问题?

    有负权环:

    这样会用floyd一直怼,最后炸了。所以也可以用floyd检查是否有负权环。(d[u][u]<0)

    当然大部分数据不会有负权环的。

     Bellman-ford

    单源最短路:

    其实n-1次就够了

    复杂度:O(nm)

    依旧是可以判负权环

    因为在n-1次时,所有值是真实值,但如果有负权环,就会被更新(有负权边不会改变)

    代码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    using namespce std;
    
    int head[10001],cnt,dis[10001];
    struct Edge{
        int to,dis,next;
    }edge[500001];
    void add(int fr,int to,int dis)
    {   edge[++cnt].to=to;
        edge[cnt].dis=dis;
        edge[cnt].next=head[fr];
        head[fr]=cnt;
    }//上面都是存图
    int main()
    { int s; 
      scanf("%d%d%d",&n,&m,&s);
      for(int i=1;i<=m;i++)
       {int fr,to,dis;
        scanf("%d%d%d",&fr,&to,&dis);
        add(fr,to,dis);
       }
       for(int i=1;i<=n;i++)dis[i]=inf;
       dis[s]=0;
      for(int i=1;i<=n;i++)//循环n边(每个点来一遍)
        { for(int j=head[i];j;j=edge[j].next)//链式前向星遍历
           {
             int v=edge[j].to,d=edge[j].dis;//这里就是上面的式子
             dis[v]=min(dis[v],dis[i]+d);
           }
        }
       for(int i=1;i<=n;i++)
         printf("%d
    ",dis[i]);
    }

    spfa:

    对bellman_ford的优化

    基础

    在bellman-ford中,我们是对全部边进行松弛操作。

      但是对于一个u,在上一轮去全局松弛中,没有被更新,那么这一轮就没有必要考虑u的出边。因为只有d[u]才会影响到所有的v,v为u的出边的终点的d[v]。如果d[u]没变,后面就没有必要了。

      所以我们维护一个队列,记录d[u]被改变的u,然后枚举u所有出边e,进行松弛,将u出队。对于每一个u的终点v,如果d[v]>d[u]+w,就更新d[v],再将v入队。注意这里一个v可以多次入队。

    终止:队空 / d[u]进队列n-1次以上。如果u入队n-1次以上,那就有负权环。

    复杂度:O(km)(最坏情况:O(nm))(k的期望为2,但是看数据喽,毒瘤数据也是nm,k为每个点被更新的次数)(记住spfa是会退化的)

    网格图可以卡掉spfa

    就像这种,每个点是一个交点,就是网格图。

    在判断网格图时,可以跑一边spfa,如果炒鸡慢,一般就是网格图

    代码:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 1e5 + 5;
    const int inf = 1 << 29;
    
    struct edge{
        int u, v, w;
    };
    vector<edge> edg[N];
    int d[N], n, m, S;
    
    queue<int> Queue;
    bool inQueue[N];
    int cntQueue[N];
    
    void add(int u, int v, int w)
    {
        edg[u].push_back((edge){u, v, w});
    }
    int main()
    {
        cin >> n >> m >> S;
        for (int i = 1; i <= n; i++) d[i] = inf;
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, add(u, v, w);
            
        d[S] = 0; inQueue[S] = true; Queue.push(S);//将原点入队 
        while (!Queue.empty())
        {
            int u = Queue.front(); Queue.pop(); inQueue[u] = false;//出队后,u就不在队列里了 
            for (int e = 0; e < edg[u].size(); e++)
            {
                int v = edg[u][e].v, w = edg[u][e].w;
                if (d[v] > d[u] + w)//可以更新 
                {
                    d[v] = d[u] + w;//更新 
                    if (!inQueue[v])//再判是否入队 
                    {
                        Queue.push(v); ++cntQueue[v]; inQueue[v] = true;//注意先入队,再记录入队次数
                        if (cntQueue[v] >= n) {cout << "Negative Ring" << endl; return 0;}//如果有负权环,就结束 
                    }
                }
            }
        }
        for (int i = 1; i <= n; i++)
            cout << d[i] << endl;
    }

     dijkstra

    适用:无负权边(比如走高速公路什么的)

     

      从起点出发,松弛所有直达点,找到距离起点最近的一个直达点,由它再松弛它的直达点,再找到一个距离它最近的直达点,继续松弛............

      所以我们维护一个队列,队列里的点已经被松弛过,找到一个不在队列中,d最小的点u,松弛所有出边,并将u入队

    例子:

    在上图中,先松弛起点的直达点,然后找到了圈红圈的点,再由这个点进行松弛操作。

    证明:显然易证

    因为在队列中的点的d已经是真实值了

    如何判断点是否在队列里?一次O(N)暴力扫描

     int head[10001],dis[100001],cnt;
     bool dl[10001];
     struct Edge{
         int to,dis,next;
     }edge[500001];
     void add(int fr,int to,int dis)
     {
         edge[++cnt].dis=dis;
         edge[cnt].to=to;
         edge[cnt].next=head[fr];
         head[fr]=cnt;
     }
     int main()
     {
         scanf("%d%d",&n,&m);
          for(int i=1;i<=m;i++)
          {int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
          }
         //dijkstra(辣鸡版本):
         for(int i=1;i<=n;i++)
         {int u;
            for(int j=1;j<=n;j++)
             if(!dl[j]&&d[j]<d[u])u=j;
           for(int i=head[u];i;i=edge[i].next)
           { dis[edge[i].to]=min(dis[edge[i].to],dis[u]+edge[i].dis); 
           }
         }
     }

    复杂度:O(n^2+m)

    优化:小根堆

    显然它太慢了

       我们可以维护一个小根堆,里面记录没有被用来更新的点u的d,每次取最上面的。如果d[v]被更新了,就把新的d[v]放进堆里,原来的d[v]一定比现在的d[v]大,那么原来的d[v]会在现在的d[v]之后出堆,如果我们发现一个点的所有出边的终点已经被松弛过了,就不需要管他了。所以原来的d[v]就不用管了。

    堆这种东西,就是优先队列辣qwq

    代码

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 1e5 + 5;
    const int inf = 1 << 29;
    
    struct edge{
        int u, v, w;
    };
    vector<edge> edg[N];
    int d[N], n, m, S;
    
    bool relaxed[N];
    struct Qnode {
        int u, du;
        bool operator<(const Qnode &v)
            const {return v.du < du;}
    };
    priority_queue<Qnode> PQueue;
    
    void add(int u, int v, int w)
    {
        edg[u].push_back((edge){u, v, w});
    }
    int main()
    {
        cin >> n >> m >> S;
        for (int i = 1; i <= n; i++) d[i] = inf;
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, add(u, v, w);
            
        d[S] = 0; PQueue.push((Qnode){S, 0});
        while (!PQueue.empty())
        {
            int u = PQueue.top().u; PQueue.pop();
            if (relaxed[u]) continue;
                //if edges staring from u are already relaxed, no need to relax again.
            relaxed[u] = true;
            for (int e = 0; e < edg[u].size(); e++)
            {
                int v = edg[u][e].v, w = edg[u][e].w;
                if (d[v] > d[u] + w)
                {
                    d[v] = d[u] + w;
                    PQueue.push((Qnode){v, d[v]});
                    //if d(v) is updated, push v into PQueue
                }
            }
        }
        for (int i = 1; i <= n; i++)
            cout << d[i] << endl;
    }

     先将个东西

    DAG

    弱连通:把有向图改为无向图后连通。

    拓扑排序:

     

     第一个:true

     第二个:false

    再加一个点:拓扑序有很多(阶乘级别)

     直观上好像并不那么显然,我们依旧用上个图举例

    出度为0:3,6

    把指向3的边扔掉,在拓扑序的最后写下3

    之后再找,再写(从后往前找)。

    1 4 5 6 2 3

    任何一个DAG都有拓扑序:

    任意一个DAG一定有出度为0的点,如果没有,就踏遍了天南地北。(因为DAG一共有n个点,但是没有的话,还可以继续走下去,然后就有环了)

    删掉一个点,肯定还是个DAG,肯定还有出度为0的点,继续找继续找继续找......拓扑序出来了

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 1e5 + 5;
    const int inf = 1 << 29;
    
    struct edge{
        int u, v;
    };
    vector<edge> edg[N];
    int n, m, outdeg[N], ans[N];
    
    queue<int> Queue;
    void add(int u, int v)
    {
        edg[u].push_back((edge){u, v});
    }
    int main()
    {
        cin >> n >> m;
        for (int u, v, i = 1; i <= m; i++)
            cin >> u >> v, add(v, u), outdeg[u]++;//因为邻接表记录的是出边,所以这里反过来记 
            
        for (int i = 1; i <= n; i++)
            if (outdeg[i] == 0) Queue.push(i);
        for (int i = 1; i <= n; i++)
        {
            if (Queue.empty())
                {printf("Not DAG"); return 0;}
            int u = Queue.front(); Queue.pop(); ans[n - i + 1] = u;
            for (int e = 0; e < edg[u].size(); e++)
            {
                int v = edg[u][e].v;//取一个点,把它的出边终点的出度都减少1 
                if (--outdeg[v] == 0) Queue.push(v);//出度为0:加入拓扑序 
            }
        }
    }

    拓扑排序的用处:

      1.求单源最短路:

        拓扑序中,出现在原点前面的是无穷

    LCA:

    有q组询问,在o(logn)的时间内完成一组询问

    O(n):先条到同一深度,在同时往上跳。

    O(log n):树上倍增

         

     

    一个比较直观的图

    如此魔幻的括号,懂了就不要看了,伤眼。

    举个例子

    这是个很长的路径,不是一条边

     

    跳到同一深度

    跳的深度从log 2 n开始,到1

    如果跳了之后相遇,就不跳,不然就跳,跳完后再往上一个就是答案

     

     

    因为x-->t=len(x)-len(t)

     

     MST:最小生成树(不是瓶颈生成树)

    但这里指的是瓶颈生成树

      为什么?

    红色是瓶颈生成树的路径。

    看起来好像有环的样子。

    如果黑色的是答案路径的话,说明黑色上的最大边权,比红色的最大边权要小,而且在有环的情况下.....wait!好像哪里不对的样子,所以这是矛盾的,所以答案就不可能在黑色上面。

     最后一句是个什么鬼呢?

    简单举个例子:

    x到它的64层祖先的最大值=max(x到32层祖先的最大值,x的32层祖先到x的64层祖先的最大值)

    就是这样(这里叫倍增)

    简单说一下倍增和分治的区别:

    分治:把一个大问题分解为几个不重叠的小问题,解决完小问题之后合并。

    倍增:先预处理出最底层问题的解,再按某种倍数关系合并。

    代码:显然这个题很难,不用证明,所以——————咕咕咕

     

  • 相关阅读:
    sftp上传到远程服务器
    mysql 的 find_in_set函数使用方法
    wamp2.5 局域网无法访问问题
    JS图片上传预览
    select2的相关问题
    linux 更新源miss问题
    freeMarker遍历map的正确方式
    Input类型是checkbox时checked属性获取
    Java 内存区域划分 备忘录
    简单聊聊java中如何判定一个对象可回收
  • 原文地址:https://www.cnblogs.com/lcez56jsy/p/10802092.html
Copyright © 2011-2022 走看看