zoukankan      html  css  js  c++  java
  • 【数模集】 图论常用算法 基础

    图与网络优化概述

             图论中所谓的“图”是指某类具体事物和这些事物之间的联系。如果我们用点表示这些具体事物,用连接两点的线段(直的或曲的)表示两个事物的特定的联系,就得到了描述这个“图”的几何形象。图论为任何一个包含了一种二元关系的离散系统提供了一个数学模型,借助于图论的概念、理论和方法,可以对该模型求解。

    我们首先通过一些例子来了解网络优化问题。

    例1  最短路问题(SPP-shortest path problem)

        一名货柜车司机奉命在最短的时间内将一车货物从甲地运往乙地。从甲地到乙地的公路网纵横交错,因此有多种行车路线,这名司机应选择哪条线路呢?假设货柜车的运行速度是恒定的,那么这一问题相当于需要找到一条从甲地到乙地的最短路。(最短路+迪杰斯特拉)

    例2  公路连接问题

       某一地区有若干个主要城市,现准备修建高速公路把这些城市连接起来,使得从其中任何一个城市都可以经高速公路直接或间接到达另一个城市。假定已经知道了任意两个城市之间修建高速公路的成本,那么应如何决定在哪些城市间修建高速公路,使得总成本最小?(最小代价生成树

    例3  指派问题(assignment problem)
         一家公司经理准备安排几名员工去完成项任务, 每人一项。由于各员工的特点不同,不同的员工去完成同一项任务时所获得的回报是不同的。 如何分配工作方案可以使总回报最大?(基础目标规划lingo可解或最小费用最大流解或匈牙利算法)

    例4 中国邮递员问题(CPP-chinese postman  problem)
        一名邮递员负责投递某个街区的邮件。如何为他(她)设计一条最短的投递路线(从邮局出发,经过投递区内每条街道至少一次,最后返回邮局)?由于这一问题是我国梅谷教授1960年首先提的,所以国际上称之为中国邮递员问题。(整数规划,最小代价生成树,复杂可用DNA算法等)

    例5  运输问题(transportation problem)

       某种原材料有个产地,现在需要将原材料从产地运往个使用这些原材料的工厂。假定个产地的产量和家工厂的需要量已知,单位产品从任一产地到任一工厂的运费已知,那么如何安排运输方案可以使总运输成本最低?(lingo目标规划可解)


            上述问题有两个共同的特点:一是它们的目的都是从若干可能的安排或方案中寻求某种意义下的最优安排或方案,数学上把这种问题称为最优化或优化(optimization)问题;二是它们都易于用图形的形式直观地描述和表达,数学上把这种与图相关的结构称为网络(network)。与图和网络相关的最优化问题就是网络最优化或称网络优化 (netwok optimization)问题。 


     1.  求 两个指定顶点之间的最短路径(Dijkstra算法)

    比如以下问题背景:给出了一个连接若干个城镇的铁路网络,在这个网络的两个指定城镇间,找一条最短铁路线。

    帮助理解链接:

    http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html

    http://www.cnblogs.com/heqinghui/archive/2012/07/26/2609563.html

    代码模板:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<cstring>
    #include<algorithm>
    #include<vector>
    #include<fstream>
    using namespace std;
    
    const int maxnum = 100;
    const int maxint = 2147483647;
    int dist[maxnum];     // 表示当前点到源点的最短路径长度
    int prev[maxnum];     // 记录当前点的前一个结点
    int c[maxnum][maxnum];   // 记录图的两点间路径长度
    int n, line;             // n表示图的结点数,line表示路径个数
    void Dijkstra(int n, int v, int *dist, int *prev, int c[maxnum][maxnum])
    {
        bool s[maxnum];    // 判断是否已存入该点到S集合中
        for(int i=1; i<=n; ++i)
        {
            dist[i] = c[v][i];
            s[i] = 0;     // 初始都未用过该点
            if(dist[i] == maxint)
                prev[i] = 0;
            else
                prev[i] = v;
        }
        dist[v] = 0;
        s[v] = 1;
    
        // 依次将未放入S集合的结点中,取dist[]最小值的结点,放入结合S中
        // 一旦S包含了所有V中顶点,dist就记录了从源点到所有其他顶点之间的最短路径长度
        for(int i=2; i<=n; ++i)
        {
            int tmp = maxint;
            int u = v;
            // 找出当前未使用的点j的dist[j]最小值
            for(int j=1; j<=n; ++j)
                if((!s[j]) && dist[j]<tmp)
                {
                    u = j;              // u保存当前邻接点中距离最小的点的号码
                    tmp = dist[j];
                }
            s[u] = 1;    // 表示u点已存入S集合中
    
            // 更新dist
            for(int j=1; j<=n; ++j)
                if((!s[j]) && c[u][j]<maxint)
                {
                    int newdist = dist[u] + c[u][j];
                    if(newdist < dist[j])
                    {
                        dist[j] = newdist;
                        prev[j] = u;
                    }
                }
        }
    }
    void searchPath(int *prev,int v, int u)
    {
        int que[maxnum];
        int tot = 1;
        que[tot] = u;
        tot++;
        int tmp = prev[u];
        while(tmp != v)
        {
            que[tot] = tmp;
            tot++;
            tmp = prev[tmp];
        }
        que[tot] = v;
        for(int i=tot; i>=1; --i)
            if(i != 1)
                cout << que[i] << " -> ";
            else
                cout << que[i] << endl;
    }
    
    int main()
    {
        //freopen("input.txt", "r", stdin);
        // 各数组都从下标1开始
        // 输入结点数
        cin >> n;
        // 输入路径数
        cin >> line;
        int p, q, len;          // 输入p, q两点及其路径长度
        // 初始化c[][]为maxint
        for(int i=1; i<=n; ++i)
            for(int j=1; j<=n; ++j)
                c[i][j] = maxint;
        for(int i=1; i<=line; ++i)
        {
            cin >> p >> q >> len;
            if(len < c[p][q])       // 有重边
            {
                c[p][q] = len;      // p指向q
                c[q][p] = len;      // q指向p,这样表示无向图
            }
        }
       for(int i=1; i<=n; ++i)
            dist[i] = maxint;
        for(int i=1; i<=n; ++i)
        {
            for(int j=1; j<=n; ++j)
                printf("%-16d", c[i][j]);
            printf("
    ");
        }
        Dijkstra(n, 1, dist, prev, c);   //仅调用函数求出了源点到其他点的距离 改法:Dijkstra(n, x, dist, prev, c);  其中x=1,2,3,4,...,n
    
    //    for(int i=1; i<=n; ++i)   //dist存储了源点到其他点的距离情况
    //    {
    //        printf("%-16d", dist[i]);
    //    }
        printf("
    ");
         // 最短路径长度
        cout << "源点到最后一个顶点的最短路径长度: " << dist[n] << endl;
         // 路径
        cout << "源点到最后一个顶点的路径为: ";
        searchPath(prev, 1, n);
        return 0;
    }
    
    
    /*
    输入数据:
     5
     7
     1 2 10
     1 4 30
     1 5 100
     2 3 50
     3 5 10
     4 3 20
     4 5 60
     输出数据:
     999999 10 999999 30 100
     10 999999 50 999999 999999
     999999 50 999999 20 10
     30 999999 20 999999 60
     100 999999 10 60 999999
     源点到最后一个顶点的最短路径长度: 60
     源点到最后一个顶点的路径为: 1 -> 4 -> 3 -> 5
    */
    

     2.  求每对顶点之间的最短距离(Floyd算法)

    Floyd算法模板:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<cstring>
    #include<algorithm>
    #include<vector>
    #include<fstream>
    using namespace std;
    
    //设点与点之间的距离均为double型
    double INFTY=2147483647;
    const int MAX=1000;
    double dis[MAX][MAX];
    double a[MAX][MAX];
    int path[MAX][MAX];
    int n,m; //结点个数
    
    void Floyd()
    {
        int i,j,k;
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=n;j++)
            {
                dis[i][j]=a[i][j];
                if(i!=j&&a[i][j]<INFTY)
                {
                    path[i][j]=i;
                }
                else
                    path[i][j]=-1;
            }
        }
    
        for(k=1;k<=n;k++)
        {
            for(i=1;i<=n;i++)
            {
                for(j=1;j<=n;j++)
                {
                    if(dis[i][k]+dis[k][j]<dis[i][j])
                    {
                        dis[i][j]=dis[i][k]+dis[k][j];
                        path[i][j]=path[k][j];
                    }
                }
            }
        }
    }
    
    int main()
    {
        //freopen("datain.txt","r",stdin);
        int beg,enda;
        double dist;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
        {
           for(int j=1;j<=n;j++)
           {
                if(i==j)
                    a[i][j]=0;
                else
                    a[i][j]=INFTY;
           }
        }
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%lf",&beg,&enda,&dist);
            a[beg][enda]=a[enda][beg]=dist;
        }
        Floyd();
        for(int i=1;i<=n;i++)
        {
           for(int j=1;j<=n;j++)
           {
                printf("%-12lf",dis[i][j]);
           }
           printf("
    ");
        }
        return 0;
    }


    3.最小代价生成树

    注意:prim算法适合稠密图,即边数较多的带权图,其时间复杂度为O(n^2),其时间复杂度与边的数目无关,而kruskal算法的时间复杂度为O(eloge)跟边的数目有关,适合稀疏图。

    3.1 prim算法模板,当前测试点下标从0开始,输出从1开始:

    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<limits.h>
    #include<algorithm>
    using namespace std;
    const int N=1000+1;
    //图中顶点个数
    
    //图中顶点个数
    //#define V 5
    #define V 7
    //未在mstSet中的点的集合中,找出最小key的点
    int minKey(int key[], bool mstSet[])
    {
       int min = INT_MAX, min_index;
    
       for (int v = 0; v < V; v++)
         if (mstSet[v] == false && key[v] < min)
             min = key[v], min_index = v;
    
       return min_index;
    }
    
    // 打印MST
    void printMST(int parent[], int n, int graph[V][V])
    {
       int sum = 0;
       printf("Edge   Weight
    ");
       for (int i = 1; i < V; i++)
          sum += graph[i][parent[i]];
       for (int i = 1; i < V; i++)
          printf("%d - %d    %d 
    ", parent[i]+1, i+1, graph[i][parent[i]]);
       printf("最小代价和:%d
    ",sum);
    }
    
    // Prim算法
    void primMST(int graph[V][V])
    {
         int parent[V]; // 保持MST信息
         int key[V];   // 所有顶点的代价值
         bool mstSet[V];  //当前包含在MST中点的集合
    
         // 初始为无穷大
         for (int i = 0; i < V; i++)
            key[i] = INT_MAX, mstSet[i] = false;
    
         key[0] = 0;     //
         parent[0] = -1; // 第一个作为树的根。
    
         //  MST 有V的顶点
         for (int count = 0; count < V-1; count++)
         {
            int u = minKey(key, mstSet);
            // 添加u到 MST Set
            mstSet[u] = true;
            //更新和u相连的顶点的代价
            for (int v = 0; v < V; v++)
              if (graph[u][v] && mstSet[v] == false && graph[u][v] <  key[v])
                 parent[v]  = u, key[v] = graph[u][v];
         }
    
         // 打印生成的MST
         printMST(parent, V, graph);
    }
    
    int main()
    {
       /* 创建以下的图
              2    3
          (0)--(1)--(2)
           |   /    |
          6| 8/   5 |7
           | /      |
          (3)-------(4)
                9          */
    //   int graph[V][V] = {{0, 2, 0, 6, 0},
    //                      {2, 0, 3, 8, 5},
    //                      {0, 3, 0, 0, 7},
    //                      {6, 8, 0, 0, 9},
    //                      {0, 5, 7, 9, 0},
    //                     };
    
         int graph[V][V];
         for(int i=0;i<V;i++)
         {
             for(int j=0;j<V;j++)
                graph[i][j]=0;
         }
         int m,beg,enda,val;
         cin>>m;
         for(int i=1;i<=m;i++)
         {
             cin>>beg>>enda>>val;
             graph[beg][enda]=graph[enda][beg]=val;
         }
        // Print the solution
        primMST(graph);
    
        return 0;
    }
    
    
    /*
    10
    0 1 50
    0 2 60
    1 3 65
    1 4 40
    2 3 52
    2 6 45
    3 4 50
    3 5 30
    3 6 42
    4 5 70
    */
    

    3.2 kruskal算法模板:

    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cstdio>
    #include<algorithm>
    using namespace std;
    #define MAX 1000
    int father[MAX], son[MAX];
    int v, l;  //v表示结点数,l表示边数
    
     struct Kruskal //存储边的信息
    {
    	int a;
    	int b;
    	int value;
    };
    
    bool cmp(const Kruskal & a, const Kruskal & b)
    {
    	return a.value < b.value;
    }
    
    int unionsearch(int x) //查找根结点+路径压缩
    {
    	return x == father[x] ? x : unionsearch(father[x]);
    }
    
    bool join(int x, int y) //合并
    {
    	int root1, root2;
    	root1 = unionsearch(x);
    	root2 = unionsearch(y);
    	if(root1 == root2) //为环
    		return false;
    	else if(son[root1] >= son[root2])
    		{
    			father[root2] = root1;
    			son[root1] += son[root2];
    		}
    		else
    		{
    			father[root1] = root2;
    			son[root2] += son[root1];
    		}
    	return true;
    }
    
    int main()
    {
    	int ltotal, sum, flag;
    	Kruskal edge[MAX];
        scanf("%d%d", &v, &l);
        ltotal = 0, sum = 0, flag = 0;
        for(int i = 1; i <= v; ++i) //初始化
        {
            father[i] = i;
            son[i] = 1;
        }
        for(int i = 1; i <= l ; ++i)
        {
            scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].value);
        }
        sort(edge + 1, edge + 1 + l, cmp); //按权值由小到大排序
        for(int i = 1; i <= l; ++i)
        {
            if(join(edge[i].a, edge[i].b))
            {
                ltotal++; //边数加1
                sum += edge[i].value; //记录权值之和
                cout<<edge[i].a<<"->"<<edge[i].b<<endl;
            }
            if(ltotal == v - 1) //最小生成树条件:边数=顶点数-1
            {
                flag = 1;
                break;
            }
        }
        if(flag) printf("%d
    ", sum);
        else printf("data error.
    ");
    	return 0;
    }
    /*
    7  10
    1 2 50
    1 3 60
    2 4 65
    2 5 40
    3 4 52
    3 7 45
    4 5 50
    4 6 30
    4 7 42
    5 6 70
    */
    

    4. 网络流

    4.1最大流算法(最简单的EK算法)

    //warn:编号从1开始
    #include <iostream>
    #include <queue>
    #include<string.h>
    using namespace std;
    #define arraysize 201
    int maxData = 0x7fffffff;
    int capacity[arraysize][arraysize]; //记录残留网络的容量
    int flow[arraysize];                //标记从源点到当前节点实际还剩多少流量可用
    int pre[arraysize];                 //标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中
    int n,m;                            //n表示边数,m表示结点数
    queue<int> myqueue;
    int BFS(int src,int des)
    {
        int i;
        while(!myqueue.empty())       //队列清空
            myqueue.pop();
        for(i=1;i<m+1;++i)
        {
            pre[i]=-1;
        }
        pre[src]=0;
        flow[src]= maxData;
        myqueue.push(src);
        while(!myqueue.empty())
        {
            int index = myqueue.front();
            myqueue.pop();
            if(index == des)            //找到了增广路径
                break;
            for(i=1;i<m+1;++i)
            {
                if(i!=src && capacity[index][i]>0 && pre[i]==-1)
                {
                     pre[i] = index; //记录前驱
                     flow[i] = min(capacity[index][i],flow[index]);   //关键:迭代的找到增量
                     myqueue.push(i);
                }
            }
        }
        if(pre[des]==-1)      //残留图中不再存在增广路径
            return -1;
        else
            return flow[des];
    }
    int maxFlow(int src,int des)
    {
        int increasement= 0;
        int sumflow = 0;
        while((increasement=BFS(src,des))!=-1)
        {
             int k = des;          //利用前驱寻找路径
             while(k!=src)
             {
                  int last = pre[k];
                  capacity[last][k] -= increasement; //改变正向边的容量
                  capacity[k][last] += increasement; //改变反向边的容量
                  k = last;
             }
             sumflow += increasement;
        }
        return sumflow;
    }
    int main()
    {
        int i;
        int start,end,ci;
        while(cin>>n>>m)
        {
            memset(capacity,0,sizeof(capacity));
            memset(flow,0,sizeof(flow));
            for(i=0;i<n;++i)
            {
                cin>>start>>end>>ci;
                if(start == end)               //考虑起点终点相同的情况
                   continue;
                capacity[start][end] +=ci;     //此处注意可能出现多条同一起点终点的情况
            }
            cout<<maxFlow(1,m)<<endl;
        }
        return 0;
    }
    
    /*
        5 4
        1 2 40
        1 4 20
        2 4 20
        2 3 30
        3 4 10
    //最大流:50
    */
    


    4.2  二分最大权匹配 KM算法模板

    类似题目链接:http://acm.njupt.edu.cn/acmhome/problemdetail.do?&method=showdetail&id=2073

    陈叔叔题解:http://www.cnblogs.com/njczy2010/p/4384505.html

    KM模板(注:如果求的是最小值,main函数初始时取负值,并且out函数输出-KM()即可):

    #include <cstdio>
    #include <cstring>
    #include <stack>
    #include <vector>
    #include <algorithm>
    #include <map>
    #include <string>
    #include <queue>
    #include <cmath>
    
    #define ll long long
    int const N = 35;
    int const M = 100005;
    int const INF = 0x3f3f3f3f;
    ll const mod = 1000000007;
    
    using namespace std;
    
    int T;
    int n;
    int nx,ny;       //两边的点数
    int g[N][N];    //二分图描述
    int linker[N],lx[N],ly[N];      //y中各点匹配状态,x,y中的点标号
    int slack[N];
    bool visx[N],visy[N];
    
    bool DFS(int x)
    {
        visx[x] = true;
        for(int y = 0;y < ny;y++)
        {
            if(visy[y]) continue;
            int tmp = lx[x] + ly[y] -g[x][y];
            if(tmp == 0)
            {
                visy[y] = true;
                if(linker[y] == -1 || DFS(linker[y]))
                {
                    linker[y] = x;
                    return true;
                }
            }
            else if(slack[y] > tmp)
                slack[y] = tmp;
        }
        return false;
    }
    
    int KM()
    {
        memset(linker,-1,sizeof(linker));
        memset(ly,0,sizeof(ly));
        for(int i = 0;i < nx;i++)
        {
            lx[i] = -INF;
            for(int j = 0;j < ny;j++)
                if(g[i][j] > lx[i])
                    lx[i] = g[i][j];
        }
        for(int x =0;x < nx;x++)
        {
            for(int i = 0;i < ny ;i++)
                slack[i] = INF;
            while(true)
            {
                memset(visx,false,sizeof(visx));
                memset(visy,false,sizeof(visy));
                if(DFS(x)) break;
                int d = INF;
                for(int i = 0;i < ny;i++)
                    if(!visy[i] && d > slack[i])
                        d = slack[i];
                for(int i = 0 ; i < nx ;i++)
                    if(visx[i])
                        lx[i] -= d;
                for(int i = 0 ; i < ny ;i++)
                {
                    if(visy[i]) ly[i] += d;
                    else slack[i] -= d;
                }
            }
        }
        int res = 0;
        for(int i = 0;i < ny ;i++)
            if(linker[i] != -1)
                res += g[ linker[i] ][i];
        return res;
    }
    
    void ini()
    {
        scanf("%d",&n);
        int i,j;
        for(i = 0;i < n;i++){
            for(j = 0;j < n;j++)
                scanf("%d",&g[i][j]);
        }
        nx = ny =n;
    }
    
    void solve()
    {
    
    }
    
    void out()
    {
        printf("%d
    ",KM());
    }
    
    int main()
    {
        //freopen("data.in","r",stdin);
        //freopen("data.out","w",stdout);
        scanf("%d",&T);
        //for(int cnt=1;cnt<=T;cnt++)
        while(T--)
        //while(scanf("%d%d%d",&a,&b,&n)!=EOF)
        {
            ini();
            solve();
            out();
        }
    }
    

    4.3 最小费用最大流

    大大2135题解链接:http://www.cnblogs.com/rainydays/archive/2012/07/05/2577386.html

    算法模板:POJ2135可转化,有点像中国邮递员问题

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    using namespace std;
    
    #define typef int
    #define typec int
    #define maxn 1005
    #define maxm 10005
    #define N maxn + 2
    #define E maxm * 4 + 4
    const typef inff = 0x3f3f3f3f;
    const typec infc = 0x3f3f3f3f;
    
    struct network
    {
        int nv, ne, pnt[E], nxt[E];
        int vis[N], que[N], head[N], pv[N], pe[N];
        typef flow, cap[E];
        typec cost, dis[E], d[N];
        void addedge(int u, int v, typef c, typec w)
        {
            pnt[ne] = v;
            cap[ne] = c;
            dis[ne] = +w;
            nxt[ne] = head[u];
            head[u] = (ne++);
            pnt[ne] = u;
            cap[ne] = 0;
            dis[ne] = -w;
            nxt[ne] = head[v];
            head[v] = (ne++);
        }
        int mincost(int src, int sink)
        {
            int i, k, f, r;
            typef mxf;
            for (flow = 0, cost = 0;;)
            {
                memset(pv, -1, sizeof(pv));
                memset(vis, 0, sizeof(vis));
                for (i = 0; i < nv; ++i)
                    d[i] = infc;
                d[src] = 0;
                pv[src] = src;
                vis[src] = 1;
                for (f = 0, r = 1, que[0] = src; r != f;)
                {
                    i = que[f++];
                    vis[i] = 0;
                    if (N == f)
                        f = 0;
                    for (k = head[i]; k != -1; k = nxt[k])
                        if (cap[k] && dis[k] + d[i] < d[pnt[k]])
                        {
                            d[pnt[k]] = dis[k] + d[i];
                            if (0 == vis[pnt[k]])
                            {
                                vis[pnt[k]] = 1;
                                que[r++] = pnt[k];
                                if (N == r)
                                    r = 0;
                            }
                            pv[pnt[k]] = i;
                            pe[pnt[k]] = k;
                        }
                }
                if (-1 == pv[sink])
                    break;
                for (k = sink, mxf = inff; k != src; k = pv[k])
                    if (cap[pe[k]] < mxf)
                        mxf = cap[pe[k]];
                flow += mxf;
                cost += d[sink] * mxf;
                for (k = sink; k != src; k = pv[k])
                {
                    cap[pe[k]] -= mxf;
                    cap[pe[k] ^ 1] += mxf;
                }
            }
            return cost;
        }
        void build(int v, int e)
        {
            nv = v;
            ne = 0;
            memset(head, -1, sizeof(head));
            int x, y;
            typec w;
            for (int i = 0; i < e; ++i)
            {
                scanf("%d%d%d", &x, &y, &w);
                addedge(x, y, 1, w);// add arc (u->v, f, w)
                addedge(y, x, 1, w);
            }
            addedge(0, 1, 2, 0);
            addedge(v - 2, v - 1, 2, 0);
        }
    } g;
    
    int n, m;
    
    int main()
    {
        //freopen("t.txt", "r", stdin);
        scanf("%d%d", &n, &m);
        g.build(n + 2, m);
        printf("%d
    ", g.mincost(0, n + 1));
        return 0;
    }
    /*
    4 5
    1 2 1
    2 3 1
    3 4 1
    1 3 2
    2 4 2
    */

    版权声明:本文为博主原创文章,未经博主允许不得转载。

  • 相关阅读:
    进度条
    html5 表单新增事件
    html5 表单的新增type属性
    html5 表单的新增元素
    html5 语义化标签
    jq 手风琴案例
    codeforces 702D D. Road to Post Office(数学)
    codeforces 702C C. Cellular Network(水题)
    codeforces 702B B. Powers of Two(水题)
    codeforces 702A A. Maximum Increase(水题)
  • 原文地址:https://www.cnblogs.com/Tobyuyu/p/4965260.html
Copyright © 2011-2022 走看看