1. 问题理解与解法思路
这道题目要求我们在给定的字符串数组中,找到两个不包含相同字符的字符串,使它们的长度乘积最大化。乍一看似乎需要比较所有字符串对的字符重合情况,但直接暴力比较的时间复杂度会达到O(n²×L),其中n是字符串数量,L是字符串平均长度,这在数据量较大时显然不够高效。
这里引入了一个巧妙的位运算技巧:用整数的二进制位来表示字符串包含的字母情况。具体来说:
- 每个字母可以映射到一个特定的二进制位(比如a对应第0位,b对应第1位,...,z对应第25位)
- 对于一个字符串,我们遍历它的每个字符,将对应的二进制位置为1
- 这样每个字符串都可以表示为一个26位的二进制数(在代码中通常用int类型存储)
通过这种表示方法,判断两个字符串是否有相同字符就变得非常简单:只需要对它们的二进制表示进行按位与运算,结果为0就说明没有相同字符。
2. 位掩码的构建与优化
2.1 位掩码生成
在代码中,我们通过以下方式生成每个字符串的位掩码:
cpp复制int mask = 0;
for (char c : word) {
mask |= 1 << (c - 'a');
}
这段代码的工作原理是:
- 初始化mask为0(所有位都是0)
- 对于字符串中的每个字符,计算它相对于'a'的偏移量(0-25)
- 将1左移这个偏移量位,得到一个只有对应字母位为1的数字
- 用按位或操作将这个位设置到mask中
例如:
- 字符串"ab"的mask是
(1<<0) | (1<<1) = 3(二进制11) - 字符串"ac"的mask是
(1<<0) | (1<<2) = 5(二进制101)
2.2 哈希表优化
为了进一步提高效率,代码中使用了一个哈希表来记录每个位掩码对应的最大字符串长度:
cpp复制unordered_map<int, int> hash;
// ...
hash[mask] = max(hash[mask], size);
这样做的目的是:
- 对于具有相同字符组成的字符串(即mask相同),我们只需要保留长度最大的那个
- 因为题目要求的是最大乘积,相同字符组成的字符串中,长度更大的显然更有潜力产生更大的乘积
3. 核心算法实现
3.1 双重循环比较
算法的核心部分是一个双重循环:
cpp复制for (const auto& pair : hash) {
int h_mask = pair.first;
int h_len = pair.second;
if (!(mask & h_mask)) {
ans = max(ans, size * h_len);
}
}
这个循环的工作原理是:
- 外层循环遍历所有字符串,为每个字符串生成mask
- 内层循环遍历哈希表中已有的所有mask
- 对于每一对mask,使用按位与运算检查是否有重叠字符
- 如果没有重叠(结果为0),则计算长度乘积并更新最大值
3.2 复杂度分析
- 时间复杂度:O(n×L + n×k),其中n是字符串数量,L是平均字符串长度,k是不同mask的数量
- 第一部分是生成所有mask的代价
- 第二部分是比较所有mask对的代价
- 空间复杂度:O(k),用于存储不同mask及其最大长度
在实际应用中,k通常远小于n(因为很多字符串可能有相同的字符组成),所以这个算法比暴力解法高效得多。
4. 代码实现细节
4.1 完整代码解析
cpp复制#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
using namespace std;
int maxProduct(vector<string>& words) {
unordered_map<int, int> hash;
int ans = 0;
for (const string& word : words) {
int mask = 0;
int size = word.size();
// 生成字母位掩码 a-z 对应 0-25 位
for (char c : word) {
mask |= 1 << (c - 'a');
}
// 保留当前掩码对应的最大长度
hash[mask] = max(hash[mask], size);
// 遍历哈希表寻找无重叠字符的字符串
for (const auto& pair : hash) {
int h_mask = pair.first;
int h_len = pair.second;
// 无公共字母
if (!(mask & h_mask)) {
ans = max(ans, size * h_len);
}
}
}
return ans;
}
int main() {
vector<string> words = { "a", "ab", "abc", "d", "cd", "bcd", "abcd" };
cout << maxProduct(words) << endl;
return 0;
}
4.2 关键点说明
- 位运算优先级:注意
1 << (c - 'a')中的括号是必要的,因为减法的优先级低于位移运算 - 哈希表更新:
hash[mask] = max(hash[mask], size)确保我们总是保存当前mask对应的最大长度 - 无重叠判断:
!(mask & h_mask)是核心判断条件,按位与结果为0表示没有重叠字符
5. 测试用例与边界情况
5.1 示例测试
给定输入:["a", "ab", "abc","d", "cd", "bcd", "abcd"]
- "a"和"d":长度乘积1×1=1
- "ab"和"cd":长度乘积2×2=4(最大)
- "abc"和"d":长度乘积3×1=3
- 其他组合要么有重叠字符,要么乘积更小
5.2 边界情况
- 空输入:应该返回0
- 单个字符串:无法形成对,应该返回0
- 所有字符串都有重叠字符:应该返回0
- 包含大写字母:题目通常假设只有小写字母,如果有大写字母需要预处理
- 超长字符串:确保不会因为长度乘积导致整数溢出
6. 算法优化空间
6.1 预排序优化
可以先按字符串长度降序排序,这样可以在找到较大的乘积后提前终止一些比较:
cpp复制sort(words.begin(), words.end(), [](const string& a, const string& b) {
return a.size() > b.size();
});
6.2 位掩码预处理
如果字符串集合固定且需要多次查询,可以预先计算所有mask并存储,避免重复计算。
6.3 并行计算
对于大规模数据,可以将字符串分组并行计算mask,最后合并结果。
7. 类似题目扩展
- 判断两个字符串是否有相同字符:直接使用位掩码按位与
- 找出所有不重叠的字符串对:类似本题但需要记录所有满足条件的对
- 统计字符串集合中的唯一字符:可以用位掩码的按位或操作
- 寻找包含特定字符集合的字符串:用位掩码和按位与操作判断
8. 实际应用场景
这种位掩码技巧在以下场景很有用:
- 快速集合运算:当元素范围有限(如26个字母)时,可以用整数表示集合
- 权限系统:用二进制位表示不同权限,快速检查权限组合
- 特征匹配:在模式识别中快速比较特征集合
- 游戏开发:快速判断状态组合或碰撞检测
9. 常见错误与调试技巧
9.1 常见错误
- 忘记初始化mask:导致随机值影响结果
- 位移位数错误:如
1 << (c - 'a' + 1)会错位 - 哈希表更新顺序错误:应该在比较后再更新当前mask
- 整数溢出:当字符串很长时,乘积可能超出int范围
9.2 调试技巧
- 打印位掩码:用
bitset<26>(mask).to_string()查看二进制表示 - 验证小案例:手动计算几个简单例子的mask和结果
- 边界测试:专门测试空输入、单个字符串等情况
- 性能分析:对于大数据集,分析各部分耗时
10. 个人实现心得
在实际实现这类位运算技巧时,有几个关键点需要注意:
- 清晰的位映射定义:明确每个位代表什么含义,最好有注释说明
- 运算符优先级:位运算的优先级容易混淆,适当使用括号
- 测试全面性:特别注意边界情况和特殊输入
- 性能权衡:虽然位运算很快,但也要考虑内存占用和算法复杂度
对于这道题目,位掩码的技巧将O(n²×L)的复杂度优化到了接近O(n²),在字符串数量较多时提升非常明显。这种用空间(二进制位表示)换时间(快速比较)的思路在很多算法问题中都很有用。