算法笔记-lc-854. 相似度为 K 的字符串(困难)

@[TOC](算法笔记-lc-854. 相似度为 K 的字符串(困难))

题目

题干

对于某些非负整数 k ,如果交换 s1 中两个字母的位置恰好 k 次,能够使结果字符串等于 s2 ,则认为字符串 s1 和 s2 的 相似度为 k 。

给你两个字母异位词 s1 和 s2 ,返回 s1 和 s2 的相似度 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 的一个字母异位词

解法

方法一:广度优先搜索

由于题目中给定的字符串的长度范围为 [1,20] 且只包含 66 种不同的字符,因此我们可以枚举所有可能的交换方案,在搜索时进行减枝从而提高搜索效率,最终找到最小的交换次数。

设字符串的长度为 n,如果当前第 i个字符满足 s1[i]!=s2[i],则从s1[i+1,⋯] 选择一个合适的字符 s1[j] 进行交换,其中满足 s1​[j]=s 2​[i],j∈[i+1,n−1]。每次我们进行交换时,可将字符串 s1 的前 xx 个字符通过交换使得s1[0,⋯,x−1]=s 2[0,⋯,x−1],最终使得 s1的所有字符与 s2相等即可。我们通过以上变换,找到最小的交换次数使得 s1与 s2相等。
在搜索时,我们需要进行减枝,我们设当前的通过交换后的字符串 s1′为一个中间状态,用哈希表记录这些中间状态,当通过交换时发现当前状态已经计算过,则此时我们可以直接跳过该状态。

class Solution {
    public int kSimilarity(String s1, String s2) {
        int n = s1.length();
        Queue<Pair<String, Integer>> queue = new ArrayDeque<Pair<String, Integer>>();
        Set<String> visit = new HashSet<String>();
        queue.offer(new Pair<String, Integer>(s1, 0));
        visit.add(s1);
        int step = 0;
        while (!queue.isEmpty()) {
            int sz = queue.size();
            for (int i = 0; i < sz; i++) {
                Pair<String, Integer> pair = queue.poll();
                String cur = pair.getKey();
                int pos = pair.getValue();
                if (cur.equals(s2)) {
                    return step;
                }
                while (pos < n && cur.charAt(pos) == s2.charAt(pos)) {
                    pos++;
                }
                for (int j = pos + 1; j < n; j++) {
                    if (s2.charAt(j) == cur.charAt(j)) {
                        continue;
                    }
                    if (s2.charAt(pos) == cur.charAt(j)) {
                        String next = swap(cur, pos, j);
                        if (!visit.contains(next)) {
                            visit.add(next);
                            queue.offer(new Pair<String, Integer>(next, pos + 1));
                        }
                    }
                }
            }
            step++;
        } 
        return step;
    }

    public String swap(String cur, int i, int j) {
        char[] arr = cur.toCharArray();
        char c = arr[i];
        arr[i] = arr[j];
        arr[j] = c;
        return new String(arr);
    }
}

复杂度分析

该方法时空复杂度分析较为复杂,暂不讨论。

方法二:深度优先搜索

与方法一同样的交换思路,我们也可以采用深度优先搜索来实现,每次遇到不同的字符 s1[i] !=s 2[i] 时,则从s1[i+1,⋯] 中选择处于不同位置的字符s 1[j]=s 2[i],将其与s1[i] 进行交换,然后保持当前子状态,搜索下一个位置 i+1,直到所有字符串s1全部与s2匹配完成;当前子状态搜索完成后,然后恢复字符串,继续搜索下一个与s2[i] 相等的字符,并进行替换即可。在进行深度优先搜索时,由于每个搜索时每个子树的状态都是不同的,所以也可以不用哈希表去重,但是可以用一些特殊的减枝技巧。我们可以得到相似字符串交换次数的上限与下限:

对于长度为 nn 的两个相似的字符串s1,s2。s1最多需要 n−1 次交换变为 s2,因为每进行一次有效交换时,我们可以将 s1 的中的一个字符调整到与s2相同,我们只需要将 s1的前n−1 个字符调整到与s2的前 n−1 个字符相同,则 s1的第 nn 个字符此时也一定与 s2 的第n 个字符相同,因此我们最多需要 n-1 次交换,即可使得s1,s2相等。

