[LeetCode] 854. K-Similar Strings 类似度为K的字符串



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. 1 <= A.length == B.length <= 20
  2. 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



相似题目:

Couples Holding Hands



参考资料:

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

https://leetcode.com/problems/k-similar-strings/discuss/140099/JAVA-BFS-32-ms-cleanconciseexplanationwhatever



LeetCode All in One 题目讲解汇总(持续更新中...)

相关文章
相关标签/搜索