bitset 的妙用:乱搞字符串匹配

最近碰到了几次 bitset 乱搞字符串匹配的情况,故写文以记之。

1. 算法简介

核心思想:假设文本串为 s,则对字符集中的每一个字符 c 开一个大小为 |s| 的 bitset Nc,记录 c 出现在 s 中的哪些位置。

用多个模式串 t 去匹配 s,并且求出 ts 中每一次出现的结束位置,那么有这样一个套路:开一个长度为 |s| 的 bitset M 作为答案,一开始每一位都为 1M 的含义:所有为 1 的位为可能的结束位置。遍历 t 中的每一个字符 ti,并将 Nti 左移(数位从小到大是从右往左,而位置是从左到右,注意区分) |t|i 位的结果 Nti 按位与 M,即 MM bitand Nti。最后得到的 M 即为 ts 中所有出现的结束位置。总的时间复杂度为 O(|s||t|ω)

这实际上就是将暴力匹配用 bitset 优化了一下。对于 ti 考虑对可能的结束位置的限制:只有 tis 中的所有出现位置向右移动 |t|i 位后的所有位置,才可能成为 t 最终的结束位置。而将所有限制合起来就可以得到最终的结束位置。

  • 冷知识:bitset 有数值类型的 _Find_first()_Find_next(x) 函数(后者如果没有找到下一个位置会返回 bitset 的大小)。这可以非常方便地帮助我们在 O(nω+c) 的复杂度内找到 bitset 中所有为 1 的位置。具体使用可以看例题 II。

废话不多说,来两道例题感受一下 bitset 的神奇之处。

2. 例题

I. CF914F Substrings in a String

题意简述:给出文本串 s,多次询问 l,r,yys[l:r] 中出现了多少次。带修。|s|,|y|105

太经典了。

注意到这个带修就很恶心,普通的 SAM 做不起来。接下来有两个选择:

  1. 巨大多难写的分块 SAM。这里安利一下 我的 SAM 学习笔记
  2. 注意到时限竟然有 6s,而数据范围只有 105,于是 bitset 暴力硬莽就完事了嗷!!!!11

具体实现方法和上题几乎一模一样,带修直接修改 bitset,查询的时候将 M 分别右移 l+|y|2 位和 r 位,使用类似前缀和的思想去掉区间带来的影响(即计算 [l+|y|1,n][r+1,n] 的贡献差)。计算得到的两个 bitset 分别有多少 1,作差,再与 0 取最大值即可(因为 l+|y|2 可能大于 r,而 bitset 的 count 应该是 unsigned int,老坑了)。

这样我们就用非常简单的方法切掉了一道 *3000 的题目。

#include <bits/stdc++.h>
using namespace std;

const int N=1e5;
const int S=26;

int n,q,len,tp,l,r;
char s[N],t[N],ch;
bitset <N> c[S],ans;

int main(){
	scanf("%s%d",s,&q),n=strlen(s);
	for(int i=0;i<n;i++)c[s[i]-'a'][i]=1;
	while(q--){
		scanf("%d%d",&tp,&l),l--;
		if(tp==1)scanf("%s",&ch),c[s[l]-'a'][l]=0,c[(s[l]=ch)-'a'][l]=1;
		else{
			scanf("%d%s",&r,t),len=strlen(t),ans.set();
			for(int i=0;i<len;i++)ans&=c[t[i]-'a']<<len-i-1;
			cout<<max(0,(int)((ans>>l+len-1).count()-(ans>>r).count()))<<"\n";
		}
	}
	return 0;
}

II. CF963D Frequency of String

题意简述:给出 s,多次询问 k,m 求出 m 至少出现了 k 次的 s 的子串的最小长度。|s|,|mi|105

这里给出一个引理:互不相同的长度之和为 M 的字符串的 endpos 集合大小之和不超过 nM

证明:互不相同的长度为 L 的字符串 endpos 集合大小之和为 nL+1,而 LM 意味着最多只有 M 种长度,得证。

那么可以 SAM + 线段树合并 or ACAM or bitset 求出 endpos 集合,然后直接暴力枚举即可。如果使用 bitset 则要用到上面提到的 _Find_first()_Find_next(x) 函数,时间复杂度为 O(|s||m|ω+|s|m)

bitset,永远滴神!!!!!11111

#include <bits/stdc++.h>
using namespace std;

const int N=1e5;
const int S=26;

int n,q,len,k,cnt,p[N];
char s[N],t[N];
bitset <N> c[S],ans;

int main(){
	scanf("%s%d",s,&q),n=strlen(s);
	for(int i=0;i<n;i++)c[s[i]-'a'][i]=1;
	while(q--){
		scanf("%d%s",&k,t),ans.set(),len=strlen(t),cnt=0;
		for(int i=0;i<len;i++)ans&=c[t[i]-'a']<<len-i-1;
		for(int it=ans._Find_first();it!=N;it=ans._Find_next(it))p[++cnt]=it;
		int ans=1e9; for(int i=k;i<=cnt;i++)ans=min(ans,p[i]-p[i-k+1]);
		cout<<(ans==1e9?-1:ans+len)<<endl;
	}
	return 0;
}
posted @   qAlex_Weiq  阅读(2972)  评论(4编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示