对于长度为 n 的两个相似的字符串s1 ,s2 ,且对于字符串中任意位置的字符均满足 s1[i]
!=s2[i]。我们可以观察到此时s1最少需要 n+1次交换变为 s2。每进行一次有效交换时,我们最多可以将 s1的中的两个字符调整到与s2相同。比如当满足s1[i]=s 2[j],s1[j]=s2[i],此时我们交换位置 (i,j),可以将两个字符调整到正确的位置,我们分两种情况进行讨论:
当 n 为偶数时,由于每次交换时最多可以将两个字符同时移动到正确的位置,因此最少需要2n次交换可以使得s1与s2相等。
当 n 为奇数数时,每次交换时最多可以将两个字符同时移动到正确的位置,当最终剩下 3 个字符时,此时我们再交换一次时无法交换两个字符到正确的位置。根据前置条件所有位置的字符均满足s1[i] !=s2[i],假设此时还剩余 3 个字符满足s1[i] !​=s 2[i],s1[j] !=s 2[j],s 1[k] !=s2[k] 时,则此时任意交换一次两个字符使得s1[i]=s2[i],s1[j]=s2[j],还剩余一个字符 s1[k],s2[k] 不相等,这与两个字符串相似矛盾,因此还需 2 次交换才能使得 s1,s2中剩余的 3 个字符相等。因此当 n 为奇数时,最少需要2n+1次交换可以使得 s1与 s2相等。
根据以上结论,我们可以进行如下减枝:
我们只需要计算两个字符s1,s2中同一位置不同的字符的交换次数即可,同一位置相同的字符直接可以跳过。根据之前的结论,假设当前已经通过计算得到的最少交换次数为ans,假设当前字符字符串s1已经过cost 次交换变为了s1′,此时我们计算出字符串s1′变为s2还需要进行交换次数的下限为minSwap(s1′ ),则字符串s1经过交换变为中间状态s1′,然后交换变为 s2所需的交换次数的下限为cur=cost+minSwap(s1′),如果当前最少交换次数下限满足cur≥ans 时,则表明当前的字符串s1′已经不是更优的搜索状态,可直接提前终止搜索。

class Solution {
    int ans;

    public int kSimilarity(String s1, String s2) {
        StringBuilder str1 = new StringBuilder();
        StringBuilder str2 = new StringBuilder();
        for (int i = 0; i < s1.length(); i++) {
            if (s1.charAt(i) != s2.charAt(i)) {
                str1.append(s1.charAt(i));
                str2.append(s2.charAt(i));
            }
        }
        if (str1.length() == 0) {
            return 0;
        }
        ans = str1.length() - 1;
        dfs(0, 0, str1.length(), str1.toString(), str2.toString());
        return ans;
    }

    public void dfs(int pos, int cost, int len, String str1, String str2) {
        if (cost > ans) {
            return;
        }
        while (pos < str1.length() && str1.charAt(pos) == str2.charAt(pos)) {
            pos++;
        }
        if (pos == str1.length()) {
            ans = Math.min(ans, cost);
            return;
        }  
        /* 当前状态的交换次数下限大于等于当前的最小交换次数 */      
        if (cost + minSwap(str1, str2, pos) >= ans) {
            return;
        }
        for (int i = pos + 1; i < str1.length(); i++) {
            if (str1.charAt(i) == str2.charAt(pos)) {
                String str1Next = swap(str1, i, pos);
                dfs(pos + 1, cost + 1, len, str1Next, str2);
            }
        }
    }

    public int minSwap(String s1, String s2, int pos) {
        int tot = 0;
        for (int i = pos; i < s1.length(); i++) {
            tot += s1.charAt(i) != s2.charAt(i) ? 1 : 0;
        }
        return (tot + 1) / 2;
    }

    public String swap(String cur, int i, int j) {
        char[] arr = cur.toCharArray();
        char c = arr[i];
        arr[i] = arr[j];
        arr[j] = c;
        return new String(arr);
    }
}

复杂度分析

