Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursively.
Below is one possible representation of s1 = "great"
:
great / \ gr eat / \ / \ g r e at / \ a t
To scramble the string, we may choose any non-leaf node and swap its two children.
For example, if we choose the node "gr"
and swap its two children, it produces a scrambled string "rgeat"
.
rgeat / \ rg eat / \ / \ r g e at / \ a t
We say that "rgeat"
is a scrambled string of "great"
.
Similarly, if we continue to swap the children of nodes "eat"
and "at"
, it produces a scrambled string "rgtae"
.
rgtae / \ rg tae / \ / \ r g ta e / \ t a
We say that "rgtae"
is a scrambled string of "great"
.
Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1.
今天的题目是判断两字符串能否通过特定方式相互转换,题目难度为Hard。
比较直观的想法是将两个字符串在任意位置一分为二,s1分为s11和s12,s2分为s21和s22,存在两种满足条件的可能性:
- s11和s21对应,s12和s22对应,s11和s21长度相同,两对字符串能否分别相互转换;
- s11和s22对应,s12和s21对应,s11和s22长度相同,两对字符串能否分别相互转换;
这样采用分治法的思想将问题化为相似的子问题来解决,通过递归即可求出最终结果。提交之后超时了,需要进行优化。
两个字符串如果能够相互转换,则两者中所含的字符种类和个数必定是相同的,在将字符串拆分递归判断之前可以先进行筛选,将不满足该条件的情况去除掉,这样可以大幅减少递归的可能性,提高算法效率,提交之后通过了。具体代码:
class Solution {
public:
bool isScramble(string s1, string s2) {
if(s1 == s2) return true;
int sz = s1.size();
vector<int> cnt(26, 0);
for(auto c:s1) ++cnt[c-'a'];
for(auto c:s2) if(--cnt[c-'a']<0) return false;
for(int i=1; i<sz; ++i) {
if(isScramble(s1.substr(0, i), s2.substr(0, i)) && isScramble(s1.substr(i), s2.substr(i)))
return true;
if(isScramble(s1.substr(0, i), s2.substr(sz-i)) && isScramble(s1.substr(i), s2.substr(0, sz-i)))
return true;
}
return false;
}
};
以上策略通过递归来自顶向下解决问题,递归中会有许多重复的子问题,应该还可以通过动态规划的方法来解决,重要的是找出动态规划的切入点。这里我们用isScr[i][j][k]表示s1从下标i开始长度为k的子串和s2从下标j开始长度为k的子串能否相互转换。k有1到s1.size()种情况,k为1时,isScr[i][j][1]只需判断s1[i]和s2[j]是否相同即可;k大于1时,和上面递归的方法相同,只需要在长度k范围内任意位置将字符串一分为二,然后依然分两种情况进行判断即可,具体的推导公式如下,大家看了应该就明白了:
isScr[i][j][k] = (isScr[i][j][x] && isScr[i+x][j+x][k-x]) || (isScr[i][j+k-x][x] && isScr[i+x][j][k-x]);
其中x是长度为k的子串一分为二后左边部分的长度。这样采用动态规划的方法自底向上也可以求出最终结果。具体代码:
class Solution {
public:
bool isScramble(string s1, string s2) {
int sz = s1.size();
vector<vector<vector<bool>>> isScr(sz, vector<vector<bool>>(sz, vector<bool>(sz+1, false)));
for(int k=1; k<=sz; ++k) {
for(int i=0; i+k<=sz; ++i) {
for(int j=0; j+k<=sz; ++j) {
if(k == 1) isScr[i][j][k] = s1[i]==s2[j];
else {
for(int x=1; x<k; ++x) {
isScr[i][j][k] = (isScr[i][j][x] && isScr[i+x][j+x][k-x]) || (isScr[i][j+k-x][x] && isScr[i+x][j][k-x]);
if(isScr[i][j][k]) break;
}
}
}
}
}
return isScr[0][0][sz];
}
};
另外,第一种方法中可能会有重复的子问题进行多次计算,这也是我们想到动态规划的原因,应该还可以缓存子问题结果来减少递归可能性进而提高算法效率,感兴趣的同学可以试一下。