【JS】寻找欧拉回路三种实现(回溯\栈\递归)

来源:10-4 求解欧拉回路的三种算法

Potter520

2023-05-10

将老师课程提供的图,改成这样方便理解。
图片描述

  1. 根据欧拉理论,图必须是联通的,联通分量必须是1,否则找不到欧拉回路,所以需要一个CC类
class CC {
  constructor(graph) {
    this._graph = graph;
    this._cccount = 0;
    this._visited = Array(this._graph.V()).fill(-1);
    for (let v = 0; v < this._graph.V(); v++) {
      if (this._visited[v] === -1) {
        this.dfs(v);
        this._cccount++;
      }
    }
  }
  dfs(v) {
    this._visited[v] = this._cccount;
    for (const w of this._graph.adj(v)) {
      if (this._visited[w] === -1) {
        this.dfs(w);
      }
    }
  }
  cccount() {
    return this._cccount;
  }
  isConnected(v, w) {
    this._graph.validateVertex(v);
    this._graph.validateVertex(w);
    return this._visited[v] === this._visited[w];
  }
  /**
   * 获取各联通分量对应的元素列表
   */
  components() {
    let res = Array(this._cccount);
    for (let v = 0; v < this._graph.V(); v++) {
      if (!res[this._visited[v]]) {
        res[this._visited[v]] = [];
      }
      res[this._visited[v]].push(v);
    }

    return res;
  }
}
  1. 由于是采用回溯寻找路径,根据老师提供的思路,遍历时需要删除边,回溯时需要恢复对应的边。所以需要添加给graph添加两个方法,deleteEdge、addEdge
class Graph {
  constructor(filePath) {
    this._set = new Set();
    this._V = 0;
    this._E = 0;
    let content = fs.readFileSync(filePath, { encoding: "utf8" });
    let arr = content.trim().split("\r\n");
    if (arr.length <= 0) {
      throw Error("file content is empty,filepath:", filePath);
    }

    let firstRow = arr.shift();
    [this._V, this._E] = firstRow.split(" ").map((it) => Number(it));
    if (arr.length !== this._E) {
      throw Error("graph data is invalid !");
    }

    //1.初始化顶点
    this._adj = new Array(this._V);
    for (let i = 0; i < this._adj.length; i++) {
      this._adj[i] = new Set();
    }

    //2.初始化边
    for (let i = 0; i < arr.length; i++) {
      let [v, w] = arr[i].split(" ").map((it) => Number(it));
      if (!this._adj[v].has(w)) {
        this._adj[v].add(w);
        //! 说明:由于是无向图,所以需要v->w是一条边,w->v也是一条边,需要互相包含
        this._adj[w].add(v);
      }
    }
  }
  deleteEdge(v, w) {
    this.validateVertex(v);
    this.validateVertex(w);
    this._adj[v].delete(w);
    this._adj[w].delete(v);
    this._E -= 1;
  }
  addEdge(v, w) {
    this.validateVertex(v);
    this.validateVertex(w);
    this._adj[v].add(w);
    //! 说明:由于是无向图,所以需要v->w是一条边,w->v也是一条边,需要互相包含
    this._adj[w].add(v);
    this._E += 1;
  }
  toString() {
    let sb = "";
    sb += `V:${this._V},E:${this._E}\n`;
    for (let i = 0; i < this._adj.length; i++) {
      let row = [];
      for (const value of this._adj[i].values()) {
        row.push(value);
      }
      sb += `${i}: ${row.join(",")}\n`;
    }
    return sb;
  }
  V() {
    return this._V;
  }
  E() {
    return this._E;
  }
  validateVertex(v) {
    if (v < 0 || v > this._V) {
      throw new Error(`vertex ${v} is invalid !`);
    }
  }
  adj(v) {
    this.validateVertex(v);
    return this._adj[v];
  }
  degress(v) {
    return this.adj(v).size();
  }
  hasEdge(v, w) {
    this.validateVertex(v);
    this.validateVertex(w);
    return this._adj[v].has(w);
  }
}
  1. 遍历边,以前都是遍历顶点,这会来个边,这咋记录边呢?一个父节点一个父节点的相邻顶点,不就构成了一条边了嚒。

