搜索与图论之欧拉回路与欧拉路径

搜索与图论之欧拉回路与欧拉路径

前置背景

AOV&AOE

A O V AOV AOV网,顶点表示活动,弧表示活动间的优先关系的有向图。 即如果a->b,那么a是b的先决条件。

A O E AOE AOE网,边表示活动,是一个带权的有向无环图, 其中顶点表示事件,弧表示活动,权表示活动持续时间。

求拓扑序列就是 A O V AOV AOV,求关键路径就是 A O E AOE AOE

入度

入度(indegree)就是有向图中指向这个点的边的数量,即有向图的某个顶点作为终点的次数和

出度

出度(outdegree)就是从这个点出去的边的数量,即有向图的某个顶点作为起点的次数和

定义
  • 欧拉回路(Eulerian Circuit从图上一个点u出发不重复地经过每一条边后,再次回到点u的一条路径。
  • 欧拉路径(Eulerian Path:从图上一个点u出发不重复地经过每一条边的一条路径(不必回到点u)。
  • 欧拉图即存在欧拉回路的图,半欧拉图即存在欧拉路径的图
  • 欧拉迹/欧拉通路/一笔画:通过图中每条边且行遍所有顶点的迹(每条边恰一次的途径),称为欧拉迹(Euler trail)
  • 半欧拉图:具有欧拉通路但不具有欧拉回路的无向图称为半欧拉图,有且仅有两个度数为奇数的结点
  • 环游:图的环游(tour)是指经过图的每条边至少一次的闭途径
  • 欧拉环游/回路:经过每条边恰好一次的环游/回路欧拉环游/回路(Eular tour)
  • 欧拉图:一个图若包含欧拉环游,则称为欧拉图(Euleriangraph)
  • 欧拉定理:一个非空连通图是欧拉图当且仅当它的每个顶点的度数都是偶数

就像是一笔画,要求每条边只走一次,但每个点可以多次经过,而要求每个点只走一次的模型是**哈密顿环。**注意欧拉回路必须回到起点,欧拉路径则不必,可以说欧拉回路一定是欧拉路径,反之不成立

欧拉回路欧拉路径
无向图每个节点都有偶数的,–>所有非0度的节点都是连通的,不用考虑0度的节点,因为不属于欧拉回路或欧拉路径每个节点都有偶数的度或者只有两个节点有奇数的(这个两个奇数度的节点其实是起点和终点)–>所有非0度的节点都是连通的,不用考虑0度的节点,因为不属于欧拉回路或欧拉路径
有向图每个节点都有相同的入度和出度最多只有一个顶点的入度-出度=1并且最多只有一个顶点的出度-入度=1,其他节点的出度与入度相等
其他结论
  • 无向图为(半)欧拉图时,只需用1笔画成;无向图为非(半)欧拉图时,即奇点(度为奇数的点)数k>2,需用k/2笔画成。
  • 可以用加边的方式把一个非欧拉图变成欧拉图。对于无向图来说,每个奇点都需加一个度,加的边为 奇点数/2 ;对于有向图来说,每个点都需加上入度与出度之差,加的边数为每个点入度与出度之差的绝对值之和再除以2。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

无向图中的欧路径和欧拉回路

判断无向图是否有欧路径和欧拉回路

  • 在欧拉路径中,每次访问一个顶点V,访问以该顶点V为端点的两条未访问过的边,在欧拉路径中的所有中间点都有偶数的度
  • 在欧拉回路中,任何顶点都可以是中间顶点,因此所有的顶点都必须具有偶数度
public class EulerianGraphOne {
   //判断给定的图是否是欧拉图或者半欧拉图
    static class Graph {
        private int V; //顶点的个数
		private LinkedList<Integer> adj[];//无向图的邻接矩阵表示

        Graph(int v) {//初始化话图
            V = v;
            adj = new LinkedList[v];
            for (int i = 0; i < v; ++i) adj[i] = new LinkedList();
        }
      
        void addEdge(int v, int w) {//向图中添加边,是无向图
            adj[v].add(w);
            adj[w].add(v); 
        }

       
        void DFSUtil(int v, boolean visited[]) {
            visited[v] = true;//标记当前的顶点v为访问过的顶点
            Iterator<Integer> i = adj[v].listIterator();//递归遍历当前顶点的邻居顶点
            while (i.hasNext()) {
                int n = i.next();
                if (!visited[n])//如果被访问,该顶点跳过
                    DFSUtil(n, visited);
            }
        }

        //判断非0度的顶点是否是连通的
        boolean isConnected() {
            boolean visited[] = new boolean[V];//初始化visited数组
            int i;
            for (i = 0; i < V; i++)//找到一个非0度的顶点
                if (adj[i].size() != 0) break;
            //如果图中没有边,返回true
            if (i == V) return true;
            // 从非0度的顶点开始dfs
            DFSUtil(i, visited);
            // 如果非0度的顶点,没有被访问,说明该顶点是孤立的联通分量,图不相连
            for (i = 0; i < V; i++)
                if (!visited[i] && adj[i].size() > 0)
                    return false;
            return true;
        }

        /* 
        0 --> 该无向图不是欧拉图或者半欧拉图
        1 --> 该无向图是半欧拉图即欧拉路径
        2 --> 该无向图是欧拉回路 */
        int isEulerian() {
            // 判断非0度的顶点是否相连
            if (!isConnected())  return 0;
            // 计数,统计有奇数度的顶点的数量
            int odd = 0;
            for (int i = 0; i < V; i++)
                if (adj[i].size() % 2 != 0)
                    odd++;
            // 如果奇数度的数量超过2,该无向图不是一个欧拉图或者半欧拉图
            if (odd > 2) return 0;
            // 有奇数度的顶点的数量是2:半欧拉图(欧拉路径)
            // 有奇数度的顶点的数量是0:欧拉图(欧拉回路)
            //注意无向图中的奇数点个数不会为1
            return (odd == 2) ? 1 : 2;
        }

        // 测试
        void test() {
            int res = isEulerian();
            if (res == 0)
                System.out.println("graph is not Eulerian");
            else if (res == 1)
                System.out.println("graph has a Euler path");
            else
                System.out.println("graph has a Euler cycle");
        }

       
        public static void main(String args[]) {
            Graph g1 = new Graph(5);
            g1.addEdge(1, 0);
            g1.addEdge(0, 2);
            g1.addEdge(2, 1);
            g1.addEdge(0, 3);
            g1.addEdge(3, 4);
            g1.test();

            Graph g2 = new Graph(5);
            g2.addEdge(1, 0);
            g2.addEdge(0, 2);
            g2.addEdge(2, 1);
            g2.addEdge(0, 3);
            g2.addEdge(3, 4);
            g2.addEdge(4, 0);
            g2.test();

            Graph g3 = new Graph(5);
            g3.addEdge(1, 0);
            g3.addEdge(0, 2);
            g3.addEdge(2, 1);
            g3.addEdge(0, 3);
            g3.addEdge(3, 4);
            g3.addEdge(1, 3);
            g3.test();

            Graph g4 = new Graph(3);
            g4.addEdge(0, 1);
            g4.addEdge(1, 2);
            g4.addEdge(2, 0);
            g4.test();

            // 顶点的具有0度的无向图
            Graph g5 = new Graph(3);
            g5.test();
        }
    }
}

打印结果

graph has a Euler path
graph has a Euler cycle
graph is not Eulerian
graph has a Euler cycle
graph has a Euler cycle

有向图中的欧拉回路

Lightbox

上图有一条欧拉路径:{1, 0, 3, 4, 0, 2, 1}

如何判断一个有向图是欧拉图?

  • 有向图满足以下条件可以断定其有欧拉回路

    • 所有的非0度的顶点属于单一的强联通分量
    • 每个顶点的入度=出度
public class EulerianGraphTwo {
    //判断给定的有向图是否是欧拉图
    static class Graph {
        private int V; // 顶点数量
        private LinkedList<Integer> adj[];//邻接矩阵
        private int in[];         //入度数组

        //初始化
        Graph(int v) {
            V = v;
            adj = new LinkedList[v];
            in = new int[V];
            for (int i = 0; i < v; ++i) {
                adj[i] = new LinkedList();
                in[i] = 0;
            }
        }

        //添加边,并对w这个端点的入度+1
        void addEdge(int v, int w) {
            adj[v].add(w);
            in[w]++;
        }

        // 从v开始dfs遍历
        void DFSUtil(int v, Boolean visited[]) {
            visited[v] = true;//设置当前顶点v为访问过的顶点
            int n;
            // 递归遍历当前顶点v的邻居顶点
            Iterator<Integer> i = adj[v].iterator();
            while (i.hasNext()) {
                n = i.next();
                if (!visited[n])
                    DFSUtil(n, visited);
            }
        }

        // 获取该有向图的镜像图
        Graph getTranspose() {
            Graph g = new Graph(V);
            for (int v = 0; v < V; v++) {
                //递归当前顶点的邻居顶点
                Iterator<Integer> i = adj[v].listIterator();
                while (i.hasNext()) {
                    g.adj[i.next()].add(v);
                    (g.in[v])++;
                }
            }
            return g;
        }

        // 判断当前的有向图是否具有强联通分量
        Boolean isSC() {
            // Step 1:标记所有的顶点为未访问状态
            Boolean visited[] = new Boolean[V];
            for (int i = 0; i < V; i++)
                visited[i] = false;
            // Step 2:从第1个顶点开始做dfs
            DFSUtil(0, visited);

            // 如果dfs遍历结束,没有访问到所有的顶点,返回false
            for (int i = 0; i < V; i++)
                if (!visited[i])
                    return false;

            // Step 3: 创建该有向图的镜像图
            Graph gr = getTranspose();

            // Step 4:标记所有的顶点为未访问状态
            for (int i = 0; i < V; i++)
                visited[i] = false;

            // Step 5: 从第1个顶点对该镜像图开始做dfs,注意开始的顶点应该和源有向图相同
            gr.DFSUtil(0, visited);

            // 如果dfs遍历结束,没有访问到所有的顶点,返回false
            for (int i = 0; i < V; i++)
                if (!visited[i])
                    return false;
            return true;
        }

        /*如果是该有向图有欧拉回路,返回T,反之返回F*/
        Boolean isEulerianCycle() {
            //检验所有的非0度顶点是否联通
            if (!isSC()) return false;
            //所有顶点的入度=出度
            for (int i = 0; i < V; i++)
                if (adj[i].size() != in[i])
                    return false;
            return true;
        }

        public static void main(String[] args) {
            Graph g = new Graph(5);
            g.addEdge(1, 0);
            g.addEdge(0, 2);
            g.addEdge(2, 1);
            g.addEdge(0, 3);
            g.addEdge(3, 4);
            g.addEdge(4, 0);
            if (g.isEulerianCycle())
                System.out.println("Given directed graph is eulerian ");
            else
                System.out.println("Given directed graph is NOT eulerian ");
        }
    }

}

打印结果

Given directed graph is eulerian

332. 重新安排行程

方法1:DFS朴素版
public List<String> findItinerary(List<List<String>> tickets) {
    List<String> res = new ArrayList<>();
    Map<String, List<String>> graph = new HashMap<>();
    for (List<String> t : tickets) {
        String u = t.get(0), v = t.get(1);
        graph.putIfAbsent(u, new ArrayList<>());
        graph.get(u).add(v);
    }
    for (List<String> values : graph.values()) Collections.sort(values);
    dfs(graph, res, "JFK");
    return res;
}


private void dfs(Map<String, List<String>> graph, List<String> res, String u) {
    List<String> nexts = graph.get(u);
    while (nexts != null && nexts.size() > 0) {
        String v = nexts.remove(0);
        dfs(graph, res, v);
    }
    res.add(0, u);
}
方法2:DFS+优先队列(Hierholzer算法)
  • PriorityQueue已经默认是最小字典序,免去了排序的操作
public List<String> findItinerary(List<List<String>> tickets) {
    Map<String, PriorityQueue<String>> graph = new HashMap<>();
    for (List<String> t : tickets) {
        String u = t.get(0), v = t.get(1);
        graph.putIfAbsent(u, new PriorityQueue<>());
        graph.get(u).offer(v);
    }
    Stack<String> stack = new Stack<>();
    dfs(graph, stack, "JFK");
    List<String> res = new ArrayList<>();
    while (!stack.isEmpty()) res.add(stack.pop());
    return res;
}

private void dfs(Map<String, PriorityQueue<String>> graph, Stack<String> stack, String u) {
    PriorityQueue<String> nexts = graph.get(u);
    while (nexts != null && nexts.size() > 0) {
        String v = nexts.poll();
        dfs(graph, stack, v);
    }
    stack.push(u);
}
方法3:Fleury算法
Map<String, List<String>> graph = new HashMap<>();
        List<String> res = new ArrayList<>();

        public List<String> findItinerary(List<List<String>> tickets) {
            for (List<String> t : tickets) {
                String u = t.get(0), v = t.get(1);
                graph.putIfAbsent(u, new ArrayList<>());
                graph.get(u).add(v);

            }
            for (List<String> values : graph.values()) Collections.sort(values);
            String u = "JFK";
            res.add(u);
            fleuryProcess(u);
            return res;

        }


        private void fleuryProcess(String u) {
            if (graph.containsKey(u)) {
                for (int i = 0; i < graph.get(u).size(); i++) {
                    String v = graph.get(u).get(i);
                    if (isValidNextEdge(u, v)) {
                        res.add(v);
                        graph.get(u).remove(v);
                        fleuryProcess(v);
                    }
                }
            }

        }


        private boolean isValidNextEdge(String u, String v) {
            if (graph.get(u).size() == 1) return true;
//            boolean[] visited = new boolean[graph.get(u).size()];
            Map<String, Boolean> visited = new HashMap<>();
            int count1 = dfs(u, visited);
            graph.get(u).remove(v);
            visited = new HashMap<>();
            int count2 = dfs(v, visited);
            graph.get(u).add(0, v);
            return count1 <= count2;
        }

        private int dfs(String u, Map<String, Boolean> visited) {
            visited.put(u, true);
            int count = 1;
            System.out.printf("%s\n", u);
            if (graph.containsKey(u)) {
                for (String adj : graph.get(u)) {
                    if (visited.get(adj) == null || (visited.get(adj) != null && !visited.get(adj))) {
                        count += dfs(adj, visited);
                    }
                }
            }
            return count;
        }

753. 破解保险箱

预置知识

NP完全问题

NP完全问题(NP-C问题),是世界七大数学难题之一。 NP的英文全称是Non-deterministic Polynomial的问题,即多项式复杂程度的非确定性问题。简单的写法是 NP=P?,问题就在这个问号上,到底是NP等于P,还是NP不等于

P类问题:所有可以在多项式时间内求解的判定问题构成P类问题。判定问题:判断是否有一种能够解决某一类问题的能行算法的研究课题。

NP类问题:所有的非确定性多项式时间可解的判定问题构成NP类问题。非确定性算法:非确定性算法将问题分解成猜测和验证两个阶段。算法的猜测阶段是非确定性的,算法的验证阶段是确定性的,它验证猜测阶段给出解的正确性。设算法A是解一个判定问题Q的非确定性算法,如果A的验证阶段能在多项式时间内完成,则称A是一个多项式时间非确定性算法。有些计算问题是确定性的,例如加减乘除,只要按照公式推导,按部就班一步步来,就可以得到结果。但是,有些问题是无法按部就班直接地计算出来。比如,找大质数的问题。有没有一个公式能推出下一个质数是多少呢?这种问题的答案,是无法直接计算得到的,只能通过间接的“猜算”来得到结果。这也就是非确定性问题。而这些问题的通常有个算法,它不能直接告诉你答案是什么,但可以告诉你,某个可能的结果是正确的答案还是错误的。这个可以告诉你“猜算”的答案正确与否的算法,假如可以在多项式(polynomial)时间内算出来,就叫做多项式非确定性问题。

NPC问题:NP中的某些问题的复杂性与整个类的复杂性相关联.这些问题中任何一个如果存在多项式时间的算法,那么所有NP问题都是多项式时间可解的.这些问题被称为NP-完全问题(NPC问题)。

欧拉路径/回路(Euler)与汉密尔顿路径/回路(Hamilton)

  • 如果给定无孤立结点图G,若存在一条路,经过图中每边一次且仅一次,这条路称为欧拉路径

  • 如果给定无孤立结点图G,若存在一条回路(起点和终点相同),经过图中每边一次且仅一次,那么该回路称为欧拉回路

  • 给定图G,若存在一条路,经过图中每个结点恰好一次,这条路称作汉密尔顿路径

  • 给定图G,若存在一条回路(起点和终点相同),经过图中每个结点恰好一次,这条回路称作汉密尔顿回路

在这里插入图片描述

欧拉回路与哈密尔顿回路的区别

「哈密尔顿回路问题」与「欧拉回路问题」看上去十分相似,然而却是完全不同的两个问题。「哈密尔顿回路问题」是访问除原出发节点以外的每个结点一次且仅一次,而「欧拉回路问题」是访问每条边一次且仅一次;对任一给定的图是否存在「欧拉回路」欧拉已给出了充分必要条件,而对任一给定的图是否存在「哈密尔顿回路」至今仍未找到满足该问题的充分必要条件。

在欧拉路径中,可能会多次访问一个顶点,在哈密尔顿路径中,可能无法穿过所有的边

De Bruijn sequence

B(k, n),是k元素构成的循环序列。所有长度为n的k元素构成序列都在它的子序列(以环状形式)中,出现并且仅出现一次。

例子

使用de Bruijn图的示例

目标:使用欧拉(n − 1 = 4 − 1 = 3)3-D de Bruijn图循环构造长度为2^4 = 16的B(2,4)de Bruijn序列。

在这里插入图片描述

两个B(2,3)de Bruijn序列和B(2,4)序列的有向图。在B(2,3)中。每个顶点被访问一次,而在B(2,4)中,每个边都被遍历一次。

此3维de Bruijn图中的每个边对应于一个四位数字的序列:三个数字分别标记该边缘要离开的顶点,其后是一个数字标记该边缘。如果一个人从000穿过标记为1的边,则一个人到达001,从而表明de Bruijn序列中存在子序列0001。精确地遍历每个边一次就是使用16个四位数序列中的每一个恰好一次。

例如,假设我们在这些顶点上遵循以下欧拉路径:

000, 000,001,011,111,111,110,101,011

​ 110,100,001,010,101,010,100,000

这些是长度为k的输出序列:

0 0 0 0

_ 0 0 0 1

_ _ 0 0 1 1

这对应于以下de Bruijn序列:

0 0 0 0 1 1 1 1 0 1 1 0 0 1 0 1

八个顶点以下列方式出现在序列中:

      {0 0 0 0} 1 1 1 1 0 1 1 0 0 1 0 1
       0 {0 0 0 1} 1 1 1 0 1 1 0 0 1 0 1
       0 0 {0 0 1 1} 1 1 0 1 1 0 0 1 0 1
       0 0 0 {0 1 1 1} 1 0 1 1 0 0 1 0 1
       0 0 0 0 {1 1 1 1} 0 1 1 0 0 1 0 1
       0 0 0 0 1 {1 1 1 0} 1 1 0 0 1 0 1
       0 0 0 0 1 1 {1 1 0 1} 1 0 0 1 0 1
       0 0 0 0 1 1 1 {1 0 1 1} 0 0 1 0 1
       0 0 0 0 1 1 1 1 {0 1 1 0} 0 1 0 1
       0 0 0 0 1 1 1 1 0 {1 1 0 0} 1 0 1
       0 0 0 0 1 1 1 1 0 1 {1 0 0 1} 0 1
       0 0 0 0 1 1 1 1 0 1 1 {0 0 1 0} 1
       0 0 0 0 1 1 1 1 0 1 1 0 {0 1 0 1}
       0} 0 0 0 1 1 1 1 0 1 1 0 0 {1 0 1 ...
   ... 0 0} 0 0 1 1 1 1 0 1 1 0 0 1 {0 1 ...
   ... 0 0 0} 0 1 1 1 1 0 1 1 0 0 1 0 {1 ...

…然后我们回到起点。八个3位数序列(对应于八个顶点)中的每一个都出现两次,而十六个4位数序列(对应于16个边沿)中的每个出现一次。

请添加图片描述

正文

Q :题目问什么?

A:将0-k^n(k进制)的数字组合在一起,使得该字符长度最小

Q:题目问什么?不太理解

A:确实,我翻看了评论区,有这种想法的不在少数,解释如下:

举例:n=2,k=2,密码是00 01 10 11,因此需要找到一个组合能包含这些所有的组合,当每次输入的时候,最后的n个数可以尝试解密码

答案是00110

当输入0的时候,没有操作
再次输入0,组合成00,这时候可以匹配到00
再次输入1,组合成001,这时候01可以匹配到01
再次输入1,组合成0011,这时候11可以匹配到11
再次输入0,组合成00110,这时候10可以匹配到10

答案要求的是找出一个最短的字符包含了所有可能的组合数

欧拉路径,有些编码技巧

方法1
public String crackSafe(int n, int k) {
    //初始化的扩散seed
    StringBuilder seed = new StringBuilder(String.join("", Collections.nCopies(n, "0")));
    Set<String> vis = new HashSet<>();
    vis.add(seed.toString());
    int t = (int) Math.pow(k, n);//目标的组合数量
    dfs(seed, vis, t, n, k);
    return seed.toString();
}

private boolean dfs(StringBuilder seed, Set<String> vis, int t, int n, int k) {
    if (vis.size() == t) return true;//出口条件,组合数到达目标的组合数量
    String last = seed.substring(seed.length() - n + 1);//最后的n-1个字符
    for (char c = '0'; c < '0' + k; c++) {
        String tmp = last + c;
        if (!vis.contains(tmp)) {
            vis.add(tmp);
            seed.append(c);
            if (dfs(seed, vis, t, n, k)) return true;
            vis.remove(tmp);
            seed.deleteCharAt(seed.length() - 1);
        }
    }
    return false;
}
方法2
Set<Integer> visited = new HashSet<>();
StringBuilder res = new StringBuilder();
int h, k;

public String crackSafe(int n, int k) {
    h = (int) Math.pow(10, n - 1);
    this.k = k;
    dfs(0);
    for (int i = 1; i < n; i++) res.append('0');
    return res.toString();
}

private void dfs(int u) {
    for (int i = 0; i < k; i++) {
        int v = u * 10 + i;
        if (!visited.contains(v)) {
            visited.add(v);
            dfs(v % h);
            res.append(i);
        }
    }
}

Reference

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值