BM58-字符串的排列

文章介绍了如何利用递归和回溯方法解决字符串中字符的所有排列问题,要求空间复杂度和时间复杂度均为O(n!)。首先对字符串进行字典序排序,然后通过递归遍历并选择未使用的字符,遇到重复字符时进行回溯,避免重复排列。递归过程中使用vis数组记录字符使用状态,并在回溯时恢复。最后,当临时字符串长度等于原字符串长度时,得到一种排列并加入结果数组。

题目

输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。

例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。

数据范围:n < 10。
要求:空间复杂度 O(n!),时间复杂度 O(n!)。

输入描述:输入一个字符串,长度不超过10,字符只包括大小写字母。

示例1

输入:"ab"

返回值:["ab","ba"]

说明:返回["ba","ab"]也是正确的

示例2

输入:"aab"

返回值:["aab","aba","baa"]

示例3

输入:"abc"

返回值:["abc","acb","bac","bca","cab","cba"]

示例4

输入:""

返回值:[""]


思路:递归+回溯

可参考:

字符串的排列icon-default.png?t=N3I4http://t.csdn.cn/aOxb2

  • 递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。
  • 如果是线型递归,子问题直接回到父问题不需要回溯,但是如果是树型递归,父问题有很多分支,我需要从子问题回到父问题,进入另一个子问题。因此回溯是指在递归过程中,从某一分支的子问题回到父问题进入父问题的另一子问题分支,因为有时候进入第一个子问题的时候修改过一些变量,因此回溯的时候会要求改回父问题时的样子才能进入第二子问题分支。

都是求元素的全排列,字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与有重复项的数字的全排列类似,只是这道题输出顺序没有要求。但是为了便于去掉重复情况,我们还是应该参照数组全排列,优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便。

使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归

  • 终止条件: 临时字符串中选取了n个元素,已经形成了一种排列情况了,可以将其加入输出数组中。
  • 返回值: 每一层给上一层返回的就是本层级在临时字符串中添加的元素,递归到末尾的时候就能添加全部元素。
  • 本级任务: 每一级都需要选择一个元素加入到临时字符串末尾(遍历原字符串选择)。

递归过程也需要回溯,比如说对于字符串“abbc”,如果事先在临时字符串中加入了a,后续子问题只能是"bbc"的全排列接在a后面,对于b开头的分支达不到,因此也需要回溯:将临时字符串刚刚加入的字符去掉,同时vis修改为没有加入,这样才能正常进入别的分支。

具体做法:

  • step 1:先对字符串按照字典序排序,获取第一个排列情况。
  • step 2:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
  • step 3:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过,也不需要将其纳入。
  • step 4:进入下一层递归前将vis数组当前位置标记为使用过。
  • step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素。
  • step 6:临时字符串长度到达原串长度就是一种排列情况。

代码

import java.util.*;

public class Solution {
    public ArrayList<String> Permutation(String str) {
        ArrayList<String> res = new ArrayList<String>();
        if (str == null || str.length() == 0) {
            return res;
        }
        //转字符数组
        char[] charStr = str.toCharArray();
        // 按字典序排序
        Arrays.sort(charStr);
        boolean[] vis = new boolean[str.length()];
        //标记每个位置的字符是否被使用过
        Arrays.fill(vis, false);
        StringBuffer temp = new StringBuffer();
        //递归获取
        recursion(res, charStr, temp, vis);
        return res;
    }

    public void recursion(ArrayList<String> res, char[] str, StringBuffer temp, boolean[] vis) {
        //临时字符串满了加入输出
        if (temp.length() == str.length) {
            res.add(new String(temp));
            return;
        }
        //遍历所有元素选取一个加入
        for (int i = 0; i < str.length; i++) {
            //如果该元素已经被加入了则不需要再加入了
            if (vis[i]) {
                 continue;
            }
            if (i > 0 && str[i - 1] == str[i] && !vis[i - 1]) {
                //当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了
                continue;
            }
            //标记为使用过
            vis[i] = true;
            //加入临时字符串
            temp.append(str[i]);
            recursion(res, str, temp, vis);
            //回溯
            vis[i] = false;
            temp.deleteCharAt(temp.length() - 1);
        }
    }
}

