[Leetcode] Alien Dictionary

这一题的关键词其实只有一个:拓扑排序。
整个流程其实就是很简单的:建图+排序。 如果建图过程里面发现了有向环,就表示order invalid,上图第三个例子就是一个有向环,z -> x -> z。否则建图之后答案就是一个拓扑排序的结果。可以有bfs和dfs两种。

先说如何建图:
1. 这是一个特殊的alphabetical排序。用example 1来说就是"wrt" > "wrf" 表示 "t" > "f"。也就是前一个字符串和后一个字符串的第一个不同的字符会表示出它在图之间的关系。譬如说我们知道了"wrt" > "wrf",所以"wrta"肯定也是"wrfb",也所以,这样的顺序你也不会知道a和b之间的关系。所以基本上顺序遍历input,找到当前字符串和下一个字符串第一个不同的字符,不停往下找你就会找到整幅图的所有的有向边,然后根据这些边造图。譬如说,第一个example

"wrt", "wrf", "er", "ett", "rftt"
1. wrt和wrf 中, t比f大,得到边 t -> f
2. wrf和er比, w比e大,得到边 w->e
3. er和ett比,r比t大,得到边r->t
4. ett和rftt,e比r大,得到边e->r
最后就能得到一个完整的链条,w->e->r->t->f,所以结果是wertf

当然,example1是一个线性图,也就是只有一条线,譬如我在加一个 af 在最后。就多了一条r -> a的边。这个时候就在r那里产生了一条分之就变成了w -> e -> r -> t -> f
                                                  -> a
这个时候作为拓扑排序的结果不管是wertfa或者是weratf或者是wertaf都是合理答案(也符合题目里的note 4)。因为t,f 和 a之间的关系是不确定的,但也是合理不冲突的。和有向环不太一样。

2. 拓扑排序
所以建图的方式基本上已经了解了。接下来就要讨论一下怎么遍历图了。有两种方式,bfs和dfs。但这两者都需要考虑到一个问题,就是无向环。举个例子,如果你有一条路径是a -> b -> c -> d,但你又有一条路径是a -> e -> d。两条路径的终点都是d但是一条中间有两个节点,另一条只有一个。如果dfs,结果会是abcded或者aedbcd,如果代码输出时忽略走过的点就是abcde或者aedbc,这样都不对。如果是bfs是abecdd,或者aebdcd,如果忽略走过的点,其中后面那个aebdc也是不对的。

接下来,我从两种不同的角度来分析如何解决这个问题
1. dfs
关键词:状态标记,自下而上。dfs经常用到拿来判断环的方式是状态表。就是走过一个点就把它在一个哈希表里记录为visited。所以一般自上而下遍历的时候,状态一般就分为两种。visited or not visited。但是其实自下而上的递归遍历可以有第三种,visiting。 我举个例子, a -> b -> c,自上而下,你从a 走到b 走到c一般就完了。自下而上,是你从a先往下传递某样东西到b,再往下传递到c,然后在c那里开始往上返回某个值回b,再返回某个值回a。这个才是一个完整的过程。所以,在往下传递的时候,我们称之为visiting。判断有向环的条件就是如果在传递的过程里,你碰到的某个值是处于visiting的状态。而无向环的条件就是在传递的过程里某个值处于visited的状态,一旦遇到无向环,我们就终止往下遍历,因为已经走过了。举个例子,如果你有一个a -> b -> a。a走到b的时候变成了visiting, 走到另一个a的时候依旧是visiting,这样我们就认为我们遇到了一个有向环。但如果说,你有一个a -> b -> c -> g -> f. 然后你还有一个a -> d -> e -> c -> g -> f。我们可以看出来a到c有两个不同的路径,形成了无向环。我们走完一遍a->b->c之后路径之后,c已经在往上返回到a的时候已经变成了visited了, g和f也走过了,所以当走a->d->e->c的时候走到了c之后你就发现visited了,就可以直接返回了。
同样的,自下而上同时还解决了另一个问题就是,子节点可以永远发生在母亲节点前面。对,是子节点在前面,然后我们再reverse结果就可以了。就拿刚才abcgf和adecgf的例子来说,走a->b->c->g->f的时候, 自下而上的过程里,我们可以回收f->g->c->b这些visited的节点,也就是fgcb,然后走a->d->e->c->g->f的过程里我们还可以回收e->d,组成fgcbed,我们最后再回收最母亲的节点a,变成fgcbeda。然后反转,就变成了一个有效的答案adebcgf

