题解 P3502 【[POI2010]CHO-Hamsters】
wlzhouzhuan
·
·
题解
讲一下自己的大致思路。
先说一下暴力分:
这是我看到此题的第一反应:dp,复杂度为O(n^2m)。
我们先定义两字符串间的距离值dis[i][j],表示j号串拼接在i号串之后,需要在i号串末尾添加的字符数。
此时我们再用dp[i][j] 表示这个串里应经有了i个字符串,并且最后出现在该串里的子串是j号串的答案,显然可以按下转移:
dp[1][i]=len(i)$,表示取1个字符串,并且是第$i$个字符串结尾,那么它的代价显然是$len(i)
后面拼接上$j$号字符串,添加字符量即为$dis[k][j]$。
注意转移时的顺序,应该先枚举$i$,再枚举$j$和$k$。读者可以想想,为什么?
贴一下`dp`的核心部分:
```cpp
void DP() {
memset(dp, 0x7f, sizeof(dp));
for (int i = 1; i <= n; i++)
dp[1][i] = len[i];
for (int i = 2; i <= m; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= n; k++)
dp[i][j] = min(dp[i][j], dp[i - 1][k] + dis[k][j]);
}
```
所以,我们分析想到了转移公式,那么答案即为$min\{ dp[m][i]\}$,其中$1\le i\le n$(因为一共有$n$种字符串)。
看到有$n$个字符串,并且是一个个字符串首尾拼接而成的,我们很自然的想到`AC自动机`。
在本人经过长达1小时的思考后,终于想出了正解。。。
本人比较擅长`KMP算法`,看到这题,果断想到是`KMP`,粗略计算一下,`KMP()`函数部分的复杂度为$O(\sum^{n}_{i=1}\sum^{n}_{j=1}(len(i)+len(j)))= O(\sum^{n}_{i=1}(n\times len[i]+\sum |S|))≈ O(n|S|)$,稍微带一点小常数。
下面是`KMP`部分的代码:
```cpp
void KMP() {
// kmp1:单个字符串内匹配
for (int p = 1; p <= n; p++) {
int j = 0;
for (int i = 2; i <= len[p]; i++) {
while (j > 0 && str[p][j + 1] != str[p][i])
j = nxt[p][j];
if (str[p][j + 1] == str[p][i]) j++;
nxt[p][i] = j;
}
}
// kmp2:两两字符串间匹配
for (int p1 = 1; p1 <= n; p1++) {
for (int p2 = 1; p2 <= n; p2++) {
int j = 0;
for (int i = 2; i <= len[p1]; i++) {
while (j > 0 && str[p2][j + 1] != str[p1][i])
j = nxt[p2][j];
if (str[p2][j + 1] == str[p1][i]) j++;
if (i == len[p1]) {
a.v[p1][p2] = len[p2] - j;
}
}
}
}
}
```
好,既然我们已经预处理`KMP`得到了两两字符串间的关系(末尾添加字符量),那么我们接下来考虑如何优化`dp`。
读者不妨再回头去看看`dp`的转移部分,有没有发现,这个转移方式似曾相识?
没错,这似乎与`floyd`的转移代码有异曲同工之妙。它的转移方式,跟`floyd`是一模一样的!那么这个转移方式等价于一个$O(n^3)$的`floyd`,我们从另一种角度去考虑如何优化这个伪的`floyd`。
在优化复杂度之前,请读者先考虑一些细节问题:
首先,将这个转换成`floyd`,是基于点操作的,我们可以将每一个字符串的每一个字符都视作是一个点,这下次就是彻彻底底的`floyd`了233。
其次,我们不知道该从第几号字符串作为整一个串的起点,所以我们考虑加一个`超级源`,设它为$0$号串,将$0$号串连接到第$i(1\le i\le n)$号串。从$0$到$i$的权值为$dis[0][i] = len(i)$,从$i$到$0$显然做不到,所以设置为无穷大,即:$dis[i][0] = inf$。
好了,接下来我们考虑如何去优化复杂度。
很显然,数据范围中$m$的范围特别大,已经到了$1e9$的地步,此时我们就数据范围倒推,显然不可能是$O(m)$线性扫,所以极大可能是$O(\sqrt{m})$或者$O(logm)$。
如果是$O(\sqrt{n})$,那么一定有关于开根号的东西,但`floyd`显然不可能与根号发生关系,所以暂时排除。
那么我们发现只可能是$O(logm)$的复杂度内运行`floyd`,此时我们果断想到:与$log$有关的,极大可能是`快速幂`,但这不是$a^p$的形式,而是一个二维的幂形式。
相信读者都已经猜到了,这就是`矩阵快速幂`!
综上,我们发现,这道题可以在$O(n|S|+n^2*log\space m)$的复杂度内跑出结果,常数较小,值得一试。
同时,作为前人,也提醒一下大家:这道题可能要开$-O2$才能$AC$,`数据#1`稍微有点卡常数!
献上代码,完美撒花:
```cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef long long ll;
const int N = 202;
const int M = 100002;
string str[N];
int len[N], t;
int nxt[N][M];
//int dis[201][201];
int n, m;
struct MAT {
ll v[N][N];
void print() {
for (int i = 0; i <= n; i++, puts(""))
for (int j = 0; j <= n; j++)
printf("%lld ", v[i][j]);
putchar('\n');
}
} ans, a;
MAT operator * (MAT a, MAT b) {
MAT c;
for (int i = 0; i <= n; i++)
for (int j = 0; j <= n; j++)
c.v[i][j] = 1e15;
for (int i = 0; i <= n; i++)
for (int j = 0; j <= n; j++)
for (int k = 0; k <= n; k++)
c.v[i][k] = min(c.v[i][k], a.v[i][j] + b.v[j][k]);
return c;
}
void KMP() {
// init
a.v[0][0] = 1e15;
for (int p = 1; p <= n; p++)
a.v[0][p] = len[p], a.v[p][0] = 1e15;
// kmp1
for (int p = 1; p <= n; p++) {
int j = 0;
for (int i = 2; i <= len[p]; i++) {
while (j > 0 && str[p][j + 1] != str[p][i])
j = nxt[p][j];
if (str[p][j + 1] == str[p][i]) j++;
nxt[p][i] = j;
}
}
// kmp2
for (int p1 = 1; p1 <= n; p1++) {
for (int p2 = 1; p2 <= n; p2++) {
int j = 0;
for (int i = 2; i <= len[p1]; i++) {
while (j > 0 && str[p2][j + 1] != str[p1][i])
j = nxt[p2][j];
if (str[p2][j + 1] == str[p1][i]) j++;
if (i == len[p1]) {
a.v[p1][p2] = len[p2] - j;
}
}
}
}
/*
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
printf("dis[%d][%d] = %d\n", i, j, dis[i][j]);
*/
}
void ksm() {
//a.print();
for (int i = 0; i <= n; i++)
for (int j = 0; j <= n; j++)
ans.v[i][j] = a.v[i][j];
// woc, m--!
m--;
while (m > 0) {
//printf("in ksm %d\n", m);
//ans.print(); a.print();
//printf("----------\n");
if (m & 1) ans = ans * a;
a = a * a; m >>= 1;
}
}
bool check() {
for (int i = 1; i <= n; i++) {
if (len[i] == 1) return 1;
}
return 0;
}
signed main() {
scanf("%lld%lld", &n, &m);
if (m == 0) {
puts("0"); return 0;
}
for (int i = 1; i <= n; i++) {
cin >> str[i];
len[i] = str[i].size();
str[i] = ' ' + str[i];
}
if (check()) {
printf("%lld\n", m);
return 0;
}
KMP();
ksm();
ll anss = 2e15;
for (int i = 1; i <= n; i++)
anss = min(anss, ans.v[0][i]);
printf("%lld\n", anss);
return 0;
}
```
附:如果有$WA$的网友,我在这里提供几组$hack$数据,您可以不妨一测。
$\color{red}hack1.in
5 100
a
b
c
d
e
\color{red}hack1.out
100
\color{red}hack2.in
1 0
a
\color{red}hack2.out
0
\color{red}hack3.in
4 5
ab
bc
cd
de
\color{red}hack3.out
7