Strings `A` and `B` are `K`-similar (for some non-negative integer `K`) if we can swap the positions of two letters in `A` exactly `K` times so that the resulting string equals `B`.
Given two anagrams A
and B
, return the smallest K
for which A
and B
are K
-similar.
Example 1:
Input: A = "ab", B = "ba"
Output: 1
Example 2:
Input: A = "abc", B = "bca"
Output: 2
Example 3:
Input: A = "abac", B = "baca"
Output: 2
Example 4:
Input: A = "aabc", B = "abca"
Output: 2
Note:
1 <= A.length == B.length <= 20
A
andB
contain only lowercase letters from the set{'a', 'b', 'c', 'd', 'e', 'f'}
這道題說是當字符串A通過交換自身的字符位置K次能得到字符串B的話,就說字符串A和B的相似度為K。現在給了兩個異構詞A和B,問最小的相似度是多少。換一種說法就是,最少交換多少次可以將字符串A變為B,在另一道題目 [Snakes and Ladders](https://www.cnblogs.com/grandyang/p/11342652.html) 中提到了求最小值還有一大神器,廣度優先搜索 BFS,最直接的應用就是在迷宮遍歷的問題中,求從起點到終點的最少步數,也可以用在更 general 的場景,只要是存在確定的狀態轉移的方式,可能也可以使用。這道題就是更 general 的應用,起點狀態就是A,目標狀態是B,狀態轉移的方式就是進行字符交換,博主開始想的是對當前狀態遍歷所有的交換可能,產生的新狀態若不在 visited 集合中就加入隊列繼續遍歷,可是這種 Naive 的思路最終超時了 Time Limit Exceeded。為什么呢?因為對於每個狀態都遍歷所有都交換可能,則每一個狀態都有平方級的復雜度,整個時間復雜度就太大了,雖然有很多重復的狀態不會加入隊列中,但就算是交換字符,HashSet 查重這些操作也夠編譯器喝一壺的了。所以必須要進行優化,而且是大幅度的優化。首先來想,為啥要限定A和B是異構詞,這表明A和B中的字符的種類及其個數都相同,就是排列順序不同,則A經過交換是一定能變為B的,而且交換的次數在區間 [0, n-1] 內,n是A的長度。再來想,是不是A中的每個字符都需要交換呢?答案是否定的,當A中某個位置i上的字符和B中對應位置的字符相等,即 A[i]=B[i] 時,就不需要交換,這樣就可以用一個 while 循環,找到第一個不相等的i。交換的第一個字符確定了,就可以再往后遍歷,去找第二個字符了,同理,第二個字符位置j,不能存在 A[j]=B[j],比如 ab 和 bb,交換之后變為 ba 和 bb,還是不相等,最好是存在 A[j]=B[i],比如 ab 和 ba,這樣交換之后就變為 ba 和 ba,完美 match 了。找到了i和j之后,就可以進行交換了,然后判斷新狀態不在 visited 中的話,加入 visited 集合,同時加入隊列 queue,之后還要交換i和j還原狀態,每一層遍歷結束后,結果 res 自增1即可,參見代碼如下:
解法一:
class Solution {
public:
int kSimilarity(string A, string B) {
int res = 0, n = A.size();
queue<string> q{{A}};
unordered_set<string> visited{{A}};
while (!q.empty()) {
for (int k = q.size(); k > 0; --k) {
string cur = q.front(); q.pop();
if (cur == B) return res;
int i = 0;
while (i < n && cur[i] == B[i]) ++i;
for (int j = i + 1; j < n; ++j) {
if (cur[j] == B[j] || cur[j] != B[i]) continue;
swap(cur[i], cur[j]);
if (!visited.count(cur)) {
visited.insert(cur);
q.push(cur);
}
swap(cur[i], cur[j]);
}
}
++res;
}
return -1;
}
};
我們也可以使用遞歸+記憶數組的方式來寫,這里沒用數組,而是用的 HashMap,沒啥太大區別。在遞歸函數中,先判斷若當前狀態 cur 和B相等了,直接返回0,若 cur 已經在 HashMap 中存在了,返回其映射值。之后就進行和上面相同的操作,先找出使得 cur[i] 和 B[i] 不同的i,然后從 i+1 開始遍歷j,遇到 cur[j]=B[j] 或 cur[j]!=B[i] 時跳過,交換 cur 中的i和j位置,對新狀態調用遞歸,若返回值不是整型最大值,則將其加1,並更新結果 res,然后恢復 cur 之前的狀態。for 循環結束后,在 HashMap 中建立 cur 和結果 res 的映射,並返回映射值即可。注意這里的 for 循環中只能寫成 cur[j] != B[i],而上面的解法好像還可以寫成 cur[i] != B[j],感覺蠻奇怪的,各位看官大神們知道原因的請留言告訴博主哈~
解法二:
class Solution {
public:
int kSimilarity(string A, string B) {
unordered_map<string, int> memo;
return helper(A, B, 0, memo);
}
int helper(string cur, string B, int i, unordered_map<string, int>& memo) {
if (cur == B) return 0;
if (memo.count(cur)) return memo[cur];
int res = INT_MAX, n = cur.size();
while (i < n && cur[i] == B[i]) ++i;
for (int j = i + 1; j < n; ++j) {
if (cur[j] == B[j] || cur[j] != B[i]) continue;
swap(cur[i], cur[j]);
int next = helper(cur, B, i + 1, memo);
if (next != INT_MAX) {
res = min(res, next + 1);
}
swap(cur[i], cur[j]);
}
return memo[cur] = res;
}
};
這道題還有一種基於貪婪算法的神奇遞歸寫法,清新脫俗,擊敗率也蠻高的。之前提到了由於A和B是異構詞,則A經過交換是一定能變為B的,而且交換的次數在區間 [0, n-1] 內,n是A的長度。最好的情況就是一次交換可以產生兩個 match,比如 bac 和 abc,通過交換 bac 的前兩個字符,直接就變成 abc。所以只要遇到能一次變換產生兩個 match 的,一定是最優解的一部分,可以直接對后面剩余部分調用遞歸並累加。但也有無法產生兩個 match 的時候,比如 bac 和 acb,不論如何變換,都沒法做到一次交換產生兩個 match,那就退而求其次吧,產生1個新的 match 也行,但此時不能直接對其調用遞歸返回,因為誰知道后面還有沒有能產生2個 match 的,所以要把當前的j位置先保存到一個數組中,直到確認了后面都不會有產生2個 match 的位置后,再來處理這些只產生1個 match 的備胎們,方法是取出每個備胎,和i進行交換,然后調用遞歸,返回加1后來更新結果 res,再交換回來恢復狀態。最后返回結果 res 即可,參見代碼如下:
解法三:
class Solution {
public:
int kSimilarity(string A, string B) {
int n = A.size(), res = n - 1;
for (int i = 0; i < n; ++i) {
if (A[i] == B[i]) continue;
vector<int> matches;
for (int j = i + 1; j < n; ++j) {
if (A[j] == B[j] || A[j] != B[i]) continue;
matches.push_back(j);
if (A[i] != B[j]) continue;
swap(A[i], A[j]);
return 1 + kSimilarity(A.substr(i + 1), B.substr(i + 1));
}
for (int j : matches) {
swap(A[i], A[j]);
res = min(res, 1 + kSimilarity(A.substr(i + 1), B.substr(i + 1)));
swap(A[i], A[j]);
}
return res;
}
return 0;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/854
類似題目:
參考資料:
https://leetcode.com/problems/k-similar-strings/
https://leetcode.com/problems/k-similar-strings/discuss/140299/C%2B%2B-6ms-Solution
https://leetcode.com/problems/k-similar-strings/discuss/139872/Java-Backtracking-with-Memorization
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)