根据上述描述,可以得到代码如下:

    public String alienOrder(String[] words) {
        HashMap<Character, HashSet<Character>> graph = new HashMap<>();
        HashMap<Character, Integer> visited = new HashMap<>();
        buildGraph(graph, visited, words);
        StringBuilder resBuilder = new StringBuilder();
        for (char curCh : visited.keySet()) {
            if (visited.get(curCh) == 0 && !topoSortDFS(graph, visited, resBuilder, curCh)) return ""; 
        }
        
        return resBuilder.reverse().toString();
    }
    
    public boolean topoSortDFS(HashMap<Character, HashSet<Character>> graph, HashMap<Character, Integer> visited, StringBuilder resBuilder, char curCh) {
        if (visited.get(curCh) != 0) {
            return visited.get(curCh) == 2;
        } else {
            visited.put(curCh, 1);
            HashSet<Character> next = graph.get(curCh);
            if (next != null) {
                for (char nextCh : next) {
                    if (!topoSortDFS(graph, visited, resBuilder, nextCh)) return false;
                }
            }
            visited.put(curCh, 2);
            resBuilder.append(curCh);
            return true;
        }
    }
    
    public void buildGraph(HashMap<Character, HashSet<Character>> graph, HashMap<Character, Integer> visited, String[] words) {
        for (int i = 0; i < words.length; i++) {
            char[] chArr = words[i].toCharArray();
            for (char ch : chArr) visited.putIfAbsent(ch, 0);
            
            if (i > 0) {
                char[] prevChArr = words[i - 1].toCharArray();
                int len = Math.min(prevChArr.length, chArr.length);
                for (int j = 0; j < len; j++) {
                    if (prevChArr[j] != chArr[j]) {
                        if (!graph.containsKey(prevChArr[j])) graph.put(prevChArr[j], new HashSet<>());
                        graph.get(prevChArr[j]).add(chArr[j]);
                        break;
                    }
                }
            }
        }
    }

2. BFS
关键词:入度。
先解释一下何为入度,就是有多少不同的母亲节点指向它。入度为0的节点在一个图里即为起始母亲节点。举个例子,a->b, c->b里,a和c入度为0,b入度为2,因为a和c都指向它。 对于bfs来说,一个关键的问题在于如果多个母亲节点指向一个节点(依旧是类似无向环的问题),而且路径长度不同,我们在到底在哪一次遍历到这个节点的时候才开始把这个节点push到queue里面作为下一层。很正常的想法同时也是正解是路径最长的那一次。问题是你做bfs的时候你很难知道这个节点所对应最长的路径是哪一条,因为按路径摸索是dfs干的事情。所以这个时候我们会考虑从入度着手,因为bfs是一层层的走,所以每遇到一次,入度减一,当入度减到0的时候,我们就知道这个节点该被纳入下一层考虑了。
举例,a->c->... ,  b->d->c->..., e->f->w->c->..。c的入度为3。第一层是a,b,e,第二层本应是c,d,f,但是在这里c的入度还没到0,所以我们略过c,第二层只有d,f,第三层本应是c,w,但这个时候c的入度还有1,所以第三层就剩下w,第四层只有一个c了,而且入度为0。这个时候,我们就可以沿着c走c以后的路径层了。

同时,bfs还需要解决的是有向环的问题,如果用visited来解决,在bfs你很难分辨当你遇到一个visited的节点的时候,它是来自于一个有向环或者无向环(或者不同的路径)。因为bfs是不记录路径的消息的(正常来说),但是入度依旧可以解决有向环的问题。如果一个入度表里(我们通常用HashMap<Character(节点),Integer(入度)>来表示),在走完一遍bfs的情况下,入度表里还有剩下入度不为1的节点,那就表示产生了有向环,没有一个合法的拓扑排序。原理可以理解为一个有向环会对环内的路径产生无限的入度,所以bfs无法消除干净。所以此时入度表里面还会存在有向环路径的节点。
根据上述原理,可以得到代码如下:

    public String alienOrder(String[] words) {
        HashMap<Character, HashSet<Character>> graph = new HashMap<>();
        HashMap<Character, Integer> degree = new HashMap<>();
        buildGraph(graph, degree, words);
        
        return topoSortBFS(graph, degree);
    }
    
    public String topoSortBFS(HashMap<Character, HashSet<Character>> graph, HashMap<Character, Integer> degree) {
        Queue<Character> queue = new LinkedList<Character>();
        StringBuilder sb = new StringBuilder();
        for (char ch : degree.keySet()) {
            if (degree.get(ch) == 0) queue.add(ch);
        }
        
        while (!queue.isEmpty()) {
            char curCh = queue.poll();
            sb.append(curCh);
            degree.remove(curCh);
            HashSet<Character> nextChs = graph.get(curCh);
            if (nextChs != null) {
                for (char nextCh : nextChs) {
                    int nextDegree = degree.get(nextCh) - 1;
                    if (nextDegree == 0) {
                        queue.add(nextCh);
                    }
                    degree.put(nextCh, nextDegree);
                }
            }
        }
        
        return degree.isEmpty() ? sb.toString() : "";
    }
    
    public void buildGraph(HashMap<Character, HashSet<Character>> graph, HashMap<Character, Integer> degree, String[] words) {
        for (int i = 0; i < words.length; i++) {
            char[] chArr = words[i].toCharArray();
            for (char ch : chArr) degree.putIfAbsent(ch, 0);
            
            if (i > 0) {
                char[] prevChArr = words[i - 1].toCharArray();
                int len = Math.min(prevChArr.length, chArr.length);
                for (int j = 0; j < len; j++) {
                    if (prevChArr[j] != chArr[j]) {
                        if (!graph.containsKey(prevChArr[j])) graph.put(prevChArr[j], new HashSet<>());
                        
                        if (!graph.get(prevChArr[j]).contains(chArr[j])) {
                            graph.get(prevChArr[j]).add(chArr[j]);
                            degree.put(chArr[j], degree.get(chArr[j]) + 1);
                        }
                        break;
                    }
                }
            }
        }
    }

猜你喜欢

转载自blog.csdn.net/chaochen1407/article/details/81602871
今日推荐