Loading

P2322 [HNOI2006] 最短母串问题

感觉这题非常有代表性。

首先我们考虑这个一定是状压 DP 了。如何设计状态呢?首先必须有一个 \(2 ^ n\) 的维度,表示哪些字符串已经被满足了。然后还需要表达一个后缀的信息。我们尝试再加一个维度 \(j\) 代表当前字符串的后缀是哪个子串。

于是我们有了 \(f(i,j)\) 代表 \(i\) 里的字符串被满足,末尾为字符串 \(j\) 的最优字符串。

这时不难想到,如果要在 \(f(i,j)\) 后面追加一个字符串 \(s\),那么对于 \(s\) 来讲,是不是这种情况更优呢?

image

\(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;
}
posted @ 2026-08-07 15:34  Trent900  阅读(22)  评论(0)    收藏  举报