No.6.2 最短路径之Dijkstra算法--通过边实现松弛


一、简介:

 Dijkstra算法:指定一个点(源点),求其到其他点的最短路径。称之为“单源最短路径”. 

一维数组 dis 记录源点到其余各点的初始距离:

1.先找到一个距离源点A最近的点B,(区别于上一节的假设:求两点之间的最短距离,必须引入另外的点),因为边最小,其他边都是正数,即使引入其他点也不能使得这两点间的距离变小!

《基于这个假设,因为所有权边都为正数时,距离已知点的最近的点是可以确定的,保证了算法后续的正确性;但是,如果存在一条负权边,那么引入这条负权边时,可能会导致之前已经引入的点的最短路径不再正确,破坏了算法的假设基础!所以该算法不能有负权边,更勿论负权回路了!》

2.再引用上节的理论,将B最为引入点,查询是否可以缩短源点A到其他点的距离,更新dis;

3.再找到第二距离远的点C,重复以上更新,直到全部节点尝试完毕。

术语:

  dis[i] 表示源点 1 到 i 的距离;

  e[i][j] 表示 i 到 j 的初始距离,对应二维数组;

  dis[i] + e[i][j] 表示源点 1 通过节点 i 到达 j 的距离;

二、完整code

int main(){
  int inf=9999999;    //根据边长和边数决定这个值的设定
  int i,j,k,h,n,m,min;   //min,h用于中间变量,临时存储 dis[] 最小值和索引下标
  int a,b,c;
  int e[100][100],dis[10];
  int book[10]={0};    //已经确定的最短路径,不需再回首

  scanf("%d %d",&n,&m);

  for(i=1;i<=n;i++) // Initialize
    for(j=1;j<=n;j++)
    {
      if(i==j) e[i][j]=0;
      else e[i][j]=inf;
    }

  for(i=1;i<=m;i++) // Input manually
  {
    scanf("%d %d %d",&a,&b,&c);
    e[a][b]=c;
  }

  for(i=1;i<=n;i++)    //从1号定点开始
  dis[i]=e[1][i];
  book[1]=1;

  for(i=1;i<=n;i++){      //虽然 i=1 已经确定了,但是为了循环的好看(代码简单),还是写上吧:每个点都要尝试
    min=inf;
    for(j=1;j<=n;j++){   //找到距离 i 最近的点,虽然有些点已经不需要再重复尝试,还是为了代码简单
      if(book[j]==0 && dis[j]         min=dis[j];   // min实现自我递归:1.min=dis[i],k=i; 2.if(book[j]==0 && dis[j]         k=j;
      }
    }
    book[k]=1;      //不管你是i,还是j,都赋值给k了,所以book[k]=1总是没错的

    for(h=1;h<=n;h++){   //确定好距离源点最近的点之后,再把所有边松弛一遍
      if(e[k][h]e[k][h]+dis[k]){   // e[k][h]         dis[h]=e[k][h]+dis[k];
      }
    }

  }

  for(i=1;i<=n;i++)
    printf("%d\t",dis[i]);

  getchar();getchar();return 0;
}

 算法的时间复杂度O(N*N + N) ~ O(N*N),结合堆和稀疏图,还可以优化算法时间复杂度约为O(M+N)logN.

 三、一张图,比较自然的表示方式是二维数组,有向、无向的都能清晰明白;但是二维数组无论是存储还是遍历,既浪费空间,也没能节省时间。对于计算机相关人士来说,要么时间换空间,要么空间换时间,现在居然出现这种两边都很low的东东,自然是不可接受的!

A.现在用数组模拟邻接表(不是指针链表):u/ v/ w/ first/ next 都是一维数组,由于作者习惯从数组的1索引开始,所以数组长度 == 所表示的东东的长度 + 1

  1.输入第 i 条边:u[i] 号顶点,v[i] 号顶点,w[i] 表示边长;

  2.first[ u[i] ] 将输入的第 i 条边(u[i], v[i], w[i])的编号 i 存储在first数组的u[i]位置,如果有多条边的起始顶点 == u[i] ,那么first 数组中,后续编号会覆盖以前的编号,以前的编号记录到next数组里面;

  next[i] :表示输入的第 i 条边的“上一条边”的编号

  所谓上一条边是指,同一个起始顶点的多条边,按照输入顺序,first中只记录最新一次输入的编号 i ,next 中记录了输入的历史编号!想象一下广度优先算法!

  =》同理,如果 first[ v[i] ],那么记录的就是同一个终止顶点的多条边!

  for(i=1;i<=m;i++){

    scanf("%d %d %d", &u[i], &v[i], &w[i]);

    next[i] = first[ u[i] ];  // u[i] 第一次出现,next 记录原则:它的上一条边 -1;非首次出现,记录的就是前一次出现的输入编号

    first[ u[i] ] =i;      // first记录原则:在u[i] 位置,记录最新出现的输入编号

B.遍历:first、next记录的都是输入的边的编号,具体数据是存储在u/ v/ w 数组中

   1.first[1] = 5:即u[5]=1, 输入的第5条边,起始顶点为1,终点v[5], 边长w[5];上一条以 1 为起始顶点的边是:next[5] =3,即u[3],v[3],w[3];此时 next 值为3 != -1,继续迭代 next[3] = 1,即u[1],v[1],w[1]... 直到 next=-1 止,退出next遍历;

  2.继续first遍历...

  k = first[1];

  while(k!=-1){

    printf("(%d %d %d)\t", u[k], v[k], w[k]);

    k=next[k];

  3.这种遍历是以first中的数据为准,即顶点个数n,而不是边数m。但是时间复杂度却是O(M),遍历所有的输入边。

  4.有没有注意到,first[3] == -1,那么遍历的时候就会跳过这一层循环,需要有自己的判断模块!

C.所以按照邻接表的方式,实现Dijkstra算法:

《写错了,下面的是Bellman-Ford算法,Dijkstra算法需要找到距离目标点最近点之后,再对其他边进行松弛,有兴趣的自己去实现,我太懒》

int main(){
  int inf=9999999;
  int i,j,k,n,m;
  int dis[10];
  int first[10],next[10];
  int u[10],v[10],w[10];

  scanf("%d %d",&n,&m);

  for(i=1;i<=n;i++) first[i]=-1;  //初始化,假设每个输入边的起始顶点都是唯一的

  for(i=1;i<=m;i++)  //邻接表的实现
  {
    scanf("%d %d %d",&u[i],&v[i],&w[i]);
    next[i]=first[u[i]];
    first[u[i]]=i;
  }

  for(i=2;i<=n;i++)  // dis array
    dis[i]=inf;
  dis[1]=0;

  // debug用的输出first/next array 

  printf("first array:");
  for(i=1;i<=n;i++){
    printf("%d\t",first[i]);
  }
  printf("\nnext array:");
  for(i=1;i<=m;i++){
    printf("%d\t",next[i]);
  }

  for(i=1;i<=n;i++){
    k=first[i];
    if(k==-1){  // 从上面的debug中,可以看出,这样的点真的是存在的
      for(j=1;j<=m;j++){
        if(dis[v[j]] > dis[u[j]]+w[j])
          dis[v[j]] = dis[u[j]]+w[j];
      }
      continue;
    }
    while(k!=-1){
      if(dis[v[k]] > dis[u[k]]+w[k]){
        dis[v[k]] = dis[u[k]]+w[k];
      }
      k=next[k];
    }
  }

  printf("\n");
  for(i=1;i<=n;i++)
    printf("%d\t",dis[i]);

  getchar();getchar();return 0;
}

相关