leetcode 854. 相似度为 K 的字符串

题目描述

字符串 s1 和 s2 是 k 相似 的(对于某些非负整数 k ),如果我们可以交换 s1 中两个字母的位置正好 k 次,使结果字符串等于 s2 。

给定两个字谜游戏 s1 和 s2 ,返回 s1 和 s2 与 k 相似 的最小 k 。
示例 1:
输入:s1 = “ab”, s2 = “ba”
输出:1
示例 2:
输入:s1 = “abc”, s2 = “bca”
输出:2
提示:
1 <= s1.length <= 20
s2.length == s1.length
s1 和 s2 只包含集合 {‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’} 中的小写字母
s2 是 s1 的一个字谜

分析

本题是个当之无愧的hard难度题目,虽然字符串长度不超过20,也只涉及6个字母,但是即使使用记忆化搜索,不去搜索已经搜索过的状态,状态数依旧庞大。字符串的每个位置有6个可能,一共有620种不同的字符串,也就是说搜索可能会涉及620种状态。这个级别的状态数不论是使用dfs、bfs、双向bfs还是A*算法,效率都很低。
使用BFS的话,对于长度为20的字符串,每次的交换位置有19 + 18 +… + 2 = 189种可能。发生一次交换后的状态就增加到189种,之后每交换一次状态数都会乘上189,三次交换后状态数就已经不能承受了。当然,去掉重复的状态,状态数会少很多,但是依然庞大。这题AC需要一定的贪心思想加上特别强的剪枝。
对于字符串两个位置字符的交换,可以分为四种情况:

  • 一次交换使得两个原本错位的字符进入到最终的位置了,比如s1=“ab”,s2 = “ba”,交换ab两个字符均归位。
  • 一次交换使得归位的字符数增加一个,比如s1=“abc”,s2=“bca”,交换ab得到bac,第一个字符b归位。
  • 一次交换后没有任何字符归位,比如s1=“abcd”,s2=“cdba”,交换ab得到bacd,没有任何字符归位。
  • 一次交换后已经归位的字符数减小了,这种交换操作相当于排序时交换增加了逆序对,离最终的解越来越远,自然不是最优的。

