从分手厨房看拓扑排序

来自:Mark Lux | 责编:乐乐

链接:marklux.cn/blog/117


     

   正文   

分手厨房(Over Cooked!)是一款以高难度合作著称的游戏,在形形色色的厨房中,你需要和你的同伴一起克服重重难关,按照指定的顺序生产出美味佳肴,满足客人的味蕾。在游戏过程中,制作一道菜需要完成许多的步骤,以第一关中的寿司为例,需要蒸米饭、切鱼片、切黄瓜、然后用紫菜把他们包在一起,与此同时你还要兼顾洗掉脏盘子。不难看出,当有多个玩家参战的时候,这里有些工序是可以同时进行的(比如蒸米饭和切鱼片),但也有些工序是有顺序依赖的(比如只有一个案板,那么切鱼片和切黄瓜就不可能同时进行),那么,如何才能将所有的工序进行一个合理的排序,来保证其正常运作呢?

其实这个问题,正是一个典型的拓扑排序问题,要讲拓扑排序,我们还得先从一种基本的数据结构:**图(Graph)**说起。

图是一种由节点和边组成的数据结构,你可以简单地联想平常使用的思维导图,这就是一种非常典型的图结构。图中的边可以是有方向的,也可以是没有方向的,这两种图分别称为有向图和无向图(注意,并不是所有节点都必须连接在一起):

接下来我们需要看看如何使用图的结构来描述上面制作寿司的工序,因为不同的工序在“顺序”上有依赖,所以需要采用有向图的结构来描述:

如图,我们把游戏中制作寿司的过程用有向图的方式来描述,分别将五个步骤标记为A,B,C,D,E,这便是图的五个节点,除此之外,由于各个步骤之间存在着互相依赖,因此还需要添加四条边(A -> D),(B -> D),(C -> D),(B -> C)。

在此基础上我们需要引入一个额外概念,那就是节点的入度和出度,入度是“指向某个节点的边的数量”,出度则是“从某个节点出发的边的数量”,在上面的图中,各个节点入度和出度的情况如下图所示:

很明显,要制作一个寿司我们需要完成上面的所有5个步骤,但各个步骤实际执行的顺序很重要,比如按照A,B,C,D,E的顺序就可以顺利制作一个寿司,但是按照D,C,B,A,E的顺序就不行,因为执行包紫菜这个步骤的时候,米饭、鱼片、黄瓜都还没有准备好,就无法继续下去了。

那么,如何对这些节点进行合理的排序,得到一个可以执行的序列,这就是图论中的拓扑排序问题,用更加抽象一点的语言来描述,就是要求得一个线性序列,使得该序列中的任意两个节点u,v,如果存在边(u -> v),保证u的顺序在v之前。

关于拓扑排序有两个显而易见的结论:

  • 拓扑排序的结果不是唯一的

  • 如果要排序的有向图中存在环,那么拓扑排序是得不到结果的,所以拓扑排序只能针对有向无环图

接下来看一看如何对一张图进行拓扑排序得到线性序列S吧:

第一步:从图中找到一个入度为0的节点,将其加入序列S

第二步:从图中删除该节点,以及从该节点出发的边,当边被删除后,同步图中所有节点的入度

不断地重复第一步和第二步,直到图中所有的节点都被删除,最终得到的序列就是这张图的一个拓扑排序了。

这个过程其实也非常的容易理解,仍然以寿司的制作为例,来看看整个拓扑排序是如何进行的:

首先选中一个入度为0的节点A,然后删除节点A。此时D的入度更新为2

选中入度为0的节点B,然后删除节点B。此时D的入度更新为1,C的入度更新为0

选中入度为0的节点C,然后删除节点C。此时D的入度更新为0

选中入度为0的节点D,然后删除节点D。

选中入度为0的节点E,然后删除节点E。

得到一个拓扑排序结果(A,B,C,D,E)


Java代码实现

顶点的结构定义

public class Vertex<T> {


    /**
     * 节点值
     */
    private T value;


    /**
     * 节点入度
     */
    private int inDegree;


    /**
     * 储存从该定点出发的边
     */
    private List<Edge<T>> edges;
}

边的定义

public class Edge<T> {


    /**
     * 终点边
     */
    private Vertex<T> endVertex;


    /**
     * 权重
     */
    private int cost;
}

图的定义

public class DirectedGraph<T> {


    private List<Vertex<T>> vertexList;


    private List<Edge<T>> edgeList;
}


拓扑排序实现

/**
 * 拓扑排序
 */
public List<T> topologySort() throws Exception {


  int cnt = 0;


  List<T> sortedList = new ArrayList<>();


  Queue<Vertex<T>> queue = new LinkedList<>();
  // 将所有入度为0的节点入队
  for (Vertex<T> vertex: vertexList) {
    if (vertex.getInDegree() == 0) {
      queue.offer(vertex);
    }
  }


  // 如果没有入度为0的节点,说明出现循环依赖
  if (queue.isEmpty()) {
    throw new Exception("detected circle, no zero indegree node.");
  }


  while (!queue.isEmpty()) {
    Vertex<T> v = queue.poll();
    // 排序
    sortedList.add(v.getValue());
    cnt++;
    for (Edge<T> edge: v.getEdges()) {
      // 更新所有关联顶点的入度
      Vertex<T> endVertex = edge.getEndVertex();
      if (endVertex != null) {
        endVertex.setInDegree(endVertex.getInDegree() - 1);
        if (endVertex.getInDegree() == 0) {
          queue.offer(endVertex);
        }
      }
    }
  }


  if (cnt != vertexList.size()) {
    // 如果拓扑排序结束后数量不匹配,说明有环
    throw new Exception("detected circle!");
  }


  return sortedList;
}
微信群仅供关注公众号「程序IT圈」 的读者加入!进群技术交流与日常交流讨论,技术内容不限于Java、Python、算法学习、求职内推、资料分享等等 。当然日常吹水也是可以的,但是禁止随意开车 。严禁一切推广链接、二维码,一经发现送上飞机票????。
加群方式:扫描二维码,备注【加群】,微信号:it4201 。
同意后,手动拉你进群~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值