数据结构与算法--浅析图

​ 图的元素叫作顶点,顶点间的连接关系叫做,跟顶点相连的边的条数称为顶点的度

​ 图根据是否有方向可分为有向图无向图,有向图的边有方向,度也分为入度(指向顶点的边的个数)和出度(顶点指向的边的个数)

​ 有权重的图称为带权图,也就是边有权值

​ 用图展示下(左上无权无向图、右上无权有向图、左下有权无向图、右下有权有向图):

存储方式

​ 图的存储方式有两种,一种是邻接矩阵,另一种是邻接表

邻接矩阵实质是n×n的二维数组m[i][j]在无权无向图中代表是否连接,m[i][j]在无权有向图中代表i是否单向连接j,m[i][j]在有权无向图中代表是否i和j的连接权重,m[i][j]在有权有向图中代表i单向连接j的权重

​ 优点是存储方式简单,查询高效,缺点是当点多边少时,邻接矩阵易造成内存空间浪费,以有向有权图为例:

​ 邻接表实质就是一组动态数组(或链表、红黑树、跳表、散列表等),优点是内存占用少,缺点就是查询相比邻接矩阵低(有序动态数组可以二分查找,红黑树、跳表、散列表相比链表也快很多),以有向有权图用邻接表(链表形式)为例:

拓扑排序

​ 让有向无环图中所有顶点排成一个线性序列,使图中任意一对顶点u和v,若点u到v连通,满足在线性序列中u位于v前。有两种实现方法,分别是Kahn算法DFS

​ 它可用于检测图中是否存在环、 还可以通过局部顺序来推导全局顺序(比如用于处理编译文件的依赖项)

Kahn算法

​ 实质上是贪心算法。如果某个顶点入度为 0 ,就表示没有任何顶点必须先于这个顶点执行,那么这个顶点就可以执行了。

​ 因此我们先从图中找出一个入度为 0 的顶点并输出,然后把这个顶点从图中删除(把这个顶点可达的顶点的入度都减 1)。循环执行上面的过程直到所有的顶点都被输出

伪代码:

void Kahn() {
  for (int i = 0; i < v; ++i) {
    for (int j = 0; j < vec[i].size(); ++j) {
      int w = vec[i][j]; //说明存在i->w
      num[w]++;			//统计节点的入度
    }
  }
  //先把入度为0的节点加入队列
  for (int i = 0; i < v; ++i) {
    if (num[i] == 0) queue.push(i);
  }
  //循环执行:输出入度为0的节点,并把该节点指向的节点入度--,若指向节点入度变为0则加入队列
  while (!queue.isEmpty()) {
    int i = queue.pop();
    printf("->%d", i);
    for (int j = 0; j < vec[i].size(); ++j) {
      int k = vec[i][j];
      num[k]--;
      if (num[k] == 0) queue.push(k);
    }
  }
}

DFS算法

​ 先通过邻接表构造逆邻接表,再递归处理每个顶点(对于一个顶点先输出可达它的所有顶点,再输出自己)

伪代码:

void Topology() {
  for (int i = 0; i < v; ++i) { // 通过邻接表生成逆邻接表
    for (int j = 0; j < vec[i].size(); ++j) {
      int w = vec[i][j]; // i->w
      vecRotate[w].push(i); // w->i
    }
  }
  for (int i = 0; i < v; ++i) { // 深度优先遍历图
    if (visited[i] == false) {	// 未标记走过的进行dfs
      visited[i] = true;	
      dfs(i);
    }
  }
}

void dfs(int v) {
  for (int i = 0; i < vecRotate[v].size(); ++i) {
    int w = vecRotate[v][i];
    if (visited[w] == true) continue;	//若指向v的节点w已经标记过就跳过
    visited[w] = true;					//否则就标记先dfs输出w
    dfs(w);
  }
  //先输出可达它的所有顶点,再输出自己
  printf("->%d", v);
}

单源最短路

​ 用于求一个顶点到一个顶点的最短距离,最常见的算法为dijkstra算法,时间复杂度为O(E × logV)E为边数,V为顶点个数,具体做法是:

​ 用 v 数组记录起点到每个点的距离(初始化无穷大),然后把起始顶点初始化为 0 并放入优先级队列

​ 每次从优先级队列取出距离最小的顶点p,若p可直达q,且v[p]+p到q距离 < v[q],就更新v[q]=v[p]+p到q距离,并把q加入到优先级队列,重复该过程直到找到终止或队空

​ 除此之外,predecessor数组记录每个顶点的前驱,用于还原最短路径inqueue数组判定顶点是否已经在优先队列中避免重复添加m数组记录相邻节点+距离

​ 具体流程图:

伪代码

void dijkstra(int s, int t) { 	//获取s->t的最短路径
  //step1:先初始化s到所有节点的距离无穷大
  for (int i = 1; i <= n; i++) {
      v[i].id = i;
      v[i].dist = INF;
  }
  //step2:再将到s节点(它本身)的距离初始化为0,并加入到优先队列
  v[s].dist = 0;
  queue.push(v[s]);
  inqueue[s] = true;
  //循环执行
  while (!queue.isEmpty()) {
    minVertex = queue.pop(); //从优先级队列取出距离最小的顶点minVertex
    if (minVertex.id == t) break; 	// 找到终点直接跳出
    //查找所有与距离最小节点相连的顶点
    for (int i = 0; i < m[minVertex.id].size(); ++i) {
      int tempDist = m[minVertex.id][i].dist; 
      int e = m[minVertex.id][i].id;
      //若minVertex可直达e,且v[minVertex]+minVertex到e距离<v[e]
      if (minVertex.dist + tempDist < v[e].dist) {
        //就更新v[e]=v[minVertex]+minVertex到e距离
        v[e].dist = minVertex.dist + tempDist;
        predecessor[v[e].id] = minVertex.id;
        //并把e加入到优先级队列(若e已经在优先队列中,则直接修改)
        if (inqueue[v[e].id] == true) {
          queue.update(v[e].dist);      // 更新队列中的 dist 值
        } else {
          queue.push(v[e]);
          inqueue[v[e].id] = true;
        }
      }
    }
  }
  // 输出最短路径
  printf(s);
  print(s, t);
}
//根据前驱递归输出路径
void print(int s, int t) {
  if (s == t) return;
  print(s, predecessor[t]);
  printf("->%d", t);
}

应用场景

​ 除了应用于地图求最短路外,还可应用于求前k优组合,例如翻译一个句子,句中有3个单词,每个单词都有 一组可选的翻译列表并针对每个翻译打分,我们从每个单词的翻译列表中选择一个,组合起来就能得到句子的整体翻译,求前k高分的翻译结果:

​ 对此,我们可以把每个单词可选翻译列表按高到低排序,然后将最高分a0b0c0放入优先队列。

​ 每次从优先级队列取出得分最高的组合,并基于这个组合进行扩展(扩展策略是每个单词的翻译分别替换成下一个单词的翻译),重复这个过程直到取出k个为止,流程图为:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值