【Leetcode】854. K-Similar Strings(配数学证明)

题目地址:

https://leetcode.com/problems/k-similar-strings/

给定两个异位词 s 1 s_1 s1 s 2 s_2 s2,长度都是 n n n,允许将 s 1 s_1 s1中某两个字符交换位置,问至少交换多少次可以使得 s 1 s_1 s1变成 s 2 s_2 s2

其实在问该(含重复元素)置换最少可以分解为多少个对换之积。遍历两个单词,同时建图,将每个字母看成一个顶点,对应位置的字母 s 1 [ i ] s_1[i] s1[i]连一条指向字母 s 2 [ i ] s_2[i] s2[i]的边,这样就建出了一个图,这个图可能有自环和平行边。

我们首先考虑一个类似大环的图(即形如 a → b → c → a a\to b\to c\to a abca这种有向图),那么其实它对应的就是一个轮换(本质上是 s 1 = a b c , s 2 = b c a s_1=abc,s_2=bca s1=abc,s2=bca),对于一个轮换,由群论相关结论,其可以拆成若干对换的乘积,并且对换个数最少是轮换长度减 1 1 1(这个定理的证明是这样的,对于一个长 k k k的轮换,经过若干次对换要变成恒等映射,那么恒等映射相当于是 k k k个长度为 1 1 1的轮换。我们考虑对换次数。首先 k − 1 k-1 k1次是可以做到的,因为有轮换分解 ( 123... n ) = ( 12 ) ( 13 ) ( 14 ) . . . ( 1 n ) (123...n)=(12)(13)(14)...(1n) (123...n)=(12)(13)(14)...(1n);其次我们证明少于 k − 1 k-1 k1次就无法做到了,这是因为做一次对换,容易观察出,如果对换换的是同一个轮换内部的两个元素,则该轮换被拆成了两个轮换;如果对换换的不是同一个轮换内部的两个元素,那么那两个轮换就会合成一个轮换。即每次最多能将轮换数加 1 1 1,所以 1 1 1个轮换要变成 k k k个轮换至少得做 k − 1 k-1 k1次对换。在图上,轮换其实就是交换了两条边的入点)。还有的一个性质是,一个长 k k k的轮换要拆成 k k k个长 1 1 1的轮换,每次只要保证是在对换某个轮换内部的两个点就行了,无论换谁,怎么换,最终都会达到相同的效果,并且次数也是一样的。

这道题的麻烦之处在于某个字母可能出现多次,即该置换是含重复元素的。所以我们需要枚举将整个图看成哪几个大环的所有方式,然后用暴搜的方式搜到对换数最小的方案。对于那些只有一个出边的点,这种点属于哪个环是毫无疑问的,所以真正需要枚举的只在某个顶点 u u u有多个出边的时候发生。由于对于某个环(轮换)来说,只要是在环内部做对换,怎么做对换都是可以的,但是由于 u u u有多条出边,怎么“看”有哪些环的方式,是不唯一的。例如对于两个字符串 s 1 = a b c b d , s 2 = b c a d b s_1=abcbd,s_2=bcadb s1=abcbd,s2=bcadb b b b有两条出边,因此将其拆为两个点 b 1 b_1 b1 b 2 b_2 b2,那么建图之后可以视为两个环 a → b 1 → c → a , b 2 → d → b 2 a\to b_1\to c\to a, b_2\to d\to b_2 ab1ca,b2db2,也可以视为是一个环 a → b 1 → d → b 2 → c → a a\to b_1\to d\to b_2\to c\to a ab1db2ca,那么这两种“看待”环的方式,会导致总对换次数不一样。看成第一种方式,需要 3 − 1 + 2 − 1 = 3 3-1+2-1=3 31+21=3次对换,看成后者,需要 4 4 4次对换。所以我们就需要枚举所有的“看法”。枚举看法,实际上就是在枚举出边,由于“看法”确定的情况下,次数也是确定的,所以可以直接先将多个出边的点拆出来成为自环,然后继续暴搜。比如对于第一种看法,枚举出边 b 1 → c b_1\to c b1c,则得三个环 a → c → a , b 1 → b 1 , b 2 → d → b 2 a\to c\to a, b_1\to b_1, b_2\to d\to b_2 aca,b1b1,b2db2,而若枚举出边 b 1 → d b_1\to d b1d,则得两个环 a → d → b 2 → c → a , b 1 → b 1 a\to d\to b_2\to c\to a, b_1\to b_1 adb2ca,b1b1

在题目里,暴搜的实际上是 s 1 = a b c b d , s 2 = b c a d b s_1=abcbd,s_2=bcadb s1=abcbd,s2=bcadb在第一个位置不匹配的时候,应该把哪个 b b b换过去。换哪一个,就决定了一种“看环”的看法。那么DFS搜索的深度,就是步数。由于我们要计算最少步数,所以需要用迭代加深。而本题的启发函数是很好写的,当前所得的字符串和 s 2 s_2 s2若有 x x x个字母不同的位置,由于每次对换最多消去 2 2 2个不同位置,所以最少还需要 ⌈ x / 2 ⌉ \lceil x/2\rceil x/2这么多次对换,启发函数可以设为 h ( s ) = ⌈ x / 2 ⌉ h(s)=\lceil x/2\rceil h(s)=x/2,于是整个算法事实上是一个IDA*算法(可以参考https://blog.csdn.net/qq_46105170/article/details/115594149)。代码如下:

import java.util.Arrays;

public class Solution {
    public int kSimilarity(String s1, String s2) {
        char[] chs1 = s1.toCharArray(), chs2 = s2.toCharArray();
        int depth = 0;
        while (!dfs(depth, 0, chs1, chs2)) {
            depth++;
        }
        
        return depth;
    }
    
    private boolean dfs(int depth, int pos, char[] s1, char[] s2) {
    	// 搜到最大深度了,就不往下搜了,直接判断是否找到了一个解
        if (depth == 0) {
            return Arrays.equals(s1, s2);
        }
        
        // 如果当前深度加上启发函数大于了最大深度,则本次搜索不可能有解,返回false
        if (h(s1, s2) > depth) {
            return false;
        }
        
        // pos前面的位置已经配对好了,直接从pos开始搜
        for (int i = pos; i < s1.length; i++) {
        	// 略过自环,这些环不需要拆
            if (s1[i] != s2[i]) {
            	// 枚举出边,每条出边就对应着环的一种“看法”
                for (int j = i + 1; j < s1.length; j++) {
                    if (s1[j] == s2[i]) {
                    	// 尝试换过去,图里对应着拆出一个自环
                        swap(s1, i, j);
                        // 继续dfs,深度加1,并且位置从i + 1开始搜,如果能搜到解则返回true
                        if (dfs(depth - 1, i + 1, s1, s2)) {
                            return true;
                        }
                        // 搜不到解则恢复现场
                        swap(s1, i, j);
                    }
                }
                // 对换的次序是无关的,只要拆出一个自环即可,所以找到了第一个拆环的位置搜下去,
                // 如果没找到答案就可以退了,因为之后搜也不可能有答案
                break;
            }
        }
        
        return false;
    }
    
    // 启发函数
    private int h(char[] s1, char[] s2) {
        int res = 0;
        for (int i = 0; i < s1.length; i++) {
            if (s1[i] != s2[i]) {
                res++;
            }
        }
        
        return res + 1 >> 1;
    }
    
    private void swap(char[] s, int i, int j) {
        char tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
    }
}

时间复杂度指数级,但IDA*实际时间复杂度不会很大,空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值