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

虽说是再谈
然而我好像在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。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值