该方法时空复杂度分析较为复杂,暂不讨论。

方法三:A* 启发式搜索

本题我们还可以使用A* 启发式搜索,可参考相关A* 算法的基础知识,例如「Wikipedia - A* search algorithm」或 「oi-wiki - A*」,力扣上也可以参考类似题解「752. 打开转盘锁」。

设估计函数为f(x)=g(x)+h(x),其中g(x) 表示起始状态到达状态 x 的实际交换次数,h(x) 为启发函数,在这里我们设h(x) 表示状态 x 到达终态可能的最小交换次数,即方法二中提到的当前状态 x 还需要的交换次数的下限minSwap(x),h(x) 满足小于等于实际的最小步数。实际上我们观察到该启发函数本质为一种贪心策略,在同样的状态下尽可能的选择一次交换 (i,j) 使得s1中两个位置(i,j) 的字符与s2相等,这样才能使得启发函数 h(x) 尽可能的小。

class Solution {
    int n;
    String t;
    int f(String s) {
        int ans = 0;
        for (int i = 0; i < n; i++) ans += s.charAt(i) != t.charAt(i) ? 1 : 0;
        return ans + 1 >> 1;
    }
    public int kSimilarity(String s1, String s2) {
        if (s1.equals(s2)) return 0;
        t = s2;
        n = s1.length();
        Map<String, Integer> map = new HashMap<>();
        PriorityQueue<String> pq = new PriorityQueue<>((a,b)->{
            int v1 = f(a), v2 = f(b), d1 = map.get(a), d2 = map.get(b);
            return (v1 + d1) - (v2 + d2);
        });
        map.put(s1, 0);
        pq.add(s1);
        while (!pq.isEmpty()) {
            String poll = pq.poll();
            int step = map.get(poll);
            char[] cs = poll.toCharArray();
            int idx = 0;
            while (idx < n && cs[idx] == t.charAt(idx)) idx++;
            for (int i = idx + 1; i < n; i++) {
                if (cs[i] != t.charAt(idx) || cs[i] == t.charAt(i)) continue;
                swap(cs, idx, i);
                String nstr = String.valueOf(cs);
                swap(cs, idx, i);
                if (map.containsKey(nstr) && map.get(nstr) <= step + 1) continue;
                if (nstr.equals(t)) return step + 1;
                map.put(nstr, step + 1);
                pq.add(nstr);
            }
        }
        return -1; // never
    }
    void swap(char[] cs, int i, int j) {
        char c = cs[i];
        cs[i] = cs[j];
        cs[j] = c;
    }
}

复杂度分析

启发式搜索不讨论时空复杂度。

方法四:动态规划

该解法思维难度较大且时间复杂度较高,可作为参考题解,同样的解题思路可以参考「zj-future04. 门店商品调配」。

解法二中提到过长度为 nn 两个相似的字符串的最大交换次数为n−1,最小的交换次数为
2n+1。我们可以观察一下什么样的字符串交换次数为n−1,比如长度以下字符串:“ab",“ba",“abcd", “bcda"在上述字符中无法如何交换,每次均只能将一个字符调整到位,因此需要的交换次数为 n-1n−1。对于部分字符串需要的交换次数少于 n−1,比如下列字符串:
“abcdef”, “ecdbfa"
“acd", “cad"
“abcd", “dcba"

对于字符串 s=“abcdef" 可以拆分为两个分别与字符串 “ecdbfa" 的子串 “cdb_“, “e___fa” 相似的字符串p = “a___ef “, q = “bcd_”。设 ks(s) 表示字符串 s 的转换为目标字符串的最小交换次数,则ks(s)=ks§+ks(q)=2+2。我们可以看到字符串“abcdef" 的交换次数也即等于相同位置相似的子串的交换次之和,(q)ks(t)=ks§+ks(q)。设初始值ks(t)=len(t)−1,其中len(t) 表示字符串 t 的长度,ks§+ks(q)=len§+len(q)−2=len(t)−2。我们可以观察到字符串 t 每进行一次相似子字符串拆分,则其交换次会减 1,字符串 s 可拆分为的相似的子串的个数越多,则其交换次数最小。我们设字符串 s 可以被拆分成 k 个相似的子字符串,长度分别为C1 ,C2,⋯,Ck,则 ks(s)=
i=1

