字符串专题-KMP+扩展KMP

KMP算法

例题1:E. Martian Strings【前缀函数的运用】

这一题笨笨地写了个SA+二分,慢死了(常数大)。虽然这一题是多串匹配,但是\(m=100\),S串长度为\(1e5\),所以是可以暴力check每一个pattern的。。但是因为这一题要把一个串分成两个不相交的区间,所以考虑顺序、逆序做一次KMP,特判长度为1的pat。复杂度\(O(m*n=1e7)\)

思路来自于:聚聚的blog LINK

查看代码

int n, q, k, nt[maxn], pv[maxn], sf[maxn];
char pat[1111], s[maxn];

inline void initNxt(char s[], int n) {
  nt[1] = 0;
  for (int i = 2, k = 0; i <= n; i++) {
    while (k && s[i] != s[k + 1]) k = nt[k];
    nt[i] = (s[i] == s[k + 1] ? ++k : 0);
  }
}

inline void search(int* ar, int n, int m) {
  for (int i = 1, k = 0; i <= n; i++) {
    while (k && k < m && s[i] != pat[k + 1]) k = nt[k];
    if (k < m && s[i] == pat[k + 1]) k++;
    ar[i] = max(ar[i - 1], k);  // 根据题目而取 max
  }
}

inline bool chk() {
  int m = strlen(pat + 1);
  if (m > n || m == 1) return false;
  initNxt(pat, m), search(pv, n, m);
  reverse(pat + 1, pat + m + 1);
  reverse(s + 1, s + 1 + n);
  initNxt(pat, m), search(sf, n, m);
  reverse(s + 1, s + 1 + n);
  reverse(sf + 1, sf + 1 + n);
  for (int i = 1; i < n; i++)
    if (pv[i] + sf[i + 1] >= m) return true;
  return false;
}

inline void solve() {
  cin >> s + 1 >> q, n = strlen(s + 1);
  int ans = 0;
  for (int i = 1; i <= q; i++) cin >> pat + 1, ans += chk();
  cout << ans << endl;
}

 

例题2:P2375 [NOI2014] 动物园【KMP + DP + 严格Border的数量】

定义严格border为str的border,且前缀和后缀不重叠。假设border长度为m,则\(str[1:m]==str[n-m+1:n]\),且\(n-m+1>m\)。求每个前缀有多少严格border。

我们求出nxt数组,发现nxt数组就是fail树上的父节点,从n开始倒着做滑动窗口/贪心/滑动指针即可(用儿子指针更新父节点指针)。就可以做到\(O(n)\)。当然也可以考虑树上倍增。

查看代码
 const int maxn = 1e6 + 9, mod = 1e9 + 7;
char str[maxn];
int TEST, n, nxt[maxn], dep[maxn], ptr[maxn];
int main() {
  scanf("%d", &TEST);
  while (TEST--) {
    scanf("%s", str + 1);
    n = 1, dep[1] = nxt[1] = 0;
    for (int i = 2, L = 0; str[i]; n++, i++) {
      while (L && str[i] != str[L + 1]) L = nxt[L];
      if (str[i] == str[L + 1]) L++;
      ptr[i] = nxt[i] = L, dep[i] = dep[L] + (L > 0);
    }
    int ans = 1;
    for (int i = n; i >= 1; i--) {
      while (ptr[i] && ptr[i] * 2 > i) ptr[i] = nxt[ptr[i]];
      ans = (1ll * ans * (dep[ptr[i]] + (ptr[i] > 0) + 1)) % mod;
      if (dep[ptr[nxt[i]]] > dep[ptr[i]]) ptr[nxt[i]] = ptr[i];
    }
    printf("%d\n", ans);
  }
}

 

例题3:D. Om Nom and Necklace【枚举 + 思维 + 循环节 + 贪心】

题意:给定长度为\(n\)的字符串str和一个常数\(k\)。对于每一个前缀str[1:i],问他能不能拆成ABABAB...A的形式,其中A出现k+1次,B出现k次,A和B可以为空。

