背景
一般情况下,我们判断一个字符串中是否存在某个字符,最常用的方法就是遍历字符串进行逐个匹配的方法。
另外,就是利用Map、Set和二叉搜索树这种结构,降低算法的复杂度。
逐个遍历的方法,很明显算法复杂度是线性增长的。利用Map、Set和二叉搜索树来进行辅助的判断,这种数据结构虽然查找效率相对较高,但是本身会占用较大的存储空间。
在不区分大小写的情况下,我们可以利用位运算来辅助进行判断。
主要思想
利用位运算来辅助判断字符是否存在于某个字符串中,主要是的思路是:
在不区分大小写的情况下进行查找,可以先把整个字符串中的大写字母全部转换成小写字母。
因为,英文字母总共有26个,所以,可以用一个整数来做位映射。
如果某个字符出现,则把整数对应的二进制位上的数字置为1,否则置为0.
然后把要查找的字符也映射成一个整数,然后用两个整数来做按位与运算,如果结果等于0,则字符串中不存在该字符,如果结果不等于0,则字符串中存在该字符。
算法示例
我们使用leetcode 1239,作为示例来展示该方法。
题目介绍
给定一个字符串数组 arr,字符串 s 是将 arr 某一子序列字符串连接所得的字符串,如果 s 中的每一个字符都只出现过一次,那么它就是一个可行解。
请返回所有可行解 s 中最长长度。
题目边界条件
-
1 <= arr.length <= 16
-
1 <= arr[i].length <= 26
-
arr[i]
中只含有小写英文字母
思路分析
初看题目,是求最大值一类的问题,以为可以用动态规划来求解这个问题。
我们可以定义一个数组dp,对于数组中的元素dp[i]所代表的含义,在数组arr中范围为[0,i]
范围内的元素,形成的可行解字符串的最大长度。
对于,arr中的任意一个元素arr[i],我们有两种选择,作为可行解,不作为可行解。
如果作为可行解,dp[i] = dp[i-1] + arr[i].length();
如果不作为可行解,dp[i] = dp[i-1];
由于我们的目标是求最大值,所以,最终dp[i] = max(dp[i-1] + arr[i].length(), dp[i-1]);
根据,题目的要求,可行解中的字符是不能重复的,所以,在进行选择的时候,我们还需要处理以下两种情况,
-
arr[i]中有重复的字符
-
arr[i]中的字符与dp[i-1]中的选择的字符有重复
如果,arr[i]本身有重复字符,那是肯定不能的选的。
同理,如果arr[i]与dp[i-1]中的字符有重复,那么也不能选。
分析到此处,我们发现,子问题的选择会影响到更大范围的问题的选择,这样就不具有最优子结构了,因此,个人认为这道题目不可以使用动态规划来求解。
因此,本题,很容易想到的另外一种思路就是回溯了。
代码实现
#include<vector>
#include<string>
using namespace std;
class Solution{
public:
int maxLength(vector<string>& arr){
int n = arr.length();
vector<int> masks(n, 0);
for(int i = 0; i < n; i++){
masks[i] = str2int(arr[i]);
}
return backtrack(masks, arr, 0, 0);
}
private:
int str2int(string str){
int mask = 0;
for(auto char c: str){
if((mask & (1 << (c - 'a'))) == 0){
mask |= (1 << (c - 'a'));
}else{
// 字符串本身存在重复字符串
return 0;
}
}
return mask;
}
// 回溯算法实现
int backtrack(vector<int>& masks, vector<string>& arr, int i, int mask){
if(i == masks.size()) return 0;
if(masks[i] == 0 || (mask & masks[i]) != 0){
// 存在重复字符串
return backtrack(masks, arr, i + 1, mask);
}else{
int a = backtrack(masks, arr, i + 1, mask | masks[i]) + arr[i].length();
int b = backtrack(masks, arr, i + 1, mask);
return max(a, b);
}
}
};
总结
在本文的示例中,由于可行解是由不同的字符组成的字符串,因为小写字母总共有26个,故而可行解最长也就是全部的26个英文字母,所以,可以用一个整数来表示字符对应的位映射结果。
还有需要注意的是,示例看起来可以用动态规划来求解,但是实际上并不可行。
位运算是算法中很常见的一种高效的方法。