• 「Wallace 笔记」序列自动机 简单入门


    基本概念

    记号

    同为 DFA 的记号。

    • (Sigma):字符集;
    • (Q):状态集;
    • (q_0(in Q)):起始状态;
    • (F(in Q)):接受状态集;
    • (delta):转移函数。
      • (delta(x, c)) 表示状态 (x) 的字符 (c) 的转移。若无记为 ( ext{null})
      • (delta(x, S)) 其中 (S) 为字符串,等价于 (delta(delta(x, S[1, |S| - 1]), S_{|S|}))

    定义

    序列自动机是接受且仅接受一个字符串的子序列的自动机。

    如当 (Sigma = { exttt{a, b, c} })(S = exttt{abac}) 时,其序列自动机为:

    Udi5bd.png

    状态

    对字符串 (S) 构建序列自动机,那么这个自动机中存在 (|S| + 1) 个状态。

    对于字符串 (T),状态 (delta(q_0, T)) 表示 (T) 作为 (S) 的子序列,在 (S) 中第一次出现的末端位置。

    (delta(q_0, T) = ext{null}),则说明 (T) 不是 (S) 的子序列。

    根据定义,序列自动机上的所有状态都是接受状态((F = Q))。

    转移函数

    转移函数 (delta) 的设计,我们可以贪心地认为 (delta(x, c)) 就是 字符串中位置 (x) 之后,字符 (c) 下一次的出现位置

    但很显然之后可能会有多个 (c),为什么选取最前面的呢?假如说 (i < j),那么后缀 (S[j, |S|]) 中的所有子序列一定被完全包含在后缀 (S[i, |S|]) 中。换句话说,(S[j, |S|]) 的所有子序列的集合为 (S[i, |S|]) 所有子序列的子集。于是不难发现后面的一定不会优于前面。

    形式化地讲,(delta(x, c) = min{ y | S_y = c, y > x })。如果不存在不妨设为 (-1) 表示 ( ext{null})

    构造算法

    朴素算法

    首先,根据上文对转移函数的讨论,我们可以从后往前扫,逐位构建。

    对于状态 (i),假如已经求出了 (delta(i, c)) 的转移,考虑如何推出状态 (i - 1) 的转移函数。

    不难发现,只有一个位置,即 (S_i) 的值的位置上,是需要变动的,那么:(delta(i - 1, S_i) = i)

    其他字符的转移,只需要直接复制下来即可。这样的算法的时空复杂度为 (O(|S| imes |Sigma|))

    代码非常简单:

    for (register int i = 1; i <= m; i++) // 最后一位的所有转移置为 null
    	next[n][i] = -1;
    for (register int i = n; i; i--) { // 倒序逐位构造
    	for (register int j = 1; j <= m; j++) // 先复制前一轮的结果
    		next[i - 1][j] = next[i][j];
    	next[i - 1][dat[i]] = i; // 更新当前字符的转移
    }
    

    可持久化数组优化

    上一个算法的效率在 (Sigma) 大小过大时都不够优秀,问题在于对前面状态复制了过多信息。

    然而我们只需要改变一个位置的值,把整个赋值显然是浪费。

    这就需要 可持久化 的思想了——我们先对最后一个建出线段树,初始值为 (-1)

    接下来在构建时只需在一个位置更新即可。时空复杂度 (O(|S|log |Sigma|))。较之前都有较大的提高。

    struct segt_node {
    	segt_node* lc;
    	segt_node* rc;
    	int l, r, val;
    	
    	#define mid ((this->l + this->r) >> 1)
    	segt_node(segt_node *last, int pos, int _val) : l(last->l), r(last->r), val(0) {
    		if (this->l == this->r) { val = _val; return; }
    		if (pos <= mid) {
    			rc = last->rc;
    			lc = new segt_node(last->lc, pos, _val);
    		} else {
    			lc = last->lc;
    			rc = new segt_node(last->rc, pos, _val);
    		}
    	}
    	segt_node(int L, int R) : l(L), r(R), val(-1) {
    		if (l == r) return;
    		lc = new segt_node(l, mid);
    		rc = new segt_node(mid + 1, r);
    	}
    	int at(int pos) {
    		if (this->l == this->r) return this->val;
    		if (pos <= mid) return this->lc->at(pos);
    		else return this->rc->at(pos);
    	}
    	#undef mid
    } *root[N];
    
    // main 函数内:
    	root[n] = new segt_node(1, m);
    	for (register int i = n; i; --i)
    		root[i - 1] = new segt_node(root[i], dat[i], i);
    

    二分查找(非显式构建)

    其实还有一种不需要显式构造出的方法。我们建出自动机,实际上就是需要快速计算出转移函数 (delta)

    但假如我们并没有构造出自动机,不过仍能高效得到转移函数的值,好像并不是不可以。

    这里就有一种非常方便的方法:二分查找。

    我们先预处理出 (Sigma) 中每一种值的出现位置,这一可以用 vector 实现。为方便讨论,记 (p(i)) 为字符 (i) 在字符串中的出现位置的集合。

    在做转移时,假设要得到 (delta(x - 1, c)),那么只要 找出集合 (p(c)) 中第一个 (ge c) 的元素

    由于我们预处理 (p) 集合时就是自然有序的,因此只需要二分查找即可(upper_bound)。

    这样的空间可以达到 (O(|S|))

    习题

    后记

    reference:

  • 相关阅读:
    自学mvc4.0 工作当中随笔(在view页面当中循环table,当遇到html标签怎么处理)
    安卓当中的线程和每秒刷一次
    通过后台代码访问前台js
    学习正则表达式记录
    net 当中动态给记事本当中插入值
    session,cookie 等区别
    vss 2.0框架与4.0框架的设置iis区别
    Javascript中char和int的互相转换的代码(转载)
    获得鼠标在页面上的坐标
    HTTP协议header头域
  • 原文地址:https://www.cnblogs.com/-Wallace-/p/13297178.html
Copyright © 2020-2023  润新知