方式1:回溯法

 function findEulerLoop(graph) {
    let cc = new CC(graph);
    if (cc.cccount() != 1) {
      return [];
    }

    let E = graph.E();
    let res = [];
    let count = 0;

    function print(...args) {
      // console.log("-".repeat(count), ...args);
    }

    function dfs(v, path) {
      count++;
      path.push(v);
      //! 说明:当前的边数全部遍历完,说明找到解
      if (graph.E() == 0) {
        res = path.slice(0);
        print("find solution: ", res);
        return true;
      }

      //! 说明:需要把当前顶点的相邻顶点copy出来,如果不copy出来就会导致for循环的是循环。for里面不能边删除又添加
      let edges = [...graph.adj(v).values()];

      for (const w of edges) {
        //! 遍历操作
        graph.deleteEdge(v, w);

        print("push:", path);

        if (dfs(w, path)) {
          return true;
        }

        //! 回溯操作
        graph.addEdge(v, w);
        path.pop();

        print("pop:", path);
      }

      count--;
      return false;
    }

    dfs(0, []);

    return res;
  }

  let res = findEulerLoop(new Graph(path.resolve(__dirname, "./assets/g0.txt")));
  console.log(res.join("->"));

输出结果

0,1->1,3->3,4->4,1->1,2->2,0

方式2:利用栈实现

 function findEulerLoop(graph) {
    function hasEulerLoop(graph) {
      let cc = new CC(graph);
      if (cc.cccount() != 1) {
        return false;
      }
      for (let v = 0; v < graph.V(); v++) {
        if (graph.degree(v) % 2 != 0) {
          return false;
        }
      }
      return true;
    }

    if (!hasEulerLoop(graph)) {
      return [];
    }

    let g = graph.clone();
    let res = [];
    let stack = [];
    let curv = 0;
    stack.push(curv);

    while (stack.length != 0) {
      if (g.degree(curv) != 0) {
        let w = g.adj(curv).values().next().value;
        /**
         * ! 注意:此处容易写成如下代码,这是错误的。
         curv = w;
         stack.push(curv);
         g.deleteEdge(curv, w);
         * 
         */
        stack.push(curv);
        g.deleteEdge(curv, w);
        curv = w;
      } else {
        res.push(curv);
        curv = stack.pop();
      }
    }

    return res;
  }

  let res = findEulerLoop(new Graph(path.resolve(__dirname, "./assets/g0.txt")));
  console.log(res.join("->"));

输出结果:

0->2->1->4->3->1->0

方式3:递归实现

  function findEulerLoop(graph) {
    function hasEulerLoop(graph) {
      let cc = new CC(graph);
      if (cc.cccount() != 1) {
        return false;
      }
      for (let v = 0; v < graph.V(); v++) {
        if (graph.degree(v) % 2 != 0) {
          return false;
        }
      }
      return true;
    }

    if (!hasEulerLoop(graph)) {
      return [];
    }

    let g = graph.clone();
    let res = [];

    function dfs(v, path) {
      for (const w of graph.adj(v)) {
        if (graph.degree(w) != 0) {
          graph.deleteEdge(v, w);
          dfs(w, path);
        }
      }
      path.push(v);
    }

    dfs(0, res);

    return res;
  }

  let res = findEulerLoop(new Graph(path.resolve(__dirname, "./assets/g0.txt")));
  console.log(res.join("->"));

输出结果:

0->2->1->4->3->1->0
写回答

1回答

liuyubobobo

2023-05-13

大赞!感谢分享!继续加油!:)

0
0

玩转算法系列--图论精讲(Java版)

30+小时系统学习,bobo带你克服被图论支配的恐惧

1591 学习 · 324 问题

查看课程