• Day 5 上午


    内容提要

    图论QwQ


    学好图论的基础:
    必须意识到图论 hendanteng
    xuehuifangqi(雾


    G = (V; E)
    一般来说,图的存储难度主要在记录边的信息
    无向图的存储中,只需要将一条无向边拆成两条即可
    邻接矩阵:用一个二维数组 edg[N][N] 表示
    edg[i][j] 就对应由 i 到 j 的边信息
    edg[i][j] 可以记录 Bool,也可以记录边权
    缺点:如果有重边有时候不好处理
    空间复杂度 O(V^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);
    }
    
    /*
    Given a graph with N nodes and M unidirectional edges.
    Each edge e_i starts from u_i to v_i and weights w_i
    Output a travelsal from node 1 and output degree of each node.
    */

    邻接表(链式前向星)
    对每一个点 u 记录一个 List[u],包含所有从 u 出发的边
    直接用数组实现 List[u]?读入边之前不知道 List[u] 长度
    手写链表
    用 STL 中的 vector 实现变长数组
    只需要 O(V + E) 的空间就能实现图的存储

    指针版本(一般不用)

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    struct edge {
        int u, v, w; edge *next;//next指针指向 
        edge(int _u, int _v, int _w, edge *_next):
            u(_u), v(_v), w(_w), next(_next) {}
    };
    edge *head[N]; //List[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);
    }
    
    /*
    Given a graph with N nodes and M unidirectional edges.
    Each edge e_i starts from u_i to v_i and weights w_i
    Output a travelsal from node 1 and output degree of each node.
    */

    数组模拟版本:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    struct edge {
        int u, v, w, next;
    }edg[N];
    int head[N]; //List[u] stores all edges start from u
    int ideg[N], odeg[N], n, m, cnt; //cnt: numbers of edges
    bool visited[N];
    
    void add(int u, int v, int w)
    {
        int e = ++cnt;
        edg[e] = (edge){u, v, w, head[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)
            if (!visited[edg[e].v])
                travel(edg[e].v, distance + edg[e].w); //if there is an edge (u, v) and v has not been visited, then travel(v)
    }
    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);
    }
    
    /*
    Given a graph with N nodes and M unidirectional edges.
    Each edge e_i starts from u_i to v_i and weights w_i
    Output a travelsal from node 1 and output degree of each node.
    */

    用edg[u][i]表示从u出发第i条信息,但这样还是n^2
    但这样很浪费,因为数组不能全部利用
    传统的方法要在定义数组是就规定他的大小,但是我们读入之前并不知道从每个点出发的边数
    我们可以用vector
    一些 vector 的细节
    vector 本质就是 c++ 模板库帮我们实现好的变长数组
    向一个数组 a 的末尾加入一个元素 x a:push_back(x)
    询问数组 a 的长度 a.size()
    注意: vector 中元素下标从 0 开始

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 5005;
    
    struct edge {
        int u, v, w;
    };
    vector<edge> edg[N]; //edge记录变长数组记录的是什么类型 
    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)
    {
        cout << u << " " << distance << endl; visited[u] = true;
        for (int e = 0; e < edg[u].size(); e++)//遍历边 
            if (!visited[edg[u][e].v])//以u出发的第e条出边 
                travel(edg[u][e].v, distance + edg[u][e].w); //if there is an edge (u, v) and v has not been visited, then travel(v)
    }
    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);
    }
    
    /*
    Given a graph with N nodes and M unidirectional edges.
    Each edge e_i starts from u_i to v_i and weights w_i
    Output a travelsal from node 1 and output degree of each node.
    */

    生成树
    给定一个连通无向图 G = (V; E)
    E′ ⊂ E
    G′ = (V; E′) 构成一棵树
    G′ 就是 G 的一个生成树

    生成树不是唯一的,并且数量是指数级别

    问题描述
    给定一个 n 个点 m 条边的带权无向图,求一个生成树,使得生成树中最大边权的最小
    数据范围: n; m ≤ 10^6
    Algorithms for Minimal Spanning Tree
    Kruskal
    Prim
    Kosaraju


    并查集
    生成树的本质:选取若干条边使得任意两点连通
    维护图中任意两点的连通性
    查询任意两点连通性
    添加一条边,使两个端点所在连通块合并

    Kruskal
    Intuitive ideas
    将原图无向边按照边权从小到大排序
    找到当前边权最小的边 e : (u; v)
    如果 u 和 v 已经连通,则直接删除这条边(因为形成了环)
    如果 u 和 v 已经未连通,将之加入生成树
    重复上述过程
    证明:
    Rigorous proof
    消圈算法:
    如果原图中有一个环,然后把这个圈中边权最大的去掉,这样不断去掉最后得到的就是最小生成树
    但是由于环不好找,所以不用他

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

    本蒟蒻的博客:传送门


    路径
    P = p0; ...; pn 为 u 到 v 的路径
    p0 = u, pn = v
    对于任意i∈2 [1; n]; 存在e : (pi−1; pi)∈E
    P 的长度
    length(P) = ∑e∈P length(e)

    我们一般考虑简单路径,就是不重复经过一个点的路径
    显然它也不一定是惟一的

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

    先介绍一下松弛操作
    SSP 算法的本质思想就是不断进行松弛操作
    d(u; v) ≤ d(u; w) + d(w; v)
    我们算出来了一个d(u,v)但是他不一定是最终的d(u,v),这时如果我们找到了d(u,w) + d(w,v)<当前的d(u,v),就更新d(u,v)的值

    floyd
    初始时, d(u; v) 就等于 u 到 v 的边权
    用邻接矩阵的形式记录 d
    u 和 v 没边, d(u,v) = +∞
    u 和 v 有重边,保留最小边权
    三层循环枚举 k; i; j,执行松弛操作
    d(i,j) = min{d(i,j); d(i,k) + d(k,j)}

    代码:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 505;
    const int inf = 1 << 29;
    
    int d[N][N], n, m;
    int main()
    {
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) d[i][j] = inf;
        for(int i = 1;i <= n; i++)
            d[i][i]=0; 
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, d[u][v] = min(d[u][v], w);
    
        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]);
        //必须先枚举中间点 
    }

    本蒟蒻博客:传送门

    三层循环枚举 k,i,j,执行松弛操作
    d(i,j) = min{d(i,j), d(i,k) + d(k,j)}
    任意两点之间的最短路最多经过 n − 1 条有向边
    k = 1 的循环结束之后
    如果 u --> v 的 最短路径 只经过 1,此时的 d(u,v) 就是真实值
    k = 2 的循环结束之后
    如果 u --> v 的 最短路径 只经过 1,2,此时的 d(u,v) 就是真实值
    k = t 的循环结束之后
    如果 u --> v 的 最短路径 只经过 1...t,此时的 d(u,v) 就是真实值
    不断有真实值被算出,然后又不断合并

    不断枚举松弛操作的中间点 k
    算法时间复杂度 O(n^3)
    优势:处理出 d 之后,任意两点 SP 能 O(1) 询问
    注意:必须先枚举中间点!!!必须先枚举中间点!!!必须先枚举中间点!!!

    负权环
    如果存在一个环,其边权和为负数,则称为负权环
    u --> v 存在一个负权环,d(u,v) = −∞
    判断图是否含负权环
    floyd 后检查是否存在 d(u,u) < 0,如果存在就说明有负权环


    单源最短路
    不需要知道图中任意两个点的 SP
    只需要知道某一点 u 到其它点的 SP
    u 称为源点,单源最短路就是求从 u 出发的最短路
    为方便,下面记源点为 S

    Bellman-Ford
    将 d(S,u) 简写为 d(u),初始 d(S) = 0, d(u) = +∞
    执行 n 次全局松弛操作
    枚举图中每条边 e : (u,v,w)
    松弛 d(v) = min{d(v),d(u) + w}

    证明:
    如果目前存在 d(u) 还不是真实值,一定有边能继续松弛
    任意最短路经过不超过 n − 1 条边, n - 1 次松弛足矣

    优势:算法很直观
    算法时间复杂度 O(nm)
    判负权环
    再多进行一次全局松弛,如果有 d 被更新,一定有负权环

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 505;
    const int inf = 1 << 29;
    
    int d[N][N], n, m;
    int main()
    {
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) d[i][j] = inf;
        for(int i = 1;i <= n; i++)
            d[i][i]=0; 
        for (int u, v, w, i = 1; i <= m; i++)
            cin >> u >> v >> w, d[u][v] = min(d[u][v], w);
    
        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]);
        //必须先枚举中间点 
    }

    如果有一次松弛没有任何边被改变,就说明已经结束了
    所以可以做一下小小的优化(常数级别)

    #include<cstdio>
    #include<iostream>
    #include<cstdlib>
    #include<iomanip>
    #include<cmath>
    #include<cstring>
    #include<string>
    #include<algorithm>
    #include<time.h>
    #include<queue>
    using namespace std;
    typedef long long ll;
    typedef long double ld;
    typedef pair<int,int> pr;
    const double pi=acos(-1);
    #define rep(i,a,n) for(int i=a;i<=n;i++)
    #define per(i,n,a) for(int i=n;i>=a;i--)
    #define Rep(i,u) for(int i=head[u];i;i=Next[i])
    #define clr(a) memset(a,0,sizeof a)
    #define pb push_back
    #define mp make_pair
    #define fi first
    #define sc second
    ld eps=1e-9;
    ll pp=1000000007;
    ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;}
    ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;}
    ll read(){
        ll ans=0;
        char last=' ',ch=getchar();
        while(ch<'0' || ch>'9')last=ch,ch=getchar();
        while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
        if(last=='-')ans=-ans;
        return ans;
    }
    //head
    
    struct edge{
        int u,v,w;
    }edg[n];
    int d[n],n,m,s;
    int main()
    {
        n=read(),m=read(),s=read()
        rep(i,1,n) d[i]=0x7fffffff;
        for(int u ,v,w,i=1;i<=m;i++)
        {
            cin>>u>>v>>w,edg[i]=(edge){u,v,w};
        }
        d[s]=0;
        rep(i,1,n)
        {
            rep(j,1,m)
            {
                int u=edg[j].u,v=edg[j].v,w=edg[j].w;
                if(d[v]>d[u]+w) d[v]=d[u]+w,tag=true;
            }
            if(!tag)break;
        }
    }

    SPFA
    Intuitive ideas
    Bellman-Ford 算法中,如果 d(u) 在上一次全局更新中没有被更新,
    那么这一次全局更新中不必松弛 u 的出边(就是不用它更新其他边)
    因为这个操作在上一次中已经做过了
    在 Bellman-Ford 的基础上改进
    维护队列 Queue,记录哪些点的出边有必要松弛
    取出 Queue 最前端的点 u
    枚举 u 的所有出边 e : (u,v,w)
    尝试松弛 e,若 d(v) > d(u) + w,更新 d(v),同时如果 v 不 在 Queue中,将 v 加入 Queue 中

    算法何时终止?
    记录每个点加入 Queue 的次数
    u 被加入 Queue 一次,意味着 d(u) 被更新了一次
    u 最多被更新 n − 1 次,否则肯定有负权环
    时间复杂度理论上是O(mn),但肯定不劣于 Bellman-ford,实际远不到 O(nm)

    Detail
    更新 d(v) 后,如何判断 v 是不是已经在 Queue 里
    用一个 Bool 数组记录就行, in/out Queue 的时候标记
    怎么记录一个点进入 Queue 的次数
    用一个计数数组记录就行, in/out Queue 的时候标记
    优势: 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;
            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;
    }

    本蒟蒻博客:传送门

  • 相关阅读:
    排序算法七:选择排序之堆排序
    排序算法七:选择排序之堆排序
    排序算法六:选择排序之直接选择排序
    排序算法六:选择排序之直接选择排序
    贝叶斯网络
    web 开发之js---js 中的数组操作
    web 开发之js---JS变量也要注意初始化
    web 开发之js---理解并解决IE的内存泄漏方式
    web 开发之js---巧用iframe实现jsp无刷新上传文件
    web 开发之js---js获取select标签选中的值
  • 原文地址:https://www.cnblogs.com/lcezych/p/10801912.html
Copyright © 2020-2023  润新知