<think>题目要求:给定N个字符串(互不相同),求一个最短的字符串,使得这N个字符串都是它的子串。
注意:N最大为10,每个字符串长度最多为10000。
思路:
由于N很小(最多10),我们可以使用状态压缩动态规划(DP)来解决。但是,首先我们需要处理字符串之间的重叠部分,因为一个字符串可能包含另一个字符串,或者两个字符串可以重叠合并。
步骤:
1. 预处理:去除被其他字符串包含的字符串。例如,如果短语A是短语B的子串,那么只要B出现在最终字符串中,A也一定出现了。因此,我们可以先过滤掉那些是其他字符串子串的字符串。
2. 计算每对字符串之间的重叠长度。定义overlap[i][j]表示字符串j接在字符串i后面时,能够重叠的最大长度(即字符串i的后缀与字符串j的前缀的最大匹配长度)。
3. 使用状态压缩DP:状态dp[mask][i]表示已经使用了mask代表的字符串集合(mask的二进制每一位代表一个字符串是否被使用),且最后一个字符串是第i个字符串时,当前拼接字符串的最小长度。
4. 状态转移:对于当前状态(mask, i),尝试加入一个尚未使用的字符串j,那么新的状态是(mask | (1<<j), j),新的长度 = 当前长度 + (len(j) - overlap[i][j])。
5. 初始状态:对于每个字符串i,dp[1<<i][i] = len(i)。
6. 结果:取所有状态mask为全1(即(1<<n)-1)时的最小dp值,即为最短超级串的长度。
但是,我们还需要注意:在第一步中,我们需要先过滤掉那些被其他字符串包含的字符串,因为如果字符串A是字符串B的子串,那么我们只需要考虑字符串B即可。
另外,计算重叠长度overlap[i][j]的方法:对于字符串i和j,我们尝试从min(len(i), len(j))开始递减,看字符串i的后k个字符是否等于字符串j的前k个字符,找到最大的k即可。
然而,由于字符串长度可能达到10000,所以我们需要一个高效的方法来计算重叠长度。我们可以使用字符串哈希(例如,双哈希避免冲突)来快速比较两个子串是否相等。
具体步骤:
1. 预处理:去除冗余字符串(被其他字符串包含的字符串)。
2. 对剩下的字符串,计算两两之间的重叠长度(使用哈希优化)。
3. 使用状态压缩DP求解最短超级串长度。
注意:由于n最大为10,所以状态总数为2^10=1024,然后每个状态最多有10个选择,所以状态转移次数为1024*10*10=102400,这是可以接受的。
实现细节:
- 去除冗余字符串:遍历所有字符串,如果字符串i不是字符串j的子串(i≠j),则保留;如果存在一个j使得i是j的子串,则移除i。注意,这里需要小心:当两个字符串相同时,我们只保留一个(但题目保证字符串互不相同),所以只需要考虑包含关系。
- 计算重叠长度:对于两个字符串s和t,我们想要计算s的后缀与t的前缀的最大匹配长度。使用哈希可以在O(min(len(s),len(t)))时间内完成。但是,我们可以用更高效的方法:枚举重叠长度k(从0到min(len(s),len(t))),然后检查s[len(s)-k:]和t[0:k]是否相等。但这样最坏情况是O(min(len(s),len(t))^2)。我们可以用哈希来优化:预处理字符串s和t的哈希值,然后二分查找最大重叠长度?或者直接线性从大到小枚举?但是线性从大到小枚举最坏情况还是O(min(len(s),len(t))),而总共有O(n^2)对,所以总时间复杂度O(n^2 * L)(L为字符串平均长度),对于n<=10,L<=10000,最坏情况10*10*10000=1e6,可以接受。
但为了效率,我们使用哈希来比较两个子串是否相等,这样可以在O(1)时间内比较一次。我们预先计算每个字符串的前缀哈希值。
哈希函数:使用双哈希避免冲突。
哈希1:base1=131, mod1=10**9+7
哈希2:base2=131, mod2=10**9+9
然后对于字符串s,计算两个哈希数组hash1, hash2,以及对应的幂数组power1, power2。
那么子串s[l:r]的哈希值可以快速计算。
但实际上,对于两个字符串s和t,我们比较s的后缀和t的前缀,我们可以这样:
令len = min(len(s), len(t))
从k=len递减到1,如果s的后k个字符的哈希值等于t的前k个字符的哈希值,则k就是重叠长度。
这样最坏情况还是O(len),但平均可能更快(因为我们从大到小,一旦找到就停止)。但最坏情况还是O(len)。
所以总的时间复杂度为O(n^2 * L),其中L为字符串的平均长度,n为去除冗余后的字符串数量(不超过10)。最坏情况10*10*10000=1000000,可以接受。
步骤总结:
1. 读取输入,存储在数组words中。
2. 去除冗余:构建一个新数组non_redundant,遍历words,对于每个字符串word,检查是否存在另一个字符串w(非word)包含word。注意:如果word被某个w包含(即word是w的子串),则跳过word。否则,加入non_redundant。
3. 令m = non_redundant数组的大小(m<=n,因为可能去除了冗余)。
4. 如果m==0,则返回0(但至少有一个字符串,所以m至少为1?因为n>=1)。
5. 计算non_redundant中每对字符串的overlap矩阵(大小为m x m),其中overlap[i][j]表示将字符串j接在字符串i后面时的最大重叠长度(即字符串i的后缀与字符串j的前缀的最大匹配长度)。
6. 初始化dp数组:dp[1<<i][i] = len(non_redundant[i])
7. 状态转移:枚举所有状态mask(0到(1<<m)-1),对于每个状态,枚举最后一个字符串i(如果mask的第i位为1),然后枚举下一个字符串j(如果mask的第j位为0),则新状态为mask|(1<<j),新长度 = dp[mask][i] + len(non_redundant[j]) - overlap[i][j]。更新dp[new_mask][j]。
8. 最终结果:ans = min{ dp[(1<<m)-1][i] for i in range(m) }
但是,我们也可以使用另一种DP:dp[mask][i]表示已经使用了mask中的字符串,且最后一个字符串是i时,超级串的最小长度。然后状态转移方程为:
dp[mask][j] = min_{i in mask} { dp[mask without j][i] + (len(j) - overlap[i][j]) }
9. 输出ans。
注意:我们也可以使用TSP(旅行商)问题的解法,因为这个问题类似于TSP(每个字符串是一个节点,边的权值为len(j)-overlap[i][j])。
但是,我们还没有考虑超级串的起始情况:初始状态,我们取一个字符串,然后不断拼接。
另外,我们需要实现字符串哈希来快速比较子串。这里我们使用双哈希。
具体代码步骤:
1. 读取n和n个字符串。
2. 去除冗余:这里注意,如果一个字符串是另一个字符串的子串,则删除这个字符串。注意:要避免自己和自己比较。
例如:words = ["abc", "abcd", "bc"] -> 去除"abc"和"bc",因为"abc"是"abcd"的子串,"bc"也是"abcd"的子串。
所以non_redundant中只剩下"abcd"。
实现:对字符串按长度降序排序(这样长的字符串先被加入,短的字符串在检查时如果被长的包含则跳过)。然后遍历每个字符串,检查它是否被non_redundant中已有的字符串包含?但是注意:non_redundant中已有的字符串都是不相互包含的(长的在前面,短的如果被包含则跳过)。但是,也有可能一个短字符串没有被任何已有字符串包含,那么它也要加入。所以:
non_redundant = []
将原字符串数组按长度从大到小排序
for word in sorted_words:
found = False
for s in non_redundant:
if word in s: # 这里可以用KMP或者哈希判断子串,但Python中可以直接用in运算符(因为in运算符对于字符串是高效的?最坏情况O(len(s)))
found = True
break
if not found:
non_redundant.append(word)
注意:这里使用in操作,最坏情况是O(len(s)*len(word)),但总的时间复杂度为O(n^2 * L^2)?因为non_redundant最多有n个字符串,每个字符串长度最大为10000,n最大10,所以最坏情况10*10*10000*10000=1e10,不可接受。
因此,我们需要更高效的方法。我们可以使用字符串哈希来快速判断一个字符串是否是另一个字符串的子串。具体方法:对non_redundant中的每个字符串t,我们预先计算其哈希值(整个字符串的哈希值不够,需要前缀哈希数组),然后对于word,计算它的哈希值,然后用滑动窗口在t中匹配?但是这样还是需要O(len(t))。总的时间复杂度还是O(n^2 * L),因为每个word要匹配non_redundant中的每个字符串(最多10个),每个字符串长度最多10000,所以10*10*10000=1e6,可以接受(因为每个word匹配一个字符串t,我们使用哈希可以在O(len(t))内完成,因为要计算word的哈希,然后遍历t中每个长度为len(word)的子串,比较哈希值)。但这样对于每个word和每个non_redundant中的字符串t,需要O(len(t))时间。总时间O(m*n*L)(m为non_redundant当前大小,最多10;n为原字符串个数,最多10;L为字符串长度,最多10000)。所以总时间10*10*10000=1e6。
但是,我们可以在构建non_redundant时,一边添加一边构建每个字符串的哈希表(前缀哈希数组)。这样,当添加一个新的字符串word时,我们检查non_redundant中已有的每个字符串t是否包含word(即word是否是t的子串)或者word是否包含t(因为word可能更长)?注意,我们按长度降序排序,所以新加入的word长度小于等于已有字符串,所以我们只需要检查word是否是已有字符串的子串。
具体步骤:
non_redundant = []
sorted_words = sorted(words, key=lambda x: len(x), reverse=True)
for word in sorted_words:
flag = False
for t in non_redundant:
# 检查word是否是t的子串
if is_substring(t, word): # 如果t包含word,那么word是冗余的
flag = True
break
if not flag:
non_redundant.append(word)
is_substring(t, word)函数:判断word是否是t的子串。使用双哈希方法:
预处理t的前缀哈希(如果之前没处理过,就处理一下;我们可以预先将non_redundant中的每个字符串都计算好前缀哈希并存储起来,但是这里non_redundant是动态增加的,所以每次我们都重新计算?因为最多10个字符串,每个10000,所以预处理每个字符串的前缀哈希需要O(L),总预处理时间10*10000=100000,可以接受。
然后,计算word的哈希值(整个字符串的哈希值),然后计算t中所有长度为len(word)的子串的哈希值,与word的哈希值比较。如果相同,则再逐字符确认(避免哈希冲突)?或者使用双哈希,两个哈希值都相同才认为匹配。这样冲突概率极低。
但是,为了简单,我们可以使用KMP算法,或者直接使用库函数(如t.find(word) != -1)?但题目是C++,我们可以用标准库的find函数(时间复杂度O(len(t)))。所以总的时间复杂度为O(len(t)),然后对于每个word和每个non_redundant中的t,总时间O(m * max_len_t * len(word)),最坏情况10*10000*10000=1e9,可能超时?
因此,我们需要更高效的方法:使用字符串哈希(滚动哈希)在O(len(t))时间内完成一次匹配?实际上,一次匹配需要O(len(t)),因为我们用滑动窗口,但计算t的前缀哈希后,我们可以在O(len(t))时间内得到所有与word长度相同的子串的哈希值,然后比较。所以一次匹配的时间复杂度为O(len(t))。总时间O(m * len(t)),而m是non_redundant的大小(最多10),len(t)最大10000,所以每个word最多10*10000=1e5,总共有10个word,所以总时间10*1e5=1e6,可以接受。
因此,我们可以这样实现is_substring(t, word):
如果len(word) > len(t),返回false(因为按长度降序,所以word长度<=t长度,所以这种情况不会出现?但按我们的排序,word长度<=t长度,所以可以跳过这个判断)。
计算word的哈希值(整个字符串)的双哈希值(hash1_word, hash2_word)。
预计算t的前缀哈希数组(如果还没计算的话)以及幂数组(用于取子串)。注意:我们只需要计算一次t的哈希数组,然后可以重复使用。
然后,在t上滑动长度为len(word)的窗口,计算每个子串的双哈希值,与word的双哈希值比较。如果相同,则返回true。
如果遍历完都没有找到,返回false。
但是,我们也可以使用KMP算法,在O(len(t))时间内完成一次匹配(其实KMP也是O(len(t)),但常数可能大一些)。这里为了简单,我们可以直接使用C++的string.find()函数(在C++中,string.find()最坏情况是O(len(t))),总时间也是1e6,可以接受。
3. 在去除冗余后,我们得到m个字符串(m<=n),然后计算一个m*m的overlap矩阵。
计算overlap[i][j]:表示字符串i和字符串j,当字符串j接在i后面时,最大的重叠长度(即i的后缀等于j的前缀的最大长度)。
方法:枚举重叠长度k(从0到min(len(i),len(j))),检查i的后k个字符和j的前k个字符是否相等。使用双哈希来加速比较:
i_str = non_redundant[i]
j_str = non_redundant[j]
k从min(len(i_str), len(j_str))开始递减到1,如果i_str.substr(len(i_str)-k, k) == j_str.substr(0, k) (用哈希比较),则overlap[i][j]=k,然后break。
如果找不到,则overlap[i][j]=0。
这里我们使用双哈希:预先计算每个字符串的前缀哈希和逆序哈希?不需要,只需要前缀哈希。对于i_str,我们只需要取后缀,即从下标L1-k到L1-1的子串,这个可以通过前缀哈希计算:H_i[L1]-H_i[L1-k]*base1^k mod mod1,同理j_str的前k个字符就是H_j[k-1](如果前缀哈希数组是从0开始)?所以我们需要预先计算每个字符串的前缀哈希数组和对应的幂数组。
具体计算子串哈希的公式(对于字符串s,前缀哈希数组为H,幂数组为P,则子串s[l..r]的哈希为H[r+1]-H[l]*P[r-l+1] mod mod。注意:我们的前缀哈希数组H[0]=0,H[1]=s[0]的哈希,H[2]=s[0]*base+s[1]...)
所以,对于i_str的后k个字符,起始位置为len(i_str)-k,长度为k,所以哈希值可以通过i_str的H_i数组计算:
hash1 = H1_i[len(i_str)] - H1_i[len(i_str)-k] * P1_i[k] (mod mod1)
hash2 = H2_i[len(i_str)] - H2_i[len(i_str)-k] * P2_i[k] (mod mod2)
对于j_str的前k个字符,就是j_str[0:k]的哈希值,这个在j_str的前缀哈希数组中已经计算:
hash1_j = H1_j[k] (注意:如果我们的前缀哈希数组H1_j[i]表示前i个字符的哈希值,那么前k个字符的哈希值就是H1_j[k])
hash2_j = H2_j[k]
然后比较(hash1, hash2)和(hash1_j, hash2_j)是否相等。
注意:我们需要为每个字符串计算前缀哈希数组和幂数组。由于最多有10个字符串,每个字符串长度最多10000,所以计算量不大。
4. 状态压缩DP:
设m = non_redundant.size()
dp[mask][i] 表示当前已经使用的字符串集合为mask(mask的二进制位表示哪些字符串被使用),且最后一个字符串是第i个字符串(i从0到m-1)时,超级串的最小长度。
初始化:dp[1<<i][i] = len(non_redundant[i]) for i in range(m)
状态转移:对于每个状态(mask, i),枚举下一个字符串j(不在mask中),则:
new_mask = mask | (1<<j)
new_length = dp[mask][i] + len(non_redundant[j]) - overlap[i][j]
更新dp[new_mask][j] = min(dp[new_mask][j], new_length)
最后,答案就是 min_{i=0}^{m-1} dp[(1<<m)-1][i]
5. 注意:由于我们可能只有一个字符串,所以需要处理边界。
但是,这个DP状态转移方程的正确性:因为我们拼接字符串时,将j接在i后面,重叠部分为overlap[i][j],所以只需要增加len(j)-overlap[i][j]的长度。
然而,这个DP并没有考虑字符串的排列顺序,而是通过状态mask和最后一个字符串来记录状态。由于我们每次只添加一个字符串,并且只考虑最后一个字符串与下一个字符串的重叠,所以是正确的。
但是,我们也可以考虑使用一个辅助数组记录状态转移的路径,以便输出最短超级串,但题目只要求长度,所以不需要。
6. 代码结构:
步骤1:读取输入,存储到vector<string> words中。
步骤2:去除冗余,得到non_redundant。
步骤3:如果non_redundant为空,则输出0(但n>=1,所以non_redundant至少有一个字符串)。
步骤4:m = non_redundant.size()。
步骤5:为non_redundant中的每个字符串计算前缀哈希数组(双哈希)和幂数组(用于计算任意子串哈希)。注意:幂数组只需要计算一次(每个字符串的幂数组独立,因为长度不同)。
步骤6:计算overlap矩阵(m x m)。
步骤7:状态压缩DP。
7. 由于m最大为10,状态总数最多为1<<10=1024,dp数组大小为1024*10,状态转移次数最多为1024*10*10=102400。
8. 实现细节:使用两个哈希模数(mod1, mod2)避免冲突。
但是,如果m==0,则输出0;m==1,则直接输出那个字符串的长度。
由于题目要求C++,我们将用C++实现。
注意:在计算双哈希时,模数取大质数。
代码框架:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <climits>
#include <unordered_set>
using namespace std;
typedef long long ll;
const int base1 = 131;
const int base2 = 131;
const ll mod1 = 1000000007;
const ll mod2 = 1000000009;
// 计算一个字符串的前缀哈希数组(双哈希)和幂数组
// 由于每个字符串都要用,所以封装一个结构体
struct StringHash {
string s;
vector<ll> H1, H2;
vector<ll> P1, P2;
void init() {
int n = s.size();
H1.resize(n+1, 0);
H2.resize(n+1, 0);
P1.resize(n+1, 0);
P2.resize(n+1, 0);
P1[0] = 1;
P2[0] = 1;
for (int i = 1; i <= n; i++) {
H1[i] = (H1[i-1] * base1 + s[i-1]) % mod1;
H2[i] = (H2[i-1] * base2 + s[i-1]) % mod2;
P1[i] = (P1[i-1] * base1) % mod1;
P2[i] = (P2[i-1] * base2) % mod2;
}
}
// 获取子串s[l, r]的双哈希值,索引从0开始,左闭右闭
pair<ll, ll> get_hash(int l, int r) {
ll hash1 = (H1[r+1] - H1[l] * P1[r-l+1] % mod1 + mod1) % mod1;
ll hash2 = (H2[r+1] - H2[l] * P2[r-l+1] % mod2 + mod2) % mod2;
return {hash1, hash2};
}
};
// 判断字符串t是否包含子串s(即s是否是t的子串)
bool contains(const string& t, const string& s) {
// 如果s长度大于t,直接返回false(但按我们的调用,s长度小于等于t)
if (s.size() > t.size()) return false;
// 使用双哈希
StringHash t_hash;
t_hash.s = t;
t_hash.init();
StringHash s_hash;
s_hash.s = s;
s_hash.init();
int n = t.size(), m = s.size();
auto s_hash_val = s_hash.get_hash(0, m-1);
// 在t中枚举起始位置i,长度为m的子串
for (int i = 0; i <= n - m; i++) {
auto t_sub_hash = t_hash.get_hash(i, i+m-1);
if (t_sub_hash == s_hash_val) {
// 双哈希相同,认为匹配
return true;
}
}
return false;
}
int main() {
int n;
cin >> n;
vector<string> words;
for (int i = 0; i < n; i++) {
string s;
cin >> s;
words.push_back(s);
}
// 去除冗余:按长度降序
sort(words.begin(), words.end(), [](const string& a, const string& b) {
return a.size() > b.size();
});
vector<string> non_redundant;
for (int i = 0; i < words.size(); i++) {
bool is_contained = false;
for (int j = 0; j < non_redundant.size(); j++) {
// 如果words[i]是non_redundant[j]的子串,则标记为冗余
if (contains(non_redundant[j], words[i])) {
is_contained = true;
break;
}
}
if (!is_contained) {
non_redundant.push_back(words[i]);
}
}
// 更新m
int m = non_redundant.size();
// 如果m为0,则输出0(但n>=1,所以m至少为1)
// 为non_redundant中的每个字符串构建StringHash对象数组
vector<StringHash> str_hashes(m);
for (int i = 0; i < m; i++) {
str_hashes[i].s = non_redundant[i];
str_hashes[i].init();
}
// 计算overlap矩阵
vector<vector<int>> overlap(m, vector<int>(m, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
if (i == j) {
// 自己和自己,重叠长度?但是同一个字符串不能连续使用两次,所以这里我们不会使用i=j的情况(在DP中不会自己接自己,因为mask中每个字符串只能用一次),所以可以忽略,或者设为0?但是为了后续DP,我们计算一下:实际上,如果两个相同的字符串,重叠长度就是整个字符串?但题目要求字符串互不相同,所以不会出现同一个字符串两次。所以这里i和j可以相同?不,在DP中我们不会把同一个字符串用两次,所以这里i==j时,我们不需要计算?但是为了避免错误,我们可以计算,但DP中不会使用(因为mask中每个字符串只能用一次)。所以计算一下也没关系。
}
int len_i = non_redundant[i].size();
int len_j = non_redundant[j].size();
int max_overlap = min(len_i, len_j);
// 从大到小枚举重叠长度k
for (int k = max_overlap; k >= 1; k--) {
// 获取i_str的后k个字符:从len_i-k到len_i-1
auto hash_i = str_hashes[i].get_hash(len_i - k, len_i - 1);
// 获取j_str的前k个字符:0到k-1
auto hash_j = str_hashes[j].get_hash(0, k-1);
if (hash_i == hash_j) {
overlap[i][j] = k;
break;
}
}
// 如果k=0,则overlap[i][j]保持0
}
}
// DP部分
// dp[mask][i]:mask是一个整数,表示集合,i是最后一个字符串的索引
vector<vector<int>> dp(1<<m, vector<int>(m, INT_MAX/2)); // 初始化为一个大数
// 初始化:只包含一个字符串
for (int i = 0; i < m; i++) {
dp[1<<i][i] = non_redundant[i].size();
}
// 状态转移
for (int mask = 0; mask < (1<<m); mask++) {
for (int i = 0; i < m; i++) {
if ((mask & (1<<i)) == 0) continue; // 如果当前状态mask不包含i,跳过
for (int j = 0; j < m; j++) {
if (mask & (1<<j)) continue; // j已经在mask中
int new_mask = mask | (1<<j);
// 尝试从i转移到j
int new_len = dp[mask][i] + non_redundant[j].size() - overlap[i][j];
if (new_len < dp[new_mask][j]) {
dp[new_mask][j] = new_len;
}
}
}
}
int ans = *min_element(dp[(1<<m)-1].begin(), dp[(1<<m)-1].end());
cout << ans << endl;
return 0;
}
注意:这个代码可能在某些边界情况有问题,比如当m=1时,DP初始化后,状态mask=1<<0,然后我们直接取dp[1][0]作为答案。然后状态转移不会执行(因为mask=1,其他字符串不在mask中,但m=1,所以内层循环j从0到0,但是mask已经包含0,所以跳过)。所以min_element会取到dp[1][0](唯一值)。
但还有一个问题:DP状态转移中,我们假设了每个字符串只能使用一次,并且必须使用一次。所以最终状态mask为全1,然后取最小值。
但是,如果非冗余集合只有一个字符串,那么答案就是该字符串的长度。
测试:两个字符串:["abc", "bcd"],应该得到"abcd",长度为4(重叠部分"bc"长度为2,所以总长=3+3-2=4)。
在overlap[0][1]:字符串"abc"和"bcd":最大重叠是"bc"(长度为2),所以overlap[0][1]=2。
然后dp[1<<0][0]=3(初始状态)。
然后从状态mask=1(二进制1)和i=0,转移到j=1:new_mask=3,new_len=3+3-2=4。
然后dp[3][1]=4。
最后mask=3,取dp[3][1]=4。
另一个测试:三个字符串:["abc", "bcd", "cde"],最优解应该是"abcde"(长度为5)。
首先,这三个字符串没有冗余。
计算overlap:
i=0 ("abc") 和 j=1 ("bcd"):重叠2
i=0和j=2:重叠0(因为"abc"的后缀和"cde"的前缀只有"c",但最大重叠是1?不对,"abc"的后缀"c"和"cde"的前缀"c"匹配,所以重叠1。但是,我们计算时从大到小枚举:最大重叠长度是min(3,3)=3,然后k=3: 比较"abc"和"cde"->不相等;k=2: "bc"和"cd"->不相等;k=1: "c"和"c"相等,所以重叠1。
i=1和j=2:重叠2("bcd"的后缀"cd"和"cde"的前缀"cd"匹配,重叠2)
i=1和j=0:重叠0(因为"bcd"的后缀和"abc"的前缀:没有重叠?因为"bcd"以'd'结尾,"abc"以'a'开头,所以重叠0?不对,最大重叠应该是0?)
注意:overlap[i][j]是指将j接在i后面,所以是i的后缀匹配j的前缀。
所以对于i=1,j=0:字符串i="bcd",j="abc",那么i的后缀和j的前缀:要求"bcd"的后缀同时是"abc"的前缀。最大重叠:k=min(3,3)=3,然后k=3:比较"bcd"和"abc"->不等;k=2:"cd"和"ab"->不等;k=1:"d"和"a"->不等,所以0。
所以overlap矩阵:
overlap[0][1]=2, overlap[0][2]=1
overlap[1][0]=0, overlap[1][2]=2
overlap[2][0]=0, overlap[2][1]=? 重叠2和0:i=2,j=0: "cde"后缀和"abc"前缀:最大重叠:k=min(3,3)=3,然后k=3: "cde"和"abc"不等;k=2: "de"和"ab"不等;k=1: "e"和"a"不等,所以0。
overlap[2][1]=? i=2,j=1: "cde"后缀和"bcd"前缀:最大重叠:k=min(3,3)=3,然后k=3: "cde"和"bcd"不等;k=2: "de"和"bc"不等;k=1: "e"和"b"不等,所以0。
然后DP:
初始:dp[1<<0][0]=3, dp[1<<1][1]=3, dp[1<<2][2]=3.
状态转移:
对于mask=1 (0001),i=0:可以转移到j=1和j=2。
j=1: new_mask=3 (0011), new_len = 3+3-2=4 -> dp[3][1]=4
j=2: new_mask=5 (0101), new_len = 3+3-1=5 -> dp[5][2]=5
对于mask=2 (0010),i=1:可以转移到j=0和j=2。
j=0: new_mask=3, new_len=3+3-0=6 -> 但dp[3][0]可能还没被更新,更新为6(但之前dp[3][1]是4,所以这里6比4大,所以保留4)
j=2: new_mask=6 (0110), new_len=3+3-2=4 -> dp[6][2]=4
对于mask=4 (0100),i=2:可以转移到j=0和j=1。
j=0: new_mask=5, new_len=3+3-0=6 -> dp[5][0]=6(之前dp[5][2]=5,所以这里5更小,保留5?不,这是两个不同的状态,dp[5][0]和dp[5][2]是独立的。所以dp[5][0]=6, dp[5][2]保持5。
j=1: new_mask=6, new_len=3+3-0=6 -> dp[6][1]=6(之前dp[6][2]=4,所以这里4更小,保留4?不,dp[6][1]和dp[6][2]是独立的,所以dp[6][1]=6, dp[6][2]=4。
然后考虑mask=3(0011): 此时有两个字符串0和1,最后一个字符串是1(因为dp[3][1]=4),然后转移到j=2(唯一剩下的):
new_mask=7 (0111), new_len = dp[3][1] + len(2) - overlap[1][2] = 4+3-2=5 -> dp[7][2]=5
对于mask=5(0101):有两个字符串0和2,最后一个字符串是2(dp[5][2]=5),转移到j=1:
new_mask=7, new_len=5+3 - overlap[2][1] = 5+3-0=8 -> 更新dp[7][1]=min(当前值,8) 但之前dp[7][2]=5,所以dp[7][1]=8。
对于mask=6(0110):有两个字符串1和2,最后一个字符串是2(dp[6][2]=4),转移到j=0:
new_mask=7, new_len=4+3 - overlap[2][0]=4+3-0=7 -> dp[7][0]=7,但之前dp[7][2]=5,所以最小值还是5。
然后最终状态mask=7,取最小值:min(dp[7][0], dp[7][1], dp[7][2]) = min(7,8,5)=5。
所以答案是5,正确。
因此,代码实现。
但是,我们也可以尝试优化:在状态转移时,如果dp[mask][i]是无穷大,可以跳过。
另外,注意:在计算重叠长度时,我们使用了双哈希,但也可以使用其他方法(如KMP),但双哈希在平均情况下足够。
由于n很小,这个算法可以接受。
但是,我们需要注意的是,在去除冗余时,我们使用的contains函数中,为每个non_redundant中的字符串和每个检查的word都计算了哈希结构体(包括前缀哈希数组和幂数组),这个计算量是O(L)(L为字符串长度),而总调用次数最多为O(n*m)(m为non_redundant的大小,最多n,所以最多10*10=100次),每次计算O(10000),所以100*10000=1e6,可以接受。
最后,输出答案。
注意:如果去除冗余后m=0,则输出0。但题目保证n>=1,所以至少有一个字符串,但可能这个字符串被它自己包含(不会,因为non_redundant中不会有两个相同的字符串),所以m至少为1。
我们写代码。
但是,上面的contains函数中,我们为每个t和每个word都构建了StringHash对象。但实际上,在去除冗余的循环中,non_redundant中的字符串是动态增加的,我们可以预先计算non_redundant中每个字符串的哈希对象,然后用于多次检查。所以我们可以优化:在循环non_redundant时,先计算好每个新加入的字符串的哈希对象,然后对于新来的word,用这些哈希对象来检查。
修改去除冗余的循环:
vector<string> non_redundant;
vector<StringHash> non_red_hashes; // 存储每个字符串的哈希结构体
sort(words...);
for (int i=0; i<words.size(); i++) {
bool is_contained = false;
for (int j=0; j<non_redundant.size(); j++) {
// 使用non_red_hashes[j]来判断words[i]是否是non_redundant[j]的子串
if (contains_via_hash(non_red_hashes[j], words[i])) {
is_contained = true;
break;
}
}
if (!is_contained) {
non_redundant.push_back(words[i]);
StringHash sh;
sh.s = words[i];
sh.init();
non_red_hashes.push_back(sh);
}
}
然后,我们写一个辅助函数contains_via_hash(StringHash& t_hash, string& s),它使用t_hash来判断t_hash.s是否包含s。
注意:在contains_via_hash中,我们只需要计算s的整个字符串的哈希值,然后在t_hash中滑动窗口比较。
具体实现:
bool contains_via_hash(StringHash& t_hash, const string& s) {
int n = t_hash.s.size();
int m = s.size();
if (m > n) return false;
// 计算s的双哈希值(整个字符串)
StringHash s_hash;
s_hash.s = s;
s_hash.init(); // 这里每次都要计算s的前缀哈希,但m最大10000,且最多100次,所以总计算量100*10000=1e6,可以接受。
auto s_val = s_hash.get_hash(0, m-1);
for (int i=0; i<=n-m; i++) {
auto t_val = t_hash.get_hash(i, i+m-1);
if (t_val == s_val) {
return true;
}
}
return false;
}
这样,我们避免了在contains函数中重复计算t_hash(因为t_hash已经在加入non_redundant时计算过)。
但是,我们也可以在StringHash结构体中添加一个方法:计算整个字符串的哈希值(其实我们已经有了,get_hash(0, n-1)),但是这里我们需要的是子串,所以还是要用上面的循环。
总之,我们按照这个思路实现C++代码。
由于题目要求使用哈希,我们这里使用了双哈希。
最后,注意内存和时间:状态压缩DP状态数1024*10,转移次数102400,可以接受。
提交代码时,注意使用C++11。
注意:在计算哈希时,取模运算要小心负数。
我们编写完整代码。
但是,由于时间关系,这里我们只提供代码框架,实际运行可能需要调试。
由于题目要求输出代码,我们写完整代码。
注意:在去除冗余时,我们按长度降序排序,这样长字符串先加入,短字符串后加入,并且短字符串如果是长字符串的子串则被过滤。
开始写代码:
</think>
以下是用C++结合哈希方法解决寻找包含所有给定炼金反应短语文本子串的最短字符串长度问题的完整代码实现:
```cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long ll;
const int base1 = 131;
const int base2 = 131;
const ll mod1 = 1000000007;
const ll mod2 = 1000000009;
// 字符串哈希结构体
struct StringHash {
string s;
vector<ll> H1, H2;
vector<ll> P1, P2;
void init() {
int n = s.size();
H1.resize(n+1, 0);
H2.resize(n+1, 0);
P1.resize(n+1, 1);
P2.resize(n+1, 1);
for (int i = 1; i <= n; i++) {
H1[i] = (H1[i-1] * base1 + s[i-1]) % mod1;
H2[i] = (H2[i-1] * base2 + s[i-1]) % mod2;
P1[i] = (P1[i-1] * base1) % mod1;
P2[i] = (P2[i-1] * base2) % mod2;
}
}
// 获取子串[l, r]的双哈希值
pair<ll, ll> get_hash(int l, int r) {
ll hash1 = (H1[r+1] - H1[l] * P1[r-l+1] % mod1 + mod1) % mod1;
ll hash2 = (H2[r+1] - H2[l] * P2[r-l+1] % mod2 + mod2) % mod2;
return {hash1, hash2};
}
};
// 使用哈希检查t是否包含s
bool contains_via_hash(StringHash& t_hash, const string& s) {
int n = t_hash.s.size();
int m = s.size();
if (m > n) return false;
StringHash s_hash;
s_hash.s = s;
s_hash.init();
auto s_val = s_hash.get_hash(0, m-1);
for (int i = 0; i <= n - m; i++) {
auto t_val = t_hash.get_hash(i, i+m-1);
if (t_val == s_val) {
return true;
}
}
return false;
}
int main() {
int n;
cin >> n;
vector<string> words;
for (int i = 0; i < n; i++) {
string s;
cin >> s;
words.push_back(s);
}
// 按长度降序排序
sort(words.begin(), words.end(), [](const string& a, const string& b) {
return a.size() > b.size();
});
vector<string> non_redundant;
vector<StringHash> non_red_hashes;
// 去除冗余字符串
for (int i = 0; i < words.size(); i++) {
bool is_contained = false;
for (int j = 0; j < non_redundant.size(); j++) {
if (contains_via_hash(non_red_hashes[j], words[i])) {
is_contained = true;
break;
}
}
if (!is_contained) {
non_redundant.push_back(words[i]);
StringHash sh;
sh.s = words[i];
sh.init();
non_red_hashes.push_back(sh);
}
}
int m = non_redundant.size();
if (m == 0) {
cout << 0 << endl;
return 0;
}
// 重新计算所有非冗余字符串的哈希(如果未在non_red_hashes中)
vector<StringHash> str_hashes(m);
for (int i = 0; i < m; i++) {
str_hashes[i].s = non_redundant[i];
str_hashes[i].init();
}
// 计算重叠矩阵
vector<vector<int>> overlap(m, vector<int>(m, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
int len_i = non_redundant[i].size();
int len_j = non_redundant[j].size();
int max_overlap = min(len_i, len_j);
for (int k = max_overlap; k >= 1; k--) {
auto hash_i = str_hashes[i].get_hash(len_i - k, len_i - 1);
auto hash_j = str_hashes[j].get_hash(0, k-1);
if (hash_i == hash_j) {
overlap[i][j] = k;
break;
}
}
}
}
// 状态压缩DP
int total_states = 1 << m;
vector<vector<int>> dp(total_states, vector<int>(m, INT_MAX));
// 初始化:单个字符串
for (int i = 0; i < m; i++) {
dp[1 << i][i] = non_redundant[i].size();
}
// DP状态转移
for (int mask = 0; mask < total_states; mask++) {
for (int i = 0; i < m; i++) {
if (dp[mask][i] == INT_MAX) continue;
for (int j = 0; j < m; j++) {
if (mask & (1 << j)) continue;
int new_mask = mask | (1 << j);
int new_len = dp[mask][i] + non_redundant[j].size() - overlap[i][j];
if (new_len < dp[new_mask][j]) {
dp[new_mask][j] = new_len;
}
}
}
}
// 找出最终结果
int ans = *min_element(dp[total_states - 1].begin(), dp[total_states - 1].end());
cout << ans << endl;
return 0;
}
```
### 算法说明
1. **问题分析**:给定$N$个互不相同的字符串,需要找到最短字符串,使得所有给定字符串都是其子串。这是一个经典的最短超串问题(Shortest Superstring Problem)。
2. **关键步骤**:
- **去除冗余**:按长度降序排序,如果字符串$A$是字符串$B$的子串,则移除$A$(因为$B$已隐含$A$)。使用双哈希(base=131, mod=10⁹+7/10⁹+9)高效判断子串关系。
- **计算重叠矩阵**:对于每对字符串$(s_i, s_j)$,计算$s_i$后缀与$s_j$前缀的最大重叠长度,使用哈希加速比较。
- **状态压缩DP**:
- 状态定义:$dp[mask][i]$表示已使用字符串集合$mask$(二进制表示),且最后一个字符串是$i$时的最小长度。
- 初始化:$dp[1<<i][i] = len(s_i)$。
- 状态转移:$dp[new\_mask][j] = \min(dp[mask][i] + len(s_j) - overlap[i][j])$,其中$new\_mask = mask \mid (1<<j)$。
- 结果:$\min(dp[2^m-1][i])$($m$为非冗余字符串数量)。
3. **时间复杂度**:
- 去冗余:$O(N^2L)$($L$为字符串平均长度)
- 计算重叠矩阵:$O(m^2L)$($m \leq N$)
- DP:$O(m^2 \cdot 2^m)$($m \leq 10$,总状态数$1024 \times 10$)
### 测试示例
输入:
```
2
abc
bcd
```
输出:
```
4
```
解释:最短超串"abcd"包含子串"abc"和"bcd"。
### 相关问题
1. **如何优化哈希冲突处理**?
双哈希(两个不同基数和模数)已极大降低冲突概率,可进一步增加哈希函数数量或使用大素数模数[^1]。
2. **当字符串包含大写字母/特殊字符时如何处理**?
修改哈希函数:扩展字符映射范围(如ASCII值),或统一转换为小写字母处理[^2]。
3. **$N$较大时(如$N>20$)的优化方法**?
状态压缩DP不再适用,可用启发式算法(如遗传算法)或近似算法(如贪心合并)[^3]。
4. **是否有更高效精确算法**?
对于$N$较小的情况,状态压缩DP已是最优;$N$较大时,可尝试分支定界或剪枝优化,但问题本质是NP难[^1]。
: 哈希冲突处理优化方法参考。
: 字符串预处理扩展方法。
[^3]: 大规模问题的近似算法解决方案。