zoukankan      html  css  js  c++  java
  • SPFA与(负)环-BFS与DFS

    前言

    一星期前用差分约束写了奖学金这道题,在判断impossible的时候,使用了spfa()常用的边数判断法,即更新一个点使用的边数最多应当是n条,在其他点跑的飞快的情况下T了一个点,是一个有环的点,看别人的代码,有的是用dfs判环(这个也有坑下面说),还有的用了双端队列优化。看的我一愣一愣的。由于对spfa及其它判环的原理并不熟悉,我不知道我错在哪里,只有改成上述两种方式才能过。于是我去洛谷寻找spfa判环的模板题,借此了解spfa,在这道题中使用dfs竟然会TLE,难道dfs不是O(m)的吗?什么样的构造会让它Tle?这次的随笔探讨的就是这几个问题:
    1. spfa原理是什么?复杂度上界怎么来的?
    2. spfa怎么处理环?bfs的两种实现,dfs的一种实现,有向图和无向图的区别?

    关于spfa

    spfa的原理

    • 实现原理
      spfa是队列优化的bellman-ford,其实我感觉它就是个普通的bfs(别人的说法SPFA 在形式上和BFS非常类似,不同的是BFS中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去)
      具体来说就是:从源点出发,遍历所有出边并入队,再然后再遍历队列中的点,每次进行"松弛操作"(其实我觉得叫"更新操作"或者叫"纠正操作"更容易理解)
      记每次队头的点为x,从对列中弹出队头x。
      记dis[]为一个点到源点的最短/长路,y为出度点,即edge[i].to,求最短路的时候松弛的条件dis[y]>dis[x]+edge[i].cost,最长路的时候就是dis[y]<dis[x]+edge[i].cost;
      当一个点满足这个条件就该更新dis,或者叫纠正dis,如果这个点不在队列中,就把它放入队列。
      重复以上步骤直到队列为空。

    • 正确性(逻辑原理):
      只要联通,bfs必定会遍历全图,每个点第一次被扫到必定更新,必定入队。
      队列中每次都会保留待扩展的节点,如果存在dis还能更新,它就会一直更新下去,队列就不会为空,只有所有点都完成更新,队列才会空,保证了正确性。

    • 复杂度:
      运气好的话,一次遍历就把图更新完了,所有点都不会再扩展,不需要再更新,比如链,或者说更新的比较少。
      运气一般的话,因为每次队列中都存在待扩展节点,所以说队头出来之后如果扫到了队列中的点,只可能更新dis而不会再把这个点多展开一次(dfs就可能搜完这次,回溯回来再搜一次),复杂度比dfs优。
      (图)

      运气很不好,就会达到上界,跨入(O(nm))的级别,怎么达到?
      比如说用网格图 来自洛谷某讨论:

      网格图,10行10000列,纵向边权为1,横向边权随机,亲测普通spfa要跑1分钟

      非常可怕的复杂度。
      所以对于正权图,跑Dijkstra+heap,稳定log

    spfa的环判定

    • BFS
      • 记录入队次数
        用cnt记录每个点入队的次数,如果一个点入队次数大于等于n次,就认为存在负环
        为什么?对于bfs,一次bfs中,没有负环的情况下一个点最多入队n次(从源点出连向所有其它点,而所有其他点又指向一个点,每个点正好将这个点更新一次,入队一次),如果说还能再更新一次这个点,那一定是这个点出发跑了一个负环再到达自己,cnt在进入是否
        入队的判断中执行
        。代码如下
    bool spfa(){   
         q.push(1);
         vis[1]=1;
       while(!q.empty()){
       	int x=q.front();q.pop();
       	vis[x]=0;
       	for(reg int i=head[x];i;i=edge[i].next){
       		int y=edge[i].to;
       		if(dis[y]>dis[x]+edge[i].cost){
       			dis[y]=dis[x]+edge[i].cost;
       			cnt[y]=cnt[x]+1;
       			if(cnt[y]>=n) return 1;//包含的边数 
       			if(!vis[y]){
       				cnt[y]++;
       				/*if(cnt[y]>=n) return 1; */
       				q.push(y);
       				vis[y]=1;
       			}
       		}
       	}
       }
       return 0;
    }
    
    • 记录到达某个点使用的经过的边数
      我们可以判断1号点到i号点的最短路径长度是否<n(即经过的点数<=n,没有任何一个点被重复经过)用cnt[y]=cnt[x]+1,cnt[y]>=n来判断,只要进入松弛的判断中就可以执行
      代码如下
    bool spfa(){
    	q.push(1);
    	vis[1]=1;
    	while(!q.empty()){
    		int x=q.front();q.pop();
    		vis[x]=0;
    		for(reg int i=head[x];i;i=edge[i].next){
    			int y=edge[i].to;
    			if(dis[y]>dis[x]+edge[i].cost){
    				dis[y]=dis[x]+edge[i].cost;
    				/*cnt[y]=cnt[x]+1;
    				if(cnt[y]>=n) return 1;//包含的边数 */
    				if(!vis[y]){
    					cnt[y]++;
    					if(cnt[y]>=n) return 1; 
    					q.push(y);
    					vis[y]=1;
    				}
    			}
    		}
    	}
    	return 0;
    }
    
    • DFS
      dfs版本的思路是在一次搜索中,一旦一个点更新了两次,就说明有负环,为什么?首先搜到一个点两次肯定是有环的,然后第一次更新完了第二次又更新了,除非环的权值是负的,否则不可能更新,于是这样就简单的找到了负环。
      对于dfs,有有向图和无向图的区别。如果是一个无向图,vis直接标记,每个非标记点都可以进入,也不用回溯清零,一旦第二次扫描到某个点,就存在环。
      如果是一个有向图,扫完一个点的所有出边需要把这个点的标记给去除,不然一个点出发两条路从而到达一个点且无回路,也会被认为是一个环。
      dfs一般来说效率是很高的,不过特殊构造下还是会T并且一次只能处理一个源点出发是否存在环,要求整个图,需要扫描n个点
      代码如下
    bool check(int k)
    {
    	vis[k]=1;
    	for(int i=head[k];i;i=edge[i].next)
    	{
    		int y=edge[i].to;
    		if(dis[k]+edge[i].cost<dis[y]){
    			dis[y]=edge[i].cost+dis[k];
    			if(!vis[y])
    			{
    				if(check(y))//前面搜到了负环 
    					return 1;
    			}
    			else return 1;//一次深搜中搜了一个点两次,则存在负环。 
    		}
    	}
    	vis[k]=0;
    	return 0;
    }
    

    二者的取舍

    用差分约束并且用bfs判负环导致奖学金这道题T了之后:这判负环必用dfs啊!
    用dfs做了洛谷P3385 判负环 之后:这dfs有问题啊。
    怎么说呢,如果题目说数据随机,但是可能有环,用dfs判一下环显然会更快,很多时候都是(O(m))的。
    如果说没有说数据随机,有几率卡dfs。
    如果说题目一看就是得用spfa的,说是有数据存在负环的,那一般不会卡spfa了,但是奖学金这道题就是卡了,不过用了双端队列也就是SLF优化之后就会快很多,直接过了。
    总结来说,实际上dfs用的时候需要谨慎,确定有负环的情况下SLF优化还是蛮必要的,这样才更有保障。
    另外肯定不能用dfs求最短路,那跟暴力没区别。
    如果图没有负权边,那妥妥的Dijkstra+heap。

  • 相关阅读:
    使用C# impersonation进行windows帐号的校验
    今天,如何将你现有的网站移动互联网化【译】
    吐槽一下拖延的生活状态
    Web前端开发工程师编程能力飞升之路【转】
    公司框架集成log4net时遇到的问题
    我所认知的电子商务解决方案
    Web开发攻城狮,欲善其事先利其器
    调用Web Service服务出现响应消息的内容类型text/html与绑定text/xml的内容类型不匹配
    服务器 '***' 上的 MSDTC 不可用
    Firefox中无法使用window.close()关闭窗口
  • 原文地址:https://www.cnblogs.com/mikuo/p/13909122.html
Copyright © 2011-2022 走看看