P2322 [HNOI2006] 最短母串问题
感觉这题非常有代表性。
首先我们考虑这个一定是状压 DP 了。如何设计状态呢?首先必须有一个 \(2 ^ n\) 的维度,表示哪些字符串已经被满足了。然后还需要表达一个后缀的信息。我们尝试再加一个维度 \(j\) 代表当前字符串的后缀是哪个子串。
于是我们有了 \(f(i,j)\) 代表 \(i\) 里的字符串被满足,末尾为字符串 \(j\) 的最优字符串。
这时不难想到,如果要在 \(f(i,j)\) 后面追加一个字符串 \(s\),那么对于 \(s\) 来讲,是不是这种情况更优呢?

即 \(j\) 并不能完整地承接 \(s\) 的前缀。
那这种情况我们的 DP 能跑到吗?又该如何转移呢?我们换一种全局视角看问题。
假如在最优答案中,字符串 \(i\) 出现的位置是 \([l_i, r_i]\)。
那么我们将所有的区间按照 \(r_i\) 为第一关键字,\(l_i\) 为第二关键字由小到大排序。
在这之后,我们考察相邻的两个区间 \([l_i,r_i]\) 和 \([l_j,r_j]\)。由于 \(r_i \le r_j\),因此两个区间的位置情况只有两种:
-
\(l_i \le l_j\),即 \(i\) 完整地承接住了 \(j\) 的前缀。
-
\(l_j < l_i\),即 \(j\) 包含了 \(i\)。
那么,考虑到状压 DP 本身就是在将全排列的等价情况按照状态合并,那么我们只需要在处理转移时,保证可以覆盖到上面的两种情况即可。
-
对于第一种情况,我们直接暴力找公共前后缀即可,或者可以使用 KMP。
-
对于第二种情况,我们判断新加进来的字符串是否是 \(j\) 的子串。
这样,相邻字符串的情况便全被覆盖了。每次通过这两种转移,即可覆盖到最终答案。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 13, M = (1 << 12);
int n;
string s[N], f[M][N];
void chmin(string &x, string y){
if(x.empty()) x = y;
else if(x.size() > y.size()) x = y;
else if(x.size() == y.size() && x > y) x = y;
}
int get(string &x, string &y){
for(int len = min(x.size(), y.size()); len >= 0; len --){
if(y.substr(0, len) == x.substr(x.size() - len, len)) return len;
}
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> s[i];
for(int i = 1; i <= n; i ++){
f[1 << (i - 1)][i] = s[i];
}
for(int i = 1; i <= (1 << n); i ++){
for(int j = 1; j <= n; j ++){
if(!(i & (1 << (j - 1)))) continue;
for(int k = 1, w = 1; k <= n; k ++, w <<= 1){
if(i & w) continue;
if(s[j].find(s[k]) != string::npos) chmin(f[i + w][j], f[i][j]);
int len = get(s[j], s[k]); chmin(f[i + w][k], f[i][j] + s[k].substr(len));
}
}
}
string ans;
for(int i = 1; i <= n; i ++){
chmin(ans, f[(1 << n) - 1][i]);
}
cout << ans << "\n";
return 0;
}

浙公网安备 33010602011771号