• 图上的文章(再谈最短路问题)


    虽说是再谈
    然而我好像在blog中从来没有谈过最短路的问题

    Dijkstra

    简单的dijkstra就是n^2的效率
    其中一个很重要的操作是找到dis最小的点
    由此进行下一次的转移
    如果我们在这里加上堆,那么复杂度就可以降为nlogn

    这是目前最好的复杂度
    在代码中,我用结构体和priority_queue完成的堆操作
    (写完我就要疯了)

    tip

    在省选中,就不要用map了,
    刚才和yhzq求证了一下,表示priority_queue还是可以用的

    struct heapnode{    //dijkstra用到的优先队列的结点 
        int d,u;        //d 距离,u 结点编号 
        bool operator <(const heapnode &a) const
        {
            return d>a.d;
        }
        //在优先队列中,是从大到小排序的,所以我们反向定义一下<号
    };
    
    struct node{
        int x,y,v;      //起点 终点 边权 
    }; 
    //定义边的结构体 
    
    struct Dijkstra{
        int n,m;               //点和边的数目 
        vector<node> e;        //存储边
        vector<int> G[N];      //存储每个点所连的边的编号
        bool p[N];             //已用标记
        int dis[N];            //最短距离
        int pre[N];            //每个点的转移边
    
        void init(int n)
        {
            this->n=n;
            e.clear();
            for (int i=1;i<=n;i++) G[i].clear();
        } 
    
        void add(int u,int w,int z)              //加边 
        {
            e.push_back((node){u,w,z});          //直接把边作为结构体塞进去
            m=e.size();
            G[u].push_back(m-1);                 //size是m,e中边是从0开始编号的 
        }
    
        void dijkstra(int s)
        {
            memset(dis,0x33,sizeof(dis));
            memset(pre,0,sizeof(pre));
            memset(p,1,sizeof(p));
            dis[s]=0;
    
            priority_queue<heapnode> Q;
            Q.push((heapnode){0,s});              //距离和点的编号作为一个结构体直接塞进优先队列
    
            while (!Q.empty())
            {
                heapnode now=Q.front(); Q.pop();
                int u=now.u;
                if (!p[u]) continue;              //打过标记的不能作为转移点
    
                p[u]=0;                           //千万不要忘了打标记 
    
                for (int i=0;i<G[u].size();i++)   //循环和u相连的所有边 
                {
                    node way=e[G[u][i]];
                    if (dis[way.y]>dis[u]+way.v)
                    {
                        dis[way.y]=dis[u]+way.v;
                        pre[way.y]=G[u][i];
                        Q.push((heapnode){dis[way.y],way.y});
                    }
                } 
            } 
        }
    };

    dijkstra不仅可以用来求单源最短路
    还可以枚举两点之间的所有最短路,以及统计最短路条数

    枚举最短路

    找出所有点到目标点的最短路d[i]
    然后从起点开始出发
    每次只能走d[j]=d[i]+w(i,j)的边
    这样得到的一定是起点到终点的最短路

    统计最短路条数

    有dp的意味
    令f[i]表示从i到终点的最短路条数
    则有f[i]=sum{f[j]|d[i]=d[j]+w(i,j)}
    从后往前推,边界条件是f[end]=1

    最短路树

    用Dijkstra可以求出单源最短路树,方法是在发现dis[j]>dis[i]+w(i,j)时,
    除了更新dis之外,还要记录一下p[j]=i
    这样把p看作是父指针,所有点就形成了一棵树
    这样要从起点出发沿最短路走到任意其他结点,只要沿着最短路树的边走即可
    在Dijkstra的模板中,我们实际上已经求出了p数组

    Bellman-Ford

    Bellmax-Ford算法一个很重要的应用就是判断负环
    在迭代n-1次后如果还可以进行松弛操作说明一定存在负环
    如果用队列实现,那么当某个点入队n次时就可以判断存在负环

    在代码中有这样一段:

    for (int i=1;i<=n;i++) 
    {
        dis[i]=0;
        in[i]=1;
        Q.push(i);
    }

    因为Bellman求的也是单源最短路,所以这就相当于建立了一个虚拟源点
    并且把所有与虚拟原点相连的结点都更新了dis

    struct node{
        int x,y,v;
    };
    
    struct Bellman{
        int n,m;
        vector<node> e;           //边列表 
        vector<int> G[N];        //每个结点所连边的编号 
        bool in[N];         //在对列中的标记 
        int pre[N];         //转移边 
        int dis[N];         //最短路 
        int cnt[N];         //入队次数
    
        void init(int n)
        {
            this->n=n;
            e.clear();
            for (int i=1;i<=n;i++) G[i].clear();
        }
    
        void add(int u,int w,int z)
        {
            e.push_back((node){u,w,z});
            m=e.size();
            G[u].push_back(m-1);
        }
    
        bool fuhuan(int s)
        {
            queue<int> Q;
            memset(in,0,sizeof(in));
            memset(cnt,0,sizeof(cnt));
            for (int i=1;i<=n;i++) 
            {
                dis[i]=0;
                in[i]=1;
                Q.push(i);
            }
    
            while (!Q.empty())
            {
                int now=Q.front(); Q.pop();
                in[now]=0;
    
                for (int i=0;i<G[now].size();i++)
                {
                    node way=e[G[now][i]];
                    int v=way.y;
                    if (dis[v]>dis[now]+way.v)
                    {
                        dis[v]=dis[now]+way.v;
                        pre[v]=G[now][i];
                        if (!in[v])
                        {
                            Q.push(v);
                            in[v]=1; 
                            if (++cnt[v]>n) return 1;
                        }
                    }
                }
            }
            return 0;
        }
    };

    差分约束

    这是图论中很重要的一部分
    ta的一般形式就是

    不等式组,每个不等式满足形式:xj-xi<=bk

    差分约束系统的经典解法就是最短路
    xj-xi<=bk,移项可得xj<=bk+xi
    符合最短路的形式

    设dis表示源点到达每一个点的最短距离
    则对于任一条边(i—>j)都有dis[j]<=dis[i]+w(i,j)

    这样我们针对每一个不等式xj-xi<=bk建图,
    单向连接i—>j,边权为bk
    在图上运行Bellman,得到的dis即为一组可行解
    如果Bellman运行失败(存在环),则原差分约束系统无解

    下面是一些前辈对于差分约束的总结(也有我自己的感悟):

    • 差分约束的作用

      求值:它可以求解不等式的一组解

      判环:判正负环

    • 差分约束中的松弛操作

      举个最短路的例子:
      1->2 ,v=3
      1->3 ,v=1
      3->2 ,v=1
      即以1为起点,d[2]>d[3]+w(3,2),可以进行松弛
      而三条边本身表示的是:a(2)-a(1)<=3,a(3)-a(1)<=1,a(2)-a(3)<=1,
      用1->3->2这条边更新1->2,
      实际上表示的是后两个不等式之和a(2)-a(1)<=2比第一个不等式的约束条件更强

    • 最长路与最短路的区别

      两者不仅是在三角不等式的表现形式上不同,具体求解的值也不同

      在求解时,我们一般加入超级源点S,并建立权值为0的边
      对于一个不等式组来说,只要有一组解,那么同时加上定值k,仍然满足约束条件。

      最短路:d[v] < d[u]+w(u,v)
      求解出来的是不等式的最大值
      解释一下:
      若确定其中一个的值,并非能得到每个数的定值,因为是不等式,所以每个值有其取值范围
      这里的最大值就是在源点S的限定条件内,所能够取值的上界
      仔细思考,这里的最短路只是满足不等式组条件所找到的解,
      由于最短路是由大到小做松弛操作,找到的最短路即为“最大”。

      最长路:d[v] > d[u]+w(u,v)
      求的是最小值

    • 环的判定

      在判环过程中,Bellman的判定条件为入队n次
      其实质是最短路最长经过(n-1)条边(一共有n个点,这n个点全在一个大环内),
      所以入队n次队列仍不为空,即可以判定存在正负环
      当点的总数+1(加入了一个超级源点),对应的判环条件也就变更为cnt>n。

    • 关于图的连通性

      无论是求值,还是判环,原图不构成强连通的前提下都是不能搜索到每一个点的,
      所以才出现设立一个源点S的方法
      事实上,最实用的做法是在初始化时,就把所有点加入队列,并把dis[i]全部初始化为0
      不仅仅是解决了连通的问题,更是避免了点数计算上的错误

    • 不等式的变形

      差分约束解决的是>=和<=的问题,若题目给出的是 > k(或 < k),需要变形
      比如若k是整数,> k等价于>=k+1。

  • 相关阅读:
    最全前端面试题
    经常犯的思维误区
    鸿蒙系统发布会
    前端面试题
    怎么做一个竖排文字?
    canvas-台球玩法
    canvas-自由落体球
    canvas-画一颗心
    canvas-学写字
    常用的65条正则表达式
  • 原文地址:https://www.cnblogs.com/wutongtong3117/p/7673061.html
Copyright © 2020-2023  润新知