对于一个前缀i,\(len=i-nxt[i]\)是最短的循环节,我们先贪心地把最短的循环节铺满,同时铺满的情况应该是k的倍数。假设AB的长度是L,那么有:\(L=len*v\),且\(v=\lfloor\cfrac{i}{k*(i-nxt[i])}\rfloor\)。

这个时候再检查一下是否有:\(v*len\ge i-v*len*k\)即可。注意特判\(k=1\)时,ans[1]='1'。

 

例题4:UVA11022 String Factoring【字符串压缩 + 区间DP】

题意:给你一个长度小于80的串,相邻的两个相邻的子串可以压缩,但是要用括号括住,之后和外部不能压缩了。求压缩后的最小长度。

如果一个串是有循环节组成的,那么它就是KMP直接压缩就行,压缩完之后还是要做区间DP的,因为还是有可能继续压缩。否则,我们考虑区间dp:\(f(str)=min(f(str[0,i])+f(str[i+1,n])) i是枚举的分割点,两边不会有压缩关系了\)。然后做一个记忆化搜索,虽然状态是字符串,但是还是很快的。

查看代码
 string s;
map<string, int> ans;

int getNxt(const string& s) {
	vector<int> nxt(s.size(), 0);
	for (int i = 1, j = 0; i < s.size(); i++) {
		while (j && s[i] != s[j]) j = nxt[j - 1];
		if (s[i] == s[j]) j++;
		nxt[i] = j;
	}
	return s.size() - nxt.back();
}

int dfs(string s) {
	if (s.size() <= 1) return s.size();
	int len = getNxt(s);
	if (len < s.size() && s.size() % len == 0) s = s.substr(0, len);
	if (ans.count(s)) return ans[s];
	int ret = s.size();
	for (int i = 1; i < s.size(); i++) 
		ret = min(ret, dfs(s.substr(0, i)) + dfs(s.substr(i, s.size() - i)));
	return ans[s] = ret;
}

int main() {
  ios_fast;
	while (cin >> s && s != "*") cout << dfs(s) << '\n';
}

 

例题5:G. Anthem of Berland【暴力DP + KMP】

做法1:定义\(f[i][j]\)为S的前i位匹配了T的第j位的最大匹配次数,然后枚举下一个字母进行转移。为了更好实现,我们引入trans[i][c]数组表示字符串T已经被匹配了i位,下一位来了个c的时候,跳fail应该转移到第nj位,这样就能辅助转移(即dp[i][nj]=dp[i-1][j]+(nj==m))。为了更好对最后一位进行转移,我们在T后面添加一个字符'#',自然,匹配完第m位之后,m+1位就是'#'了,就要自动跳nxt进行转移了(注意,滚动数组注意清空,否则wa10)。

  关于trans:trans数组的转移类似AC自动机的trie图一样,利用fail进行转移。

做法2:考虑i是匹配T的最后一位,(一开始先\(O(m)\)暴力for循环check一下,i能否作为最后一位)那么定义f[i]为S的第i位匹配了T的最后一位的最多匹配次数。然后转移就是枚举fail链(即枚举和之前重叠的部分而已)。这个做法剪掉了26的常数,还好出题人有点良心没有卡第一种做法。(f数组做一遍前缀最大值进行转移即可)

【同样用到trans数组的一个题目:KMP转移矩阵优化DP】:P3193 [HNOI2008]GT考试

 

例题6:P3435 [POI2006] OKR-Periods of Words【模板题 - 每个前缀的最长不严格周期】

我们知道最短不严格周期(就是长度不一定是n的约数)就是i-nxt[i],但是对于最长的不严格周期(是一个周期,但是不能是自己),我们只能暴力跳fail了。(注意,题目意思应该是如果没有的话,长度就是0)

可是暴力跳fail会被卡成\(O(n^2)\)复杂度,不行。但是我们发现,最大的i-nxt[i],其实就是取出fail树上i节点的最浅非0父亲而已。我们考虑一些单调性质。如果i节点已经知道答案,那么它的子树中的节点跳到i节点也会知道答案,最终每条边最多被跳一次,所以!记忆化之后是\(O(n)\)的。

查看代码
 const int maxn = 1e6 + 7;
