• 清北学堂2019.5.2


    Day 5(吴耀轩)

    今天以图论为主

    不要写奇奇怪怪的东西,那是闲得蛋疼,这很弱智,而且我喜欢被回应的感觉(比如:哦~~~~)

                                        ————吴耀轩

    主要讲了MST(生成树),SPP(最短路径),Topsort(拓扑排序),LCA(最近公共祖先)

    首先:

    学好图论的基础:

    • 必须意识到图论 hen dan teng
    • xue hui fang qi(雾

    基础知识

    什么是图? 

    图(Graph【这也是为什么oier们通常设g数组的原因】)是表示物件与物件之间的关系的数学对象,是图论的基本研究对象。

    一般来说,图的存储难度主要在记录边的信息,无向图的存储中,只需要将一条无向边拆成两条即可

    邻接矩阵:用一个二维数组 edg[N][N] 表示
         edg【i】【j】 就对应由 i j 的边信息
         edg【i】【j】可以记录 Bool,也可以记录边权
         缺点:如果有重边有时候不好处理
         空间复杂度 O(V2)

     如何存图?

    1. 邻接矩阵
    2. 邻接表:
    3.  vector:

     MST(生成树)

    给定一个联通无向图G=(V,E),E’⊂ E,G‘=(V,E‘)构成一棵树

    G’就是G的一棵生成树(一个树的生成树并不唯一,其数目是一个指数级别的数量级)

    给定一个 n 个点 m 条边的带权无向图,求一个生成树,使得生成树中最大边权的最小 (数据范围: n; m 106

    Algorithms for Minimal Spanning Tree

    • Kruskal
    • Prim
    • Kosaraju

     这里主要介绍一下Kruskal

    基础知识:并查集(冰茶几TAT)

    对于百度百科的定义我也是醉了。。。

    算法流程

    初始化最小生成树中没有任何边。 然后由长度从小到大依次考虑每条边: 若加入最小生成树后形成了环,则不加入;否则加入。

    分析

    排序过程可以直接使用STL库的sort。 算法的瓶颈在于判断环上。

    解法

    使用并查集维护顶点之间的连接关系。 开始时没有任何点相连,即初始化并查集。 判断环时,只需判断该边的两个顶点是否原来就相连,即是否在一个集合中。 加边时,合并集合。

    证明(来自OI wiki)

    判环时(消圈算法qwq)【运用并查集的知识】

    主要代码如下:

    #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;
    }

    Prim几乎与Kruskal同样的道理,每次添加最近的点(本质还是最短的边)。

    分析

    维护一个数组记录每个点到当前的最小生成树的最短距离。每次选择其中该值最小的点加入最小生成树。

    普里姆算法(Prim算法):

    图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

    大概就是这样,嗯。。。

    SPP(最短路径问题)

    给定一个有向图 G,询问 u v 之间最短路径长度
    d(u; v) 表示 u v 的最短路径长度
    为方便起见,不妨规定 u v 不连通时, d(u; v) = +
    Algorithms for Shortest Path Problem

    • floyd
    • Bellman-Ford
    • SPFA
    • Dijkstra

    松弛操作

    SSP 算法的本质思想就是不断进行松弛操作
    d(u; v) d(u; w) + d(w; v)

    Topsort(拓扑排序)

    G 是一个有向无环图,则称为 DAG

    DAG 不要求弱连通

    DAG一定有出度为0的点

    拓扑排序:

    n 个点的 DAG 一定存在拓扑序列

    P G 的拓扑序,当且仅当
      P n 阶排列
      若 G 中存在 u ! v 的路径,排列 P u 一定出现在 v 的前面

    Intuitive ideas
      找到 DAG 中入读为 0 的点 uu 可以放在当前 DAG 拓扑序末尾
      将 u和与 u 有关的连边从 DAG 中删除
    如何判 DAG?
      DAG 图一定能根据上述算法找到拓扑序
    时间复杂度 O(n + m)

    代码大体如下:

    #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;
                if (--outdeg[v] == 0) Queue.push(v);
            }
        }
    }

    LCA(最近公共祖先)

    r 为根的有根树上,若 a r x 的路径上,称 a x 的祖先
    a x y 共同的祖先,则称 a x y 的公共祖先
    x y 所有公共祖先中,离他们最近的称为 x y 的最近公共祖先,记为 lca(x; y)

    O(log n) 询问两点 LCA
    Algorithms for Least Common Ancestors

    •   树上倍增
    •   RMQ with Euler sequence

    树上倍增:

      Initialization
          记录 dep(x) 表示 x 到树根的距离
          预处理 anc[x][j] 表示 x 在树上向上 2j 层的祖先
            anc[x][0] = parent(x)
            anc[x][j] = anc[anc[x][j - 1]][j - 1]

      寻找 x k 层祖先
        写出 k 的二进制表示 k = 2e0 + 2e1 + · · · + 2et
        kth(x) = anc[anc[anc[anc[... ][e3]][e2]][e1]][e0]

      Query LCA(x,y)
        将 x y 调整到同一深度
          不妨设 dep(x) > dep(y),将 x 跳至 dep(x) - dep(y) 层祖先
        若 x = ylca(x; y) = x,下面假定 x ̸= y
        枚举 j : log n ! 0
          若 anc[x][j] ̸= anc[y][j],将 x := anc[x][j],将 y := anc[y][j]
        此时答案就是 parent(x)

      Property
        将二者调至同一深度,显然不影响答案
        下面的过程可以粗略理解为:将 x y 同时向上跳相同的尽量远的高度,但保持二者不相遇,那最终的 LCA 一定是 x y 的父亲
      预处理时间复杂度 O(n log n)
        anc[x][j] 中的 j 只需要 0 ! log n
      单次询问时间复杂度 O(log n)

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int maxn = 1000005;
    struct edge {
        int u, v, w;
    }edg[maxn];
    int n, m, p[maxn], Q, dep[maxn];
    vector<edge> adj[maxn]; // edges in MST
    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 anc[maxn][20], maxw[maxn][20];
    void dfs(int u)//yu chu li
    {
        for (int j = 1; j < 20; j++)
            anc[u][j] = anc[anc[u][j - 1]][j - 1],
            maxw[u][j] = max(maxw[u][j - 1], maxw[anc[u][j - 1]][j - 1]);
        for (unsigned i = 0; i < adj[u].size(); ++i)
        {
            int v = adj[u][i].v, w = adj[u][i].w;
            if (v != anc[u][0])
                dep[v] = dep[u] + 1, anc[v][0] = u, maxw[v][0] = w, dfs(v);
        }
    }
    int solve(int u, int v)
    {
        int res = 0;
        if (dep[u] < dep[v]) swap(u, v);
        for (int d = dep[u] - dep[v], j = 0; d ; d >>= 1, ++j)
            if (d & 1) res = max(res, maxw[u][j]), u = anc[u][j];
            //adjust u & v to the same depth
        if (u == v)    return res; //u & v meet now
        
        for (int j = 19; j >= 0; j--)
            if (anc[u][j] != anc[v][j]) // if anc[u][j] & anc[v][j] dont meet together, then jump u & v 
                res = max(res, maxw[u][j]),
                res = max(res, maxw[v][j]),
                u = anc[u][j], v = anc[v][j];
        //now u & v 's lca must be their parent now, in an easy word, it's anc[u][0] or anc[v][0]
        
        res = max(res, maxw[u][0]);
        res = max(res, maxw[v][0]);
        u = anc[u][0]; v = anc[v][0];
        return res;
    }
    int main()
    {
        cin >> n >> m >> Q;
        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, u, v; i <= m; i++)
            if (merge(u = edg[i].u, v = edg[i].v))
                adj[u].push_back(edg[i]),
                adj[v].push_back((edge){v, u, edg[i].w});
                
        dfs(1);
        for (int u, v, i = 1; i <= Q; i++)
            cin >> u >> v, cout << solve(u, v) << endl;
    }
    
    /*Problem statement
    Given a graph G containing n nodes and m bidirectional edges.
    The weight of a path p0->p1->p2->...->pt is the maximum weight of edges on the path.
    There are Q queries given (u, v), output the minimum weight of paths from u to v.
    Guarantee between any nodes u & v , there is at least one path from u to v.
    */

    安排!!!

  • 相关阅读:
    请求转发和请求重定向的区别
    查看电脑连过的WiFi密码
    linux mysql不能远程登录
    map的遍历方法
    ________________springbootのMybatis
    ________________springbootのTest
    ________________springbootの自定义starter
    ________________springbootのAOP
    ________________springbootのjdbc、事物
    ________________初学springboot14
  • 原文地址:https://www.cnblogs.com/gongcheng456/p/10803245.html
Copyright © 2020-2023  润新知