【Leetcode】269. Alien Dictionary

题目地址:

https://leetcode.com/problems/alien-dictionary/

给定一个有小写英文字母组成的字符串组成的长 n n n数组 A A A A A A是按照字典序排序的,但是其字典序是另外定义的,并不一定是 a b c d . . . z abcd...z abcd...z这样的顺序。要求将这个字典序求出来,返回按字典序排序的字符串。字符串只需要包含数组里出现过的字符即可。如果答案不唯一,则返回任意一个都可以。

两个字符串 s 1 s_1 s1 s 2 s_2 s2的字典序的先后,是按照字符依次比较,如果发现了第一个不同的字符,例如在下标为 i i i的位置不同,那么就说明 s 1 [ i ] s_1[i] s1[i]的字典序排在 s 2 [ i ] s_2[i] s2[i]之前。如果没有发现不同的字符,但是 s 1 s_1 s1的长度大于 s 2 s_2 s2,那么说明不存在合法的字典序;如果 s 1 s_1 s1的长度小于等于 s 2 s_2 s2,则得不出任何关于字符的字典序的结论。由此可以看出,可以用图论建模,如果某个字符 x x x排在另一个字符 y y y之前,则在图中就有 x x x y y y的一条边。问题就转化为拓扑排序。

由于这个题两个字母之间只能连一条边(连多条边没有意义),所以可以采用bool邻接矩阵的方式建图。并且要尤其注意 n = 1 n=1 n=1的情形,此时任何顺序都是合法的,但是要注意去重。

法1:BFS。如果发现排序的结果的长度不够全部的字符的个数,则直接返回空串,否则返回排序结果。代码如下:

class Solution {
 public:
  string alienOrder(vector<string>& ws) {
    // 只含一个字符串是特例,需要去重然后返回
	if (ws.size() == 1) {
      auto& s = ws[0];
      unordered_set<char> st(s.begin(), s.end());
      return string(st.begin(), st.end());
    }

    vector<vector<bool>> g(26, vector<bool>(26));
    unordered_set<char> st(ws[0].begin(), ws[0].end());
    auto cmp = [&](auto& s1, auto& s2) {
      st.insert(s2.begin(), s2.end());
      int m = s1.size(), n = s2.size();
      bool diff = false;
      for (int i = 0; i < m && i < n; i++)
        if (s1[i] != s2[i]) {
          g[s1[i] - 'a'][s2[i] - 'a'] = diff = true;
          if (g[s2[i] - 'a'][s1[i] - 'a']) return false;
          break;
        }
      if (!diff && m > n) return false;
      return true;
    };
    for (int i = 0; i + 1 < ws.size(); i++)
      if (!cmp(ws[i], ws[i + 1])) return "";
    // 开始BFS版本的拓扑排序
    unordered_map<int, int> ind;
    for (int i = 0; i < 26; i++)
      for (int j = 0; j < 26; j++)
        if (g[i][j]) ind[j]++;
    queue<int> q;
    for (char ch : st)
      if (!ind.count(ch - 'a')) q.push(ch - 'a');
    string res;
    while (q.size()) {
      auto t = q.front();
      q.pop();
      res += 'a' + t;
      for (int ne = 0; ne < 26; ne++)
        if (g[t][ne] && !--ind[ne]) q.push(ne);
    }
    if (res.size() < st.size()) return "";
    return res;
  }
};

时空复杂度 O ( V + E ) O(V+E) O(V+E)

注解:
建图的时候需要注意碰到相同的边,不要重复计。所以graph的value取的是哈希表,这样加入相同的边的时候会被覆盖掉。如果不取哈希表取list的话,就会造成平行边。同时,在计入度的时候,要在建图完了再计,遍历数组的时候只需要计一下哪些字符出现过就可以了,原因是,怕平行边出现,会把入度计的更多。所以,建图有很多细节需要注意。

法2:DFS。深搜递归返回的顺序天然是拓扑序,而且DFS的时候还可以顺便判断有没有圈。建图和法1一样。代码如下:

class Solution {
 public:
  string alienOrder(vector<string>& ws) {
	if (ws.size() == 1) {
      auto& s = ws[0];
      unordered_set<char> st(s.begin(), s.end());
      return string(st.begin(), st.end());
    }

    vector<vector<bool>> g(26, vector<bool>(26));
    unordered_set<char> st(ws[0].begin(), ws[0].end());
    // 将s1<s2给出的字母顺序的信息记在g里。如果发现了矛盾则返回false,否则返回true
    auto cmp = [&](auto& s1, auto& s2) {
      st.insert(s2.begin(), s2.end());
      int m = s1.size(), n = s2.size();
      bool diff = false;
      for (int i = 0; i < m && i < n; i++)
        // 第一个不同的位置即得到字母的大小信息
        if (s1[i] != s2[i]) {
          g[s1[i] - 'a'][s2[i] - 'a'] = diff = true;
          // 发现了环,直接返回false
          if (g[s2[i] - 'a'][s1[i] - 'a']) return false;
          break;
        }
      // 如果s2是s1的真前缀,就矛盾了,返回false
      if (!diff && m > n) return false;
      return true;
    };
    for (int i = 0; i + 1 < ws.size(); i++)
      if (!cmp(ws[i], ws[i + 1])) return "";
    string res;
    vector<int> vis(26, -1);
    for (char ch : st)
      if (!~vis[ch - 'a'])
        if (!dfs(ch - 'a', g, vis, res)) return "";

    // DFS记录的是拓扑排序的逆序,要翻一下
    reverse(res.begin(), res.end());
    return res;
  }

  // 从点u开始DFS,返回u所在的连通子图是否能拓扑排序
  bool dfs(int u, auto& g, auto& vis, string& res) {
    // 标记为当前轮访问过
    vis[u] = 0;
    for (int ne = 0; ne < 26; ne++)
      if (g[u][ne]) {
      	// 发现环了,不可拓扑排序,返回false
        if (!vis[ne])
          return false;
        // 走到一个未访问节点并且之后发现了不可拓扑排序,也返回false
        else if (!~vis[ne] && !dfs(ne, g, vis, res))
          return false;
      }
    // 标记为访问过
    vis[u] = 1;
    res.push_back('a' + u);
    return true;
  }
};

时空复杂度 O ( V + E ) O(V+E) O(V+E)

注解:
关于DFS的细节可以参考
https://blog.csdn.net/qq_46105170/article/details/105722476

比较BFS和DFS我们可以发现, DFS的优势在于不需要记录入度,并且逻辑相对也更简单,更好写。所以拓扑排序尽可能的用DFS来写更好。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值