一些有意思的科技(其一)
假设读者已经熟知自动机 \(\text{DP}\) (也就是 \(\text{DP}\) 套 \(\text{DP}\) )
介绍一个黑科技, \(\text{Hopcroft}\) 算法!这里不会讲的太细,只是看到貌似网上鲜有其代码实现,故开篇文章讲一下,建议阅读 \(2021\) 国家集训队论文 里徐哲安写的《浅谈有限状态自动机及其应用》
它可以干什么?帮助压缩自动机大小,只要原本设计的自动机点数 \(N\) 大的不是那么夸张(复杂度为 \(O(N |\Sigma| \log n )\) 就可以用此算法压缩到这种自动机的最小情况!
当然,自动机的两个状态什么时候叫等价还未定义,一般我们只关注最后那些终止点的答案,所以我们先考虑把终止点看作一个大状态,其他点看作非终止状态,然后考虑慢慢找出那些有区别的然后分成一些小状态
算法流程:
-
先将终止状态放进一个集合 \(W\) 里
-
若 \(S\) 已经为空则退出,否则从 \(W\) 中拿出并删掉一个状态 \(A\)
-
枚举一个字符 \(c\) ,标记每个能经过 \(c\) 到达 \(A\) 状态中的一个点的点
-
若一个状态 \(S\) 中存在有点被标记,也有点没被标记,说明 \(S\) 并不能统一其所有点,将其裂开,把被标记的点视作一个大状态 \(X\) ,其余的视作一个大状态 \(Y\) ,若 \(S\) 已经还未从 \(W\) 中取出过,则把 \(W\) 中的 \(S\) 删去,加入 \(X\) 与 \(Y\) ,否则选择其中大小小的加入 \(W\) 即可(证明可以见论文),另一个视作已经取出。然后返回第二步
若实现妥当,可以使时间复杂度为每个点能被任意一种字符到达的总点数乘上其出现在第三步里的次数,因为第四步保证每个新加入的集合都会折半,故总复杂度即为 \(O(N |\Sigma| \log n )\)
实现考虑延迟删除,具体意思可以看代码理解
点击查看代码
inline void init(){
for(int i=1;i<=N;i++)
for(int x=0;x<=sigma;x++)
rto[x][Trans[i][x]].push_back(i);
}
inline void Hopcroft(){
static bool inw[maxs];
static int top,sta[maxs];
for(int o=1;o<=2;o++)
vec[o].clear(),size[o]=0;
for(int i=1;i<=N;i++){
int c=isEnd[i]?1:2;
vec[c].push_back(i),bel[i]=c,size[c]++;
}
tot=2,inw[sta[top=1]=1]=true;
while(top){
int A=sta[top--];
if(!inw[A])continue;
std::vector<int> veca;
for(auto x:vec[A])
if(bel[x]==A)veca.push_back(x);
inw[A]=false,vec[A]=veca;
for(int c=0;c<=sigma;c++){
std::vector<int> X;
for(auto x:veca)
for(auto y:rto[c][x]){
if(pos[bel[y]].empty())
X.push_back(bel[y]);
pos[bel[y]].push_back(y);
}
for(auto S:X){
if(size[S]==(int)pos[S].size()){
pos[S].clear();
continue;
}
int u=++tot;
for(auto x:pos[S])bel[x]=u;
vec[u]=pos[S],size[S]-=(size[u]=vec[u].size());
if(inw[S]||size[u]<=size[S])inw[u]=true,sta[++top]=u;
else inw[S]=true,sta[++top]=S;
pos[S].clear();
}
}
}
for(int i=1;i<=tot;i++){
while(bel[vec[i].back()]!=i)
vec[i].pop_back();
dot[i]=vec[i].back();
end[i]=isEnd[[dot[i]]];
}
for(int i=1;i<=tot;i++)
for(int x=0;x<=sigma;x++)
trans[i][x]=bel[Trans[dot[i]][x]];
}

浙公网安备 33010602011771号