【Lintcode】892. Alien Dictionary

题目地址:

https://www.lintcode.com/problem/alien-dictionary/description

给定一个字符串数组,在某种字典序下这个数组是有序的,要求求这个字典序并以字符串的形式返回。如果不存在这样的字典序则返回空串。如果存在多个合法解,则返回字典序最小的那个(这里的字典序指的是英文字母的自然的字典序)。

本质是求拓扑排序。首先要排除几个特殊情况,如果数组里只有一个字符串,那么只需要将其字符排序后返回即可。如果数组中有两个字符串,后面的那个是前面的那个的子串,但是后面的那个长度更短,这时候也是不存在合法解的,因为字符串的比较中若同位置的字符都相等,那么短的应该在前面。排除完特殊情况后,通过字符串的顺序建图,如果某个字符 a a a顺序在另一个 b b b之前,则在图中从 a a a b b b连一条有向边。最后对图做拓扑排序即可。

但是,由于题目要求返回所有拓扑序中字典序最小的那个,这个时候DFS是不管用的,DFS不能对顶点分层。而BFS则可以,BFS做拓扑排序需要算一下每个点的入度,并且要用队列。这里需要用优先队列,将字典序最小的那个入度为 0 0 0的点入队。代码如下:

import java.util.*;

public class Solution {
    /**
     * @param words: a list of words
     * @return: a string which is correct order
     */
    public String alienOrder(String[] words) {
        // Write your code here
        if (words == null || words.length == 0) {
            return "";
        }
        
        // 严格来说,在数组长度为1的时候要先对其排序后再返回
        if (words.length == 1) {
            char[] res = words[0].toCharArray();
            Arrays.sort(res);
            return new String(res);
        }
        
        // 对words进行建图
        Map<Character, Set<Character>> graph = buildGraph(words);
        // 如果返回了空图说明不存在合法字典序,返回空串
        if (graph == null) {
            return "";
        }
        
        // 得到所有顶点的入度
        Map<Character, Integer> indegrees = getIndegrees(graph);
        
        // 开一个优先队列,将所有入度为0的顶点入队
        PriorityQueue<Character> pq = new PriorityQueue<>();
        for (Map.Entry<Character, Integer> entry : indegrees.entrySet()) {
            if (entry.getValue() == 0) {
                pq.offer(entry.getKey());
            }
        }
        
        StringBuilder sb = new StringBuilder();
        while (!pq.isEmpty()) {
            char cur = pq.poll();
            sb.append(cur);
            for (char next : graph.get(cur)) {
                indegrees.put(next, indegrees.get(next) - 1);
                if (indegrees.get(next) == 0) {
                    pq.offer(next);
                }
            }
        }
        // 最后字典序的长度不足字符个数,则说明存在环,返回空串
        if (sb.length() != graph.size()) {
            return "";
        }
        
        return sb.toString();
    }
    
    private Map<Character, Integer> getIndegrees(Map<Character, Set<Character>> graph) {
        Map<Character, Integer> indegrees = new HashMap<>();
        for (Map.Entry<Character, Set<Character>> entry : graph.entrySet()) {
            indegrees.putIfAbsent(entry.getKey(), 0);
            for (char ch : entry.getValue()) {
                indegrees.put(ch, indegrees.getOrDefault(ch, 0) + 1);
            }
        }
        
        return indegrees;
    }
    
    private Map<Character, Set<Character>> buildGraph(String[] words) {
        Map<Character, Set<Character>> graph = new HashMap<>();
        // 把所有字符(也就是顶点)都先加入图中
        for (int i = 0; i < words.length; i++) {
            for (int j = 0; j < words[i].length(); j++) {
                graph.putIfAbsent(words[i].charAt(j), new HashSet<>());
            }
        }
        
        for (int i = 0; i < words.length - 1; i++) {
            String w1 = words[i], w2 = words[i + 1];
            int idx = 0;
            while (idx < w1.length() && idx < w2.length()) {
                char c1 = w1.charAt(idx), c2 = w2.charAt(idx);
                if (c1 != c2) {
                    graph.get(c1).add(c2);
                    break;
                }
                idx++;
            }
            
            // 这个对应的情况是两个字符串对应位置字符都相等,但排在后面的字符串更短,
            // 这时是不存在合法字典序的,直接返回空图
            if (idx == w2.length() && w2.length() < w1.length()) {
                return null;
            }
        }
        
        return graph;
    }
}

时间复杂度 O ( V log ⁡ V + E ) O(V\log V+E) O(VlogV+E),空间 O ( V + E ) O(V+E) O(V+E)

算法正确性证明:
算法得出的序列是拓扑排序,这一点是没问题的。至于为什么字典序最小,可以用数学归纳法来证明。当第一个点出队后,这个点显然是排在第一位的,之后将其所有邻居的入度减一,也就是将原图中的这个点和其所有邻边都删掉,这样问题规模就变小了,由归纳假设,剩余图的字典序最小的拓扑排序就得到了,由数学归纳法知道算法正确。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值