动态规划最短路算法总结——以【Layout】差分约束为例

动态规划最短路算法总结

——以【Layout】差分约束为例

一.差分约束的概念

如果一个系统由 n 个变量和 m 个不等式组成,并且这 m 个不等式对应的系数矩阵每一行有且仅有一个1和-1,其它的都为0,这样的系统成为差分约束系统。

将不等式 x [ i ] x [ j ] a [ k ] 变形为 x [ i ] x [ j ] + a [ k ] ,再令 a [ k ] = w ( j , i ) ,令 i = v j = u ,再将数组名改为 d ,不等式即可变形为: d [ u ] + w ( u , v ) d [ v ] 。这就使人联想到SPFA中的一个松弛操作:

if(d[u] + w(u,v) <= d[v])
  d[v] = d[u] + w(u,v);

虽然两个式子符号不同,但想想看,差分约束受到多个值的约束,这些值里面的最小值就是差分约束能取到的最大数值,而这也正是最短路的结果。所以:

对于每个不等式 x [ i ] x [ j ] a [ k ] ,对结点 i j 建立一条 j > i 的有向边,边权为 a [ k ] ,求 x [ n 1 ] x [ 0 ] 的最大值,就是求0到n-1的最短路。

二.最短路算法详解

最短路可分为全点对最短路,代表算法Floyd-Warshall单源最短路,代表算法Dijkstra和Bell-Ford

1.全点对最短路及Floyd-Warshall算法

全点对最短路的决策量为 D [ i , j ] ,表示 i j 之间的最短距离

​ 子结构为 [ 1 : k ] ,表示经过的节点数

D [ i , j ] [ k ] 表示从 V [ i ] V [ j ] ,中间只经历的最大结点不超过K

其最优子结构性质可以描述为:
D [ i , j ] [ k ] = w ( i , j ) i = j

​ min{ D [ i , j ] [ k 1 ] D [ i , k ] [ k 1 ] + D [ k , j ] [ k 1 ] } i j

D [ i , j ] [ k 1 ] 表示中间经过的最大结点为 k 1 D [ i , k ] [ k 1 ] + D [ k , j ] [ k 1 ] k 作为源点所以一定经过 k

代码实现:

struct node{
  int vertex[maxn];//存储顶点数
  int edges[maxn][maxn];//邻接矩阵
  int n,e;//顶点数和边数
}MGraph;
void floyd(MGraph g)
{
  int A[maxn][maxn];
  int path[maxn][maxn];
  int k,i,j;
  for(i=0;i<n;i++){
    for(j=0;j<n;j++){
      A[i][j]=g.edges[i][j];
      path[i][j]=i;
    }
  }
  for(k=0;k<n;k++){
    for(i=0;i<n;i++){
      for(j=0;j<n;j++){
        if(A[i][j]>(A[i][k]+A[k][j])){//关键步骤
          A[i][j]=A[i][k]+A[k][j];
          path[i][j]=k;
        }
      }
    }
  }
}
2.图的存储方式
  • 邻接矩阵,优点是实现简单,缺点是容易造成空间浪费,当点数过多时,无法实现矩阵。
  • 邻接链表,优点是不会有空间浪费,缺点是实现相对麻烦
  • 前向星,存入(起点,终点,边长)三元组,并对起点进行排序。优点是实现简单,容易理解,缺点是需要读入所有边后,对边进行一次排序。时间开销大,实用性差。
  • 链式前向星,存入(起点,终点,边长,下一条边)四元组,用head[i]数组来记录边。
struct node{
  int u, v, w;
  int next;
}A[10000];
void init()
{
  node = 0;
  memset(head, -1, sizeof(head));
}
void add(int u, int v, int w)
{
  A[node].u = u;
  A[node].v = v;
  A[node].w = w;
  A[node].next = head[u]; //指向同一个结点的前一条边
  head[u] = node++;       //把这次的边放进这个数组中去,head里存放的是起点为u的最后输入的
                          //的边对应的结点,可以通过这个结点的next,求得之前边的结点
}
单源最短路:
3.Dijkstra算法(松弛最小 d i s [ i ] 周围边)

