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
.html
Given two anagrams A
and B
, return the smallest K
for which A
and B
are K
-similar.git
Example 1:github
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
and B
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 中提到了求最小值還有一大神器,廣度優先搜索 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便可,參見代碼以下:code
解法一:htm
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],感受蠻奇怪的,各位看官大神們知道緣由的請留言告訴博主哈~blog
解法二:
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