文章目录
(Week 7 算法博客)
题目
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 and B contain only lowercase letters from the set {‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’}
分析
根据 K-Similar String 的产生过程,可以设想,每个 a[i]->b[i]
都是一条边(除了 a[i] == b[i]
的情况),所有的 a[i]->b[i]
构成一个图。a[i]
不断交换到 b[j]
的过程产生一个环。
例如,由"aabc"和"abca",“aabbccddee"和"cdacbeebad”,可以构造如下的图:
对一个环,交换的次数 = 这个环的节点数 - 1。
那么是不是可以通过把环分离出来,寻找答案呢?
其实很难。因为有一个例子:
A="aeabcdefbcdf"
B="bcdfbeaaefcd"
由此构造的图,可以有如下两组不同的环分解结果:
我改了许多次代码后,因为这个例子而再次失败了,一看,我觉得……扑街啊。
如果要把里面包含的各种环都分离出来,是很复杂的。那么,考虑不将字符看作节点,而是将整个字符串看作一个节点,一次简单的交换操作构成一条边,通过BFS来找到终点的最短路径。
以字符串为节点,进行BFS
设A为源字符串,B为目的字符串,这样定义一个节点字符串S的子节点:
找到B的第一个不和S匹配的字符 c
,位于 i
,从 i
开始再找S的第一个 c
所在的位置 j
,交换 S[i]
和 S[j]
获得子节点。
C++代码如下:
class Solution {
public:
int kSimilarity(string A, string B) {
queue<string> q;
map<string, int> distance;
q.push(A);
distance[A] = 0;
while (!findNext(q, distance, B));
return distance[B];
}
bool findNext(queue<string> &q, map<string, int> &distance, const string &B) {
string A = q.front();
q.pop();
int i;
for (i = 0; i < A.length(); i++) {
if (A[i] != B[i]) break;
}
if (i == A.length()) return true;
for (int j = i + 1; j < A.length(); j++) {
if (B[i] == A[j]) {
string C = A;
C[j] = A[i];
C[i] = A[j];
if (distance.find(C) == distance.end()) {
distance[C] = distance[A] + 1;
q.push(C);
if (C == B) {
return true;
}
}
}
}
return false;
}
};
用时 。
暴力动态规划
根据网站所给的解答,还可以采用暴力动态规划来做这道题。
在进行规划之前,将字符看作节点,源字符串和目的字符串之间的转移关系是初始图,用一个长度为6*6的数组来表示这个图。
因为这个图中的交换次数等于 ,其中 表示图中某个环 的长度,
所以,这个图中的交换次数也等于 。
如果要使交换次数最小,就令环数 最大。
然后,可以得出这样的关系: ,其中 指从图中去除某个环 后的新图。
接下来,暴力进行动态规划:求出所有环 ,有 ,其中,参与运算的 应该是能够被 减去的环。
对 继续进行如上的过程,这样递归下去,得到最终答案。
相应的Java代码及注释如下:
class Solution {
String[] alphabet = new String[]{"a", "b", "c", "d", "e", "f"}; // 字母表
Map<String, Integer> memo; // key为由表示图的数组转换而来的字符串,value指这个图中最多有多少个环
// memo缓存初始图,以及初始图和空图之间所有可能的中间图
public int kSimilarity(String A, String B) {
if (A.equals(B)) return 0;
int N = A.length();
memo = new HashMap();
int ans = 0;
// count是初始图,表示源字符串和目的字符串之间的转移关系,用6*6数组表示
int[] count = new int[alphabet.length * alphabet.length];
for (int i = 0; i < N; ++i)
if (A.charAt(i) != B.charAt(i)) {
count[alphabet.length * (A.charAt(i) - 'a') + (B.charAt(i) - 'a')]++;
ans++;
}
// ans表示非自向边的数目
// possibles存储所有由环转换而来的图数组
List<int[]> possibles = new ArrayList();
// 枚举大小为size的环
for (int size = 2; size <= alphabet.length; ++size)
search: for (String cycle: permutations(alphabet, 0, size)) {
// 检查环内的字符是否升序排列的,是则合法,否则检查下一个环
for (int i = 1; i < size; ++i)
if (cycle.charAt(i) < cycle.charAt(0))
continue search;
// cycle合法,将cycle转换成图,加入possibles
int[] row = new int[count.length];
for (int i = 0; i < size; ++i) {
int u = cycle.charAt(i) - 'a';
int v = cycle.charAt((i+1) % size) - 'a';
row[alphabet.length * u + v]++;
}
possibles.add(row);
}
int[] ZERO = new int[count.length];
memo.put(Arrays.toString(ZERO), 0);
return ans - numCycles(possibles, count);
}
// count是某个图,它可能为初始图,也可能是某个由G-C得到的中间图
public int numCycles(List<int[]> possibles, int[] count) {
String countS = Arrays.toString(count);
if (memo.containsKey(countS)) return memo.get(countS);
// 如果possibles中的环图row能被count减去,则得到count2
// 然后求max{ans, 1+numCycles(possibles, count2)}
// ans指目前已求得的count图中的最大环数
int ans = Integer.MIN_VALUE;
search: for (int[] row: possibles) {
int[] count2 = count.clone();
for (int i = 0; i < row.length; ++i) {
if (count2[i] >= row[i])
count2[i] -= row[i];
else
continue search;
}
ans = Math.max(ans, 1 + numCycles(possibles, count2));
}
// 向memo中加入(count图,它对应的最大环数)
memo.put(countS, ans);
return ans;
}
// 枚举出所有的长度为size、不包含used代指的字符、有序的环
// 比如,used值为0b1,代指字符'a',size=1,则返回结果为["b","c","d","e","f"]
// used值为0b11,代指字符'a'和'b',size=1,则返回结果为["c","d","e","f"]
// used值为0b1111,代指字符'a''b''c''d',size=2,则返回结果为["ef"]
// used值为0,则返回的就是所有长度为size的有序字符串/环
public List<String> permutations(String[] alphabet, int used, int size) {
List<String> ans = new ArrayList();
if (size == 0) {
ans.add(new String(""));
return ans;
}
for (int b = 0; b < alphabet.length; ++b)
if (((used >> b) & 1) == 0)
for (String rest: permutations(alphabet, used | (1 << b), size - 1))
ans.add(alphabet[b] + rest);
return ans;
}
}