没有进行剪枝的BFS是不会区分以上四种情况的,也就说,大量的第四种情况的交换得到的状态进入到队列里。所以,第一次剪枝我们应该让发生交换的字符都不是处于最终位置的,这样即使不能增加归位的字符数,也不会减少归位的字符数。
再来考虑第三种情况,交换没有改变归位的字符数,比如s1 = “abcd”,s2 = “cdba”,第一次交换ab得到bacd,四个字符没有任何字符归位,但是这种交换的意义在于这次交换后原本交换一次不能都归位的两个字符在一次交换后都归位了,在bacd的基础上交换bc得到cabd,再交换ad得到cdba,也就是最终状态s2,这种交换方案也是最优的,但是一定存在其他的最优解,比如abcd,我们先让c归位,得到cbad,再让d归位得到cdab,再让b归位得到adba。也就是说,可以用三次第三类交换同样实现最优解的方案数。一次交换没有归位任何字符,要么这次交换没有接近最终状态,要么接近了最终状态但是存在其他类别的交换方案不比第三类交换差。既然存在可替代的交换方案,那么第三类交换我们也不必考虑了。
这样一来我们每次交换都只用考虑第一种和第二种情况,要么让一个字符归位,要么让两个字符归位。事实上,一次交换至少可以使得一个字符归位,这意味着长度为20的字符串最多交换19次就可以全部归位了。每次交换我们可以选择让1个或者2个字符归位,为了交换的次数最少,自然优先选择第一类交换。也就是s1[i] != s2[i];s1[j] != s2[j]时,如果s1[i] == s2[j];s1[j] = s2[i],那么就交换i和j两个位置的字符,使得两个位置的字符归位。如果不存在满足第一种情况的交换,我们就只能选择第二种交换了,s1[i] != s2[i],就从i + 1向后遍历,找到等于s2[i]的字符与s1[i]交换。
到这里我们已经将交换的字符限制在前两种情况了,并且还排了优先级,但是依旧会TLE。一个字符串满足第一种情况的交换位置可能很少,但是满足第二种情况的交换位置是正比于字符串的长度的。这样一来我们每层的状态数依旧会以20倍左右的速度扩散,尽管相比于一开始的189倍的速度已经大幅减少了,但是几轮下来状态数依旧会爆炸。
考虑下第一种情况,交换使得两个字符归位,这相当于问题规模减少了2,如果一次性把符合第一种情况的位置都交换了,就会很接近最终状态了。这时候我们可以使用剪枝,如果一次交换是第一种情况,那么我们不再将第二种情况的状态加入队列。初始状态s1,如果交换前两个位置能够使得前两个字符归位,交换三四的位置能够使得第三个字符归位,那么我们第一次交换前两个字符,后面可以再交换三四位置,这等价于先交换三四,再交换前两个位置,所以一次交换能够到达的状态里没必要加上交换三四状态的位置。这相当于在BFS搜索树中,我们知道了从第一条路走可能走到最优解,从第二条路走也可能走到最优解,我们只需要选择一条能够找到最优解的路去走就行了。
上面这种剪枝还是比较容易想到的,但是下面这种剪枝就比较难想了。思考一个问题:从初始状态s1出发,优先选择走第一种情况的路径,没有第一种情况的路径就走第二种情况的路径,这样沿着一条路径往下走是不是一定能走到最优解?取个极端情况,某次交换时没有符合第一种情况的交换,那么我们选择让任一个字符归位的交换方法是不是都能走到最优解?答案是不一定。假设s1[1] = ‘a’,而s2[1] = ‘b’,我们在s1中找到第3,4,5的位置字符都是b,选择其中任意一个字符与第一个字符交换都能让b归位,但是效果是不是一样的呢?如果交换1,3后不仅能让第一个字符归位,而且交换后还产生了符合第一种情况的交换位置,也就是下次交换3和另一个位置能够增加两个归位元素,那么这种交换肯定是优于其他位置的交换的。我们剪枝的目的在于找到一条能够确定能够到达最优解的路径,如果不能确定某次交换能否到达最优解,我们就不能剪去其他的分支。
第二种情况的交换位置可以分为:x1个与第一个元素交换能够让第一个元素归位的位置,x2个与第二个元素交换能够让第二个元素归位的位置,…,与第一个元素交换的x1个位置在BFS搜索树里相当于x1个分支,我们不能确定哪个分支能够到达最优解,但是可以确定其中至少有一条累计能够到达最优解。这样一来我们就只需要保留x1个分支,剩下的x2,x3分支可以全部剪掉。通俗的说就是先让第一个元素归位,再让第二个元素归位和先让第二个元素归位,再让第一个元素归位都能到达最优解,保留其中一条即可。这次剪枝我们每轮入队的的元素都是能够让其中一个元素归位的交换状态,平均而言相当于一层状态数仅扩展了四五倍。
剪枝到了这里已经可以ac了,我们还可以继续剪枝,让状态数再少几倍。也就是每次交换第一种情况的交换状态仅仅入队一个,比如交换1,2和交换3,4都能让两个位置元素归位,如果我们把1,2和3,4交换后的状态都加入队列,相当于后续状态数翻倍了,而交换1,2后下一层状态可以交换3,4,交换3,4后下一轮状态也能交换1,2,既然顺序无所谓,就只需要其中一种状态入队了。
下图是本题的BFS搜索树:
在这里插入图片描述
图中的数字表示每次交换属于第几种情况,2.1表示第二种情况的交换让第一个字符归位的状态,2.2表示第二种情况的交换让第二个字符归位的情况。借助搜索树总结下本题的剪枝操作。
树的第二层:每次交换的两个位置的元素都不能是处于最终位置的元素,剪掉4分支;每次交换至少要让一个字符归位,剪掉3分支;存在一次交换让两个字符归位的状态就仅将该状态入队,忽略其它的状态,剪掉2分支。
树的第三层:存在多个第一种情况的交换,选择其中的一种状态加入队列,剪掉其它状态。
树的第四层:不存在第一种情况的交换状态时,只将能够使得其中一个字符归位的状态加入队列,剪掉其它状态。
要将本题的细节说清楚只能耗费这么多篇幅了,自己总结下几种剪枝的情况还是不难理解的。

代码

class Solution {
public:
    queue<pair<string,int> > q;
    unordered_map<string,bool> st;
    int bfs(string s1,string s2){
        q.push({s1,0});//初始状态和步数入队
        st[s1] = true;
        int n = s1.size();
        while(q.size()){
            auto t = q.front();
            string u = t.first;
            q.pop();
            bool flag = false;//是否存在第一种交换状态
            for(int i = 0;i < n;i++){
                if(u[i] == s2[i] || flag)	continue;//flag为true说明已经加入了一个第一种状态,不再考虑其它状态
                for(int j = i + 1;j < n;j++){
                    if(!flag && u[j] == s2[i] && u[i] == s2[j]){//第一种情况交换判断
                        swap(u[i],u[j]);
                        if(u == s2) return t.second + 1;
                        if(!st.count(u)){//状态之前未入队
                            q.push({u,t.second + 1});
                            st[u] = true;
                        }
                        swap(u[i],u[j]);//恢复状态
                        flag = true;
                    }
                }
            }
            if(flag)    continue;//找到第一种情况的状态就不再考虑第二种情况的状态
            int i = 0;
            while(u[i] == s2[i])    i++;//只考虑归位最先不匹配的字符
            for(int j = i + 1;j < n;j++){
                if(u[j] != s2[j] && u[j] == s2[i] || u[i] == s2[j]){
                    swap(u[i],u[j]);
                    if(u == s2) return t.second + 1;
                    if(!st.count(u)){
                        q.push({u,t.second + 1});
                        st[u] = true;
                    }
                    swap(u[i],u[j]);
                }
            }
        }
        return -1;
    }
    int kSimilarity(string s1, string s2) {
        if(s1 == s2)    return 0;
        return bfs(s1,s2);
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值