k
​(Ci −1)=len(s)−k,由此只需求出字符串可拆分的最大次数即可求出最小交换步数。

字符串相似:即两个字符串中含有的字符和数量完全相等。我们应当将字符串尽可能的拆分成相似的子串,直到不能拆分为止。求字符串的最小交换次数则转换为求该字符串最多的相似子字符串的拆分次数。如果字符串不可拆分,则该字符串的最小交换次数即为字符串的长度减 1。

因此我们可以使用动态规划来解决这个问题,令 dp(s) 表示字符串 s 最多拆分为相似子字符串 t 的数目,如果 ss 不能继续拆分,则令dp(s)=1。枚举 s 的所有相似的子串 t,状态转移方程为dp(s)=max(dp(t)+dp(s−t))。由于题目中字符的长度串最多为 20,为了计算方便使用位图来表示字符串 s 的子串,如果字符串 t为 s 的子串,则一定满足(s&t)=t,对于字符串 t 我们枚举其所有的子集即可。当然可以直接遍历所有子串的子集,则时间复杂度为 3^n,在题目给定的测试用例下会超时。此时需要进行减枝以降低时间复杂度,减枝技巧如下:

由于子串中只含有 6 种字符,因此长度大于 6的字符串则其一定可以拆分为长度小于等于 6 的相似子串,此时字符串中一定含有相同的字符。我们可以将所有长度大于 6的字符串删选出来,依次尝试将其拆分出一个长度小于等于 6 的子字符串。

通过筛选,只筛选出所有相似的子字符串,对于非相似的字符串直接忽略,因为只有相似的字符串才可以拆分成相似的子串。

对于所有筛选出来的相似的子字符串按照字符的个数进行排序,这样就能保证字符的子串一定排列在前面而保证最优的子状态现行进行计算。对于题目中给定的字符串 s1一定是与s2相似的,我们求出s1进行最多的拆分次数即可,最终返回结果即为len(s1)−dp(s1)。
从图论的角度来分析相似字符串,设相似字符串s1,s2 ,我们用有向图来表示相似字符串,每个字符为有向图中的一个节点,s1中的字符指向 s2中同一个位置的字符表示一条有向边s1[i]→s2[i],则该有向图一定由多个环组成,且每个节点都在环上。我们进行一次“有效”的字符交换(即将其中一个字符交换到最终位置),等价于把有向图中两条首尾相连的边变成一条新边和被一个节点的自环。我们最终的目标是把 s1中的所有字符都变成自环。一个长度为 k 的环则我们需要 k−1 次交换才能把所有的节点都变为自环。设长度为 n 的字符串s1可以拆分为 m 个环,则此时需要的有效交换次数为 n−m。因此,求最少的交换次数即等价于 s1拆分成环的最大数目。

比如相似字符为:

s1​=“abcdef",s2 =“bcdefa"
将其交换一次后则变为:
s1=“fbcdea",s 2=“bcdefa"

如下图所示可以看到交换后被拆分为一个新的环和一个字符的自环。
在这里插入图片描述

由于此时字符串只包含 6 种不同的字符,因此长度超过 6 的字符串构成的有向图一定含有入度和出度大于 1 的节点,则此时该有向图一定可以拆分为多个环。