int n, nxt[maxn];
char str[maxn];

int main() {
  ios_fast;
  cin >> n >> str + 1;
  for (int i = 2, j = 0; i <= n; i++) {
    while (j && str[i] != str[j + 1]) j = nxt[j];
    nxt[i] = (str[i] == str[j + 1] ? ++j : j);
  }
  long long ans = 0;
  for (int i = 2; i <= n; i++) {
    int j = i;
    while (nxt[j]) j = nxt[j];
    if (nxt[i]) ans += (i - j), nxt[i] = j;
    // 只有nxt[i]!=0是 ,才有真正的最长非严格周期
  }
  cout << ans << endl;
}

 

扩展KMP算法 - Z函数

例题1: E. Text Editor【贪心 + Z函数预处理】

题意:给一个长度为m的T串,和一个长度为n>m的S串。现在让你删去S中的一些位置,使得S变成T。最开始光标在S串末尾,你只能操作1.backspace、2.left、3.right、4.home、5.end 这5个按键。求出最少按键次数。

思路:

枚举S中的一个分界点sp,再枚举T中的一个分界点tp。

① sp左侧尽可能往左边匹配,计算公式为:\(sp-tp+|LCS|\),其中LCS是S[1:sp]和T[1:tp]的最长公共后缀

② sp右侧尽可能往右匹配,计算公式为:\(|LCP|\),其中LCP是S[sp+1:n]和T[tp+1:m]的最长公共前缀。

考虑使用Z函数预处理优化掉一个n,同时使用贪心策略判断能不能表示,最后复杂度是:\(O(n^2+n*m)\)。妈的,因为脑子猪了,所以写了一年。

查看代码
 int n, m, pre[maxn], suf[maxn];
string S, T, revT;

vector<int> getZ(const string& s) {
  vector<int> z(s.size(), 0);
  for (int i = 1, l = 0, r = 0; i < s.size(); ++i) {
    if (i <= r && z[i - l] < r - i + 1) {
      z[i] = z[i - l];
    } else {
      z[i] = max(0, r - i + 1);
      while (i + z[i] < s.size() && s[z[i]] == s[i + z[i]]) ++z[i];
    }
    if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
  }
  return z;
}

void solve() {
  cin >> n >> m >> S >> T;
  revT = T, reverse(all(revT));
  for (int i = 0, j = -1; i <= n; i++) {
    if (i < n && j < m - 1 && S[i] == T[j + 1]) j++;
    pre[i] = j;
  }
  suf[n] = m + 1;   // 记得初始化
  for (int i = n - 1, j = m; i >= 0; i--) {
    if (j > 0 && S[i] == T[j - 1]) j--;
    suf[i] = j;
  }
  int ans = inf_int;
  string LS, RS = S;
  for (int sp = 0; sp <= n; sp++) {
    auto lz = getZ(LS + "#" + revT);
    move(lz.begin() + LS.size() + 1, lz.end(), lz.begin());
    auto rz = getZ(RS + "#" + T);
    move(rz.begin() + RS.size() + 1, rz.end(), rz.begin());
    for (int tp = 0; tp <= sp; tp++) {
      if (tp - 1 > (sp ? pre[sp - 1] : -1)) break;
      if (tp < suf[sp] || m - tp > n - sp) continue;
      // 这里存在一个边界条件,当tp=0且sp=0时,说明不会按home键,反之默认按home键
      int Lans = 2 * (int)LS.size() + (tp ? 1 - lz[m - tp] - tp : (sp != 0));
      int Rans = (int)RS.size() - (tp < m ? rz[tp] : 0);
      if (Lans < 0 || Rans < 0) continue;
      ans = min(Lans + Rans, ans);
    }
    if (sp == n || RS.size() == 0) break;
    LS.insert(0, 1, S[sp]);
    RS = RS.substr(1, (int)RS.size() - 1); \\ string的erase有bug,会删除p之后所有字符
  }
  cout << (ans == inf_int ? -1 : ans) << '\n';
}

 

posted @ 2022-08-13 23:45  PigeonG  阅读(54)  评论(0)    收藏  举报