LJFYYJ的博客

努力学习技术……

动态规划最短路算法总结——以【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=vj=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],对结点ij建立一条j>i的有向边,边权为a[k],求x[n1]x[0]的最大值,就是求0到n-1的最短路。

二.最短路算法详解

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

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

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

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

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

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

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

D[i,j][k1] 表示中间经过的最大结点为k1D[i,k][k1]+D[k,j][k1]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算法(松弛最小dis[i] 周围边)

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

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

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

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

d[y]=min(d[y],d[x]+w(x,y))

具体算法描述如下:

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

  • 初始化,所有顶点d[i]=0 ,vis[i]=falsed[s]=0
  • vis[i]=false的所有点中,选择d[i]最小的点(贪心选择性质),并令x=i。如果不存在,则算法结束。
  • 标记vis[i]=true,更新和x直接相邻的所有顶点y 的最短路:d[y]=min(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(logn)时间内插入或者删除数据,并且可以在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(ElogV)

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

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

具体的算法描述如下:

  • 输入:G(V,E,s,w)
  • 初始化,d[i]=infd[s]=0
  • 对于 i=1:V1
  • ​ 对每条边(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

阅读更多
个人分类: 算法
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

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

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