class Solution {
    public int kSimilarity(String s1, String s2) {
        StringBuilder str1 = new StringBuilder();
        StringBuilder str2 = new StringBuilder();
        for (int i = 0; i < s1.length(); i++) {
            if (s1.charAt(i) != s2.charAt(i)) {
                str1.append(s1.charAt(i));
                str2.append(s2.charAt(i));
            }
        }
        int n = str1.length();
        if (n == 0) {
            return 0;
        }
        List<Integer> smallCycles = new ArrayList<Integer>();
        List<Integer> largeCycles = new ArrayList<Integer>();
        for (int i = 1; i < (1 << n); i++) {
            int[] cnt = new int[6];
            for (int j = 0; j < n; j++) {
                if ((i & (1 << j)) != 0) {
                    cnt[str1.charAt(j) - 'a']++;
                    cnt[str2.charAt(j) - 'a']--;
                }
            }
            boolean isCycle = true;
            for (int j = 0; j < 6; j++) {
                if (cnt[j] != 0) {
                    isCycle = false;
                    break;
                }
            }
            if (isCycle) {
                int size = Integer.bitCount(i);
                if (size <= 6) {
                    smallCycles.add(i);
                } else {
                    largeCycles.add(i);
                }
            }
        }
        Collections.sort(smallCycles, (a, b) -> Integer.bitCount(a) - Integer.bitCount(b));
        Collections.sort(largeCycles, (a, b) -> Integer.bitCount(a) - Integer.bitCount(b));
        int[] dp = new int[1 << n];
        Arrays.fill(dp, 1);
        dp[0] = 0;
        for (int i = 0; i < smallCycles.size(); i++) {
            for (int j = 0; j < i; j++) {
                int x = smallCycles.get(i), y = smallCycles.get(j);
                if ((x & y) == y) {
                    dp[x] = Math.max(dp[x], dp[y] + dp[x ^ y]);
                }
            }
        }
        for (int x : largeCycles) {
            for (int y : smallCycles) {
                if ((x & y) == y) {
                    dp[x] = Math.max(dp[x], dp[y] + dp[x ^ y]);
                }
            }
        }
        return n - dp[(1 << n) - 1];
    }
}

复杂度分析

时间复杂度:O(2n×∣Σ∣+ 3n),其中 n为字符串的长度,∣Σ∣ 表示字符集,在此题中字符集为 ‘a’,‘b’,‘c’,‘d’,‘e’,‘f’,本题中 ∣Σ∣=6。需要遍历并检测所有可能成环的子串,需要的时间为O(2n×∣Σ∣),检测每个环的最小交换次数需要的时间上限为 O(3n),因此时间复杂度为 O(2n ×∣Σ∣+3n )。

空间复杂度:O(2n),其中 n 为字符串的长度。需要记录字符串所有的子串的状态,因此需要的存储空间为 2^n 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Sure! Here's your code with comments added: ```matlab F = zeros(length(z), 1); % Initialize the F vector with zeros for i = 1:length(z) % Define the Phi function using anonymous function Phi = @(theta, R, r) (z(i) + lc - lm) .* r.R .(R - r.sin(theta)) ./ ... ((R.^2 + r.^2 - 2*R.*r.*sin(theta)).sqrt(R.^2 + r.^2 + (z(i) + lc - lm).^2 - 2*R.*r.*sin(theta))) + ... (z(i) - lc + lm) .* r.R .(R - r.sin(theta)) ./ ... ((R.^2 + r.^2 - 2*R.*r.*sin(theta)).sqrt(R.^2 + r.^2 + (z(i) - lc + lm).^2 - 2*R.*r.*sin(theta))) + ... (z(i) + lc + lm) .* r.R .(R - r.sin(theta)) ./ ... ((R.^2 + r.^2 - 2*R.*r.*sin(theta)).sqrt(R.^2 + r.^2 + (z(i) + lc + lm).^2 - 2*R.*r.*sin(theta))) + ... (z(i) - lc - lm) .* r.R .(R - r.sin(theta)) ./ ... ((R.^2 + r.^2 - 2*R.*r.sin(theta)).sqrt(R.^2 + r.^2 + (z(i) - lc - lm).^2 - 2*R.*r.sin(theta))); % Calculate the value of F(i) using the integral3 function F(i) = BrNI / (4 * lc * (Rc - rc)) * integral3(Phi, 0, 2*pi, rc, Rc, rm, Rm); end ``` This code calculates the values of the vector `F` using a loop. The `Phi` function is defined as an anonymous function that takes `theta`, `R`, and `r` as input parameters. It performs a series of calculations and returns a value. The integral of `Phi` is then calculated using the `integral3` function. The result is stored in the corresponding element of the `F` vector. Please note that I have made some assumptions about the variables and functions used in your code since I don't have the complete context. Feel free to modify or clarify anything as needed.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值