对于正权图,在可达的情况下最短路一定存在,最长路不一定存在。最短路具有最优子结构性质,所以是动态规划问题,其最优子结构的性质可以描述为:

D ( s , t ) = V s V i V j V t ) 表示从 s t 的最短路,其中 i j 是这条路径上的两个中间节点,那么 D ( i , j ) 一定是 i j 的最短路。

Dijkstra算法是最经典的最短路算法,用于计算正权图的单源最短路。它是基于这样的一个事实:

如果源点到 x 点的最短路已经求出,并且保存在 d [ x ] 中,则可以利用 x 去更新 x 能够直接到达的点的最短路。 即:

d [ y ] = m i n ( d [ y ] , d [ x ] + w ( x , y ) )

具体算法描述如下:

输入: G ( V , E , s , w ) ,源点为 s d [ i ] 表示 s i 的最短路, v i s [ i ] 表示 d [ i ] 是否已经确定(布尔值)。

  • 初始化,所有顶点 d [ i ] = 0 , v i s [ i ] = f a l s e d [ s ] = 0
  • v i s [ i ] = f a l s e 的所有点中,选择 d [ i ] 最小的点(贪心选择性质),并令 x = i 。如果不存在,则算法结束。
  • 标记 v i s [ i ] = t r u e ,更新和 x 直接相邻的所有顶点 y 的最短路: d [ y ] = m i n ( d [ y ] , d [ x ] + w ( x , y ) )

时间复杂度:

当Q非空

找到最小的 d [ i ] O ( | V | 2 )

​ 枚举法找到与 i 相连的 j O ( | E | )

​ 松弛 d [ j ]

Q非空,使整个循环进行|V|次,每次找出最小的 d [ i ] ,又要进行|V|次。对于所有的节点,枚举法枚举与 i 相连的 j 进行松弛需要2*|E|次。所以总的时间复杂度为 O ( | V | 2 + | E | )

//使用链式前向星
queue<int>Q;
memset(vis,false,sizeof(vis));
memset(dis,inf,sizeof(dis));
dis[s] = 0; vis[s] = true;
while(true){//如果队列不为空
  int min = inf; int i;
  int j = 0;
  for(i = 1; i <= n; i++){//选出最小的dis[i]
    if(vis[i] == false && min > dis[i]){
      min = dis[i];
      j = i;
    }
  }
  if(j == 0) break;//如果所有顶点都已经成为最小值了,算法结束
  vis[j] = true;//标记vis
  for(int k = head[j]; k != -1;k = A[k].next){//松弛j周围的每一条边
    v = A[k].v;
    if(dis[u] + A[k].w < dis[v]){
      dis[v] = dis[u] + A[k].w;
    }
  }
}
4.Dijkstra算法 + 优先队列(小顶堆)

堆是用数组表示二叉树,小顶堆(大根堆)是指每个数都大于等于自己的父结点。可在 O ( l o g n ) 时间内插入或者删除数据,并且可以在 O ( 1 ) 时间内得到当前数据最小值。

算法思路:

