题目地址:
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来写更好。