<think>我们正在解决YBT平台上的字符串匹配问题。根据引用[1]和引用[3],我们看到一种方法是通过哈希(Hash)将字符串转化为数值,然后比较数值来间接比较字符串。这种方法被称为字符串哈希,它可以在O(1)的时间内判断两个子串是否相等(在假设没有哈希冲突的情况下),从而优化字符串匹配的效率。 引用[1]提到:“通过把字符转化为数的方法 比较数也就是比较字符了”,这实际上就是字符串哈希的核心思想。 引用[3]则详细介绍了字符串哈希在解决回文串问题中的应用,并提到了如何避免负数以及冲突的问题。 因此,我们可以使用字符串哈希来解决YBT的字符串匹配问题。 ### 字符串哈希的基本原理 1. **预处理哈希前缀和**:对于一个字符串$s$,我们定义其前缀哈希数组$H$,其中$H[i]$表示从$s[0]$到$s[i-1]$(或$s[1]$到$s[i]$,取决于下标从0还是1开始)的子串的哈希值。通常,我们采用多项式哈希: $$H[i] = (H[i-1] \times base + s[i]) \mod mod$$ 其中,$base$是一个大于字符集大小的基数(例如131, 13331等),$mod$是一个大质数(如$2^{64}$,这样可以利用unsigned long long的自然溢出)来减少冲突。 2. **计算子串哈希**:对于子串$s[l..r]$(闭区间),其哈希值可以通过前缀哈希数组计算: $$hash(l, r) = (H[r] - H[l-1] \times base^{r-l+1}) \mod mod$$ 注意:这里假设字符串下标从1开始,且$H[0]=0$。如果下标从0开始,则$l$和$r$需要相应调整。 3. **比较子串**:通过比较两个子串的哈希值,就可以判断它们是否相等。 ### 解决字符串匹配问题 假设我们有两个字符串:文本串$T$和模式串$P$。我们希望在$T$中查找$P$出现的所有位置。 步骤: 1. 预处理模式串$P$的哈希值$hash_P$。 2. 预处理文本串$T$的前缀哈希数组。 3. 遍历$T$中所有长度为$|P|$的子串,计算每个子串的哈希值,并与$hash_P$比较。如果相等,则当前位置匹配成功。 注意:为了避免哈希冲突,我们可以使用双哈希(即使用两个同的基数和模数),但根据题目要求,有时候单哈希(尤其是模$2^{64}$)已经足够。 ### 代码实现(C++) 以下是一个使用字符串哈希解决字符串匹配问题的示例代码(假设字符串下标从1开始): ```cpp #include <iostream> #include <cstring> #include <string> using namespace std; typedef unsigned long long ULL; const int N = 1000010, base = 131; ULL H[N], p[N]; // p[i] = base^i // 计算子串哈希值 ULL get_hash(int l, int r) { return H[r] - H[l-1] * p[r - l + 1]; } int main() { string T, P; cin >> T >> P; int n = T.size(), m = P.size(); // 将字符串调整为下标从1开始 T = " " + T; P = " " + P; // 预处理文本串T的哈希前缀和 p[0] = 1; for (int i = 1; i <= n; i++) { H[i] = H[i-1] * base + T[i]; p[i] = p[i-1] * base; } // 计算模式串P的哈希值 ULL hash_P = 0; for (int i = 1; i <= m; i++) { hash_P = hash_P * base + P[i]; } // 在T中匹配P for (int i = 1; i <= n - m + 1; i++) { if (get_hash(i, i + m - 1) == hash_P) { // 匹配成功,输出位置(注意:题目中通常下标从1开始,所以直接输出i) cout << i << endl; } } return 0; } ``` ### 注意事项 - 在实际问题中,需要注意字符串下标从0开始还是从1开始,上述代码调整为从1开始。 - 使用自然溢出(即模$2^{64}$)可以简化代码,但要注意冲突的可能性。如果题目对正确性要求极高,建议使用双哈希。 - 时间复杂度:预处理$O(n)$,匹配过程$O(n)$,总时间复杂度$O(n+m)$,其中$n$是文本串长度,$m$是模式串长度。 ### 相关问题 1. 字符串哈希在什么情况下会出现冲突?如何降低冲突概率? 2. 除了字符串哈希,还有哪些字符串匹配算法?(如KMP、BM、Sunday等) 3. 如何用字符串哈希解决回文串问题? 4. 在YBT平台上,除了字符串匹配,还有哪些问题可以用字符串哈希解决?</think>### YBT 字符串匹配问题解决方案 针对YBT平台上的字符串匹配问题,核心解决方案是**字符串哈希(String Hashing)**技术。该方法将字符串转化为数值进行比较,避免了直接字符匹配的低效性,时间复杂度可达$O(n)$。以下是具体实现步骤和原理: --- #### 一、字符串哈希原理 1. **预处理哈希值**: - 定义哈希函数:$H[i] = (H[i-1] \times base + s[i]) \mod mod$ - $base$取质数(如131, 13331),$mod$取大质数(如$2^{64}$,利用自然溢出优化) 2. **子串哈希计算**: - 子串$s[l..r]$的哈希值:$hash(l, r) = (H[r] - H[l-1] \times base^{r-l+1}) \mod mod$ 3. **比较机制**: - 直接比较子串哈希值(数值比较替代字符串比较) > 关键优势:$O(1)$时间内比较任意子串[^1][^3]。 --- #### 二、算法实现步骤(C++) ```cpp #include <iostream> #include <string> using namespace std; typedef unsigned long long ULL; const int N = 1e6 + 10, base = 131; ULL H[N], p[N]; // H[i]:前缀哈希值;p[i]:base的i次幂 // 初始化哈希数组 void init_hash(string s) { p[0] = 1; for (int i = 1; i <= s.size(); i++) { H[i] = H[i-1] * base + s[i-1]; p[i] = p[i-1] * base; } } // 获取子串s[l..r]的哈希值 ULL get_hash(int l, int r) { return H[r] - H[l-1] * p[r - l + 1]; } int main() { string T, P; // T:文本串, P:模式串 cin >> T >> P; init_hash(T); // 预处理文本串哈希 ULL hashP = 0; for (char c : P) hashP = hashP * base + c; // 计算模式串哈希 int n = T.size(), m = P.size(); for (int i = 1; i <= n - m + 1; i++) { if (get_hash(i, i + m - 1) == hashP) { cout << "匹配位置: " << i << endl; } } return 0; } ``` --- #### 三、关键优化技巧 1. **避免哈希冲突**: - 使用双哈希(两个同$base$和$mod$) - 特殊字符处理:如回文问题中插入分隔符`?`[^3] 2. **性能提升**: - 自然溢出法:利用`unsigned long long`自动取模$2^{64}$ - 预处理幂数组$p[i]$,避免重复计算 3. **边界处理**: - 字符串下标从1开始($H[0]=0$) > 注:当模式串以通配符开头时(如`LIKE '%abc'`),需调整算法逻辑[^4]。 --- #### 四、YBT平台应用场景 1. **基础字符串匹配**(如ybt 1455) 2. **回文串检测**(通过双向哈希比较) 3. **最长重复子串**(二分+哈希验证) 4. **字典树优化**(结合哈希加速前缀匹配) --- ### 相关问题 1. 字符串哈希的冲突概率如何计算?如何选择最优的$base$和$mod$? 2. 在回文串问题中,为什么要插入特殊字符(如`?`)?如何设计其数值? 3. 相比KMP算法字符串哈希在哪些场景下更具优势? 4. 如何处理带通配符(如`*`或`?`)的字符串匹配问题?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值