priority_queue <int,vector<int>,greater<int> > p;
void Dijkstra_Heap(s){
  memset(dis, inf, sizeof(dis));
  dis[s] = 0;
  q.push(s);//在队列中放入源点
  while(!q.empty()){
    u = q.top();
    q.pop();
    for(int k = head[u]; e != inf; k = A[k].next){//对于每一个结点找其相邻的边
      v = A[k].v;
      if(dis[u] + A[k].w < dis[v]){
        dis[v] = dis[u] + A[k].w;//进行松弛
        q.push(v);//松弛完了放进去
  }
}

使用两者结合的算法,时间复杂度为:

堆中的元素共有O(V)个,取出并更新O(E)次,所以时间复杂度为 O ( E l o g V )

5.Bellman-Ford算法(处理负权路)

Bellman-Ford算法可以在最短路存在的情况下求出最短路,并且存在负权圈的情况下判断出最短路不存在。它基于这样一个事实:一个图的最短路如果存在,那么最短路中必定不存在圈,所以最短路的顶点数除起点外只有n-1个

具体的算法描述如下:

  • 输入: G ( V , E , s , w )
  • 初始化, d [ i ] = i n f d [ s ] = 0
  • 对于 i = 1 : V 1
  • ​ 对每条边 ( u , v ) 进行松弛: 若 d [ u ] + w ( u , v ) < d [ v ] d [ v ] = d [ u ] + w ( u , v )
  • 再对于每条边判断,若 d [ v ] > d [ u ] + w ( u , v ) 则存在负权圈
  • ​ 否则不存在

理解:如果没有负权圈,那么V-1遍已经完成松弛。如果有负权圈,松弛无法停止。

6.SPFA算法(松弛所有起始点相邻边,并计算一个顶点周围边的松弛次数)

SPFA(Shortest Path Faster Algorithm)是基于Bellman-Ford的思想,采用先进先出队列进行优化的一个计算但源最短路的快速算法。一般用来解决带负权圈的最短路问题。

算法思想:

​ 建立一个队列,初始时队列只有一个起始点即源点,然后松弛与起始点相邻的边,并则将边的终点放入队列最后作为起始点。重复执行直到队列为空。

​ 判断有无负环:如果一个顶点加入队列的次数超过n次则说明有负环存在。

​ 具体算法代码可参考下文Layout的代码分析

三.Layout分析及代码

1.题意分析:

编号1到n的牛排队,有些牛比较友好,他们相隔的距离有最大值。有些牛很不友好,他们的相隔距离有最小值。可以看出题目中给出了差分约束限制,对于有最大值的情况,将其作为正权路,对于有最小值的情况,将其作为负权路,最终目的即求解最短路。

因为有负权有正权,所以采用SPFA算法。

2.代码分析:
#include<cstdio>
#include<cstdlib>
#include<queue>
#define max 1000000
using namespace std;
struct node{
  int u,v,w;
  int next
}A[maxn];//建立链式前向星
int cnt[maxn];//用于判断是否有负权圈
int dis[maxn];//用于计算最短距离
bool vis[maxn];//用于判断是否被放入过队列中!
int head[maxn];//存储起点为u的结点最大值
int node;
void init()
{
  node = 0;
  memset(head, -1, sizeof(head));
  memset(vis, false, sizeof(vis));
  memset(dis, max, sizeof(dis));
  memset(cnt, 0, sizeof(cnt));
}
void add(int u,int v,int w)
{
  A[node].u = u;
  A[node].v = v;
  A[node].w = w;
  A[node].next = head[u];
  head[u] = node++;
}
int spfa(int n)
{
  int u = 1;
  queue<int>Q;
  dis[u] = 0; vis[u] = true;
  Q.push(u); cnt[u] = 1;
  while(!Q.empty()){
    u = Q.front();//按照先进先出的顺序对队列中结点周围的边进行松弛
    Q.pop();
    vis[u] = false;
    for(int k = head[u]; k != -1; k = A[k].next){
      int v = A[k].v;
      if(d[u] + A[k].w < d[v]){//如果发生了松弛
        d[v] = d[u] + A[k].w;
        if(!vis[v]){//如果v不在队列里,如果v在队列里就不把它再放进去一次了
           cnt[v]++;
           vis[v]  = true;
           Q.push(v);
        }        
      }
    }
  }
  return dis[n];
}
int main()
{
  int n,ml,md;
  int i;
  while(scanf("%d%d%d",&n,&ml,&md)!=EOF){
    int a,b,c;
    init();
    for(i = 0; i < ml; i++){
      scanf("%d%d%d",&a,&b,&c);
      add(a,b,c);
    }
    for(i = 0;i < md; i++){
      scanf("%d%d%d",&a,&b,&c);
      add(b,a,-c);//负权
    }
    int ans = spfa(n);
    if(ans == inf){//没有更新,则可以取任意远,没有约束
      printf("%d\n", -2);
    }
    else if(ans == -1){
      printf("%d\n", -1);
    }
    else
      printf("%d\n",ans);
  }
  return 0;
}

代码参考链接:https://blog.csdn.net/r1986799047/article/details/50444805

算法分析参考链接:http://www.cppblog.com/menjitianya/archive/2015/11/19/212292.html

https://blog.csdn.net/scythe666/article/details/50938123

猜你喜欢

转载自blog.csdn.net/ljfyyj/article/details/79946796