自动状态机,也称为自动机(Automaton),是理论计算机科学中的一个概念。它是一个抽象的机器模型,可以理解为一种能够根据输入序列和当前状态,进行状态转换并可能产生输出的系统。
自动机主要有两种类型:确定性自动机(Deterministic Finite Automaton, DFA)和非确定性自动机(Nondeterministic Finite Automaton, NFA)。在确定性自动机中,对于每一个状态和输入,都有唯一的下一个状态。而在非确定性自动机中,可能存在多个可能的下一个状态。
自动机的算法原理通常涉及以下几个部分:
- 状态集合:一个自动机有一系列的状态,其中有一个初始状态,并且可能有一个或多个接受状态(也称为最终状态)。
- 输入字母表:自动机接受一系列的输入符号,这些符号组成了输入字母表。
- 状态转移函数:定义了对于每个状态和每个输入符号,自动机应该如何改变其状态。
- 接受条件:定义了什么样的输入序列被接受(即,使得自动机到达接受状态)。
自动机被广泛应用于计算机科学的许多领域,其中包括:
- 编译器和解析器:自动机被用于词法分析(把代码字符串分解成“单词”或符号)和语法分析(确保代码的结构符合语法规则)。
- 字符串匹配:自动机可以用于实现高效的字符串搜索算法,例如 KMP 算法和 Aho-Corasick 算法。
- 协议设计:网络和通信协议可以使用自动机进行建模,以帮助设计和验证协议的行为。
- 硬件设计和验证:自动机可用于模拟和验证电子设备和电路的行为。
自动机提供了一种结构化的方法,用于理解和设计涉及输入、输出和状态转换的系统。
以KMP算法为例: Knuth-Morris-Pratt(KMP)算法是一个经典的字符串匹配算法,它利用自动状态机的概念来实现高效的搜索。
KMP 算法的关键思想是,当匹配失败时,可以利用之前已经匹配过的信息,避免重新从头开始匹配。为了实现这一点,KMP 算法预处理模式串,构造一个“部分匹配表”或“失败函数”,也可以看作是构造了一个 DFA。
以查找模式串 ABABAC 为例,我们首先构造部分匹配表:
字符 | 失败函数值(最长公共前后缀长度) |
A | 0 |
AB | 0 |
ABA | 1 |
ABAB | 2 |
ABABA| 3 |
ABABAC | 0 |
这个部分匹配表是如何构造的呢?取每一个子串,观察其最长公共前后缀。比如 ABA,它的前缀有 A、AB,后缀有 A、BA,最长的公共前后缀是 A,长度为 1,所以 ABA 对应的失败函数值为 1。
接着我们以这个模式串,使用 DFA 进行匹配。假设输入字符串为 ABABABABAC,我们从左向右扫描。一开始,模式串与输入串的前五个字符匹配成功(ABABA)。当模式串的下一个字符 B 与输入串的下一个字符 B 不匹配时,我们可以通过查找部分匹配表,了解应该回退到哪个位置,以便尽可能多地重复使用已经匹配的字符。在这个例子中,因为 ABABA 的最长公共前后缀为 ABA,所以我们将模式串右移 2 位,跳过公共前后缀,然后继续匹配。
最终,模式串完全匹配输入串的一个子串,算法返回匹配成功。
这种自动机模型的使用,使得 KMP 算法的复杂度降低到了线性时间。因为每个字符只需被扫描一次,无论是否匹配成功,算法都会继续运行。
#include <vector>
#include <string>
#include <iostream>
using namespace std;
// 构造部分匹配表
vector<int> buildFailureFunction(const string& pattern) {
int patternSize = pattern.size();
vector<int> failureFunction(patternSize + 1);
failureFunction[0] = failureFunction[1] = 0;
for (int i = 2; i <= patternSize; i++) {
int j = failureFunction[i - 1];
while (true) {
if (pattern[j] == pattern[i - 1]) {
failureFunction[i] = j + 1;
break;
}
if (j == 0) {
failureFunction[i] = 0;
break;
}
j = failureFunction[j];
}
}
return failureFunction;
}
// KMP 字符串匹配函数
void KMP(const string& text, const string& pattern) {
int textSize = text.size(), patternSize = pattern.size();
vector<int> failureFunction = buildFailureFunction(pattern);
int i = 0, j = 0;
while (i < textSize) {
if (text[i] == pattern[j]) {
i++, j++;
if (j == patternSize) {
cout << "Pattern found at index: " << i - j << "\n";
j = failureFunction[j];
}
} else if (i < textSize && text[i] != pattern[j]) {
if (j != 0)
j = failureFunction[j];
else
i++;
}
}
}
int main() {
string text = "ABABABDAAAAAABABAD";
string pattern = "ABABAC";
KMP(text, pattern);
return 0;
}
主要步骤:
while (i < textSize):这是主循环,遍历整个文本字符串。if (text[i] == pattern[j]):这是在检查当前的文本字符和模式字符是否匹配。如果匹配,则将两个索引i和j都增加 1,并检查是否已经完成了模式的匹配(即j == patternSize)。如果已经匹配完整个模式,就打印出模式在文本中的开始位置(即i - j),然后使用部分匹配表(failureFunction)来决定下一步的j值。else if (i < textSize && text[i] != pattern[j]):这是在当前的文本字符和模式字符不匹配的情况下。这时,如果j不为 0(表示已经有部分匹配),就需要利用部分匹配表来更新j的值,这样就可以避免从头开始匹配。否则,如果j为 0,就将i加 1,即移动到下一个文本字符。
通过这种方式,KMP算法可以在文本字符串中高效地查找模式字符串,即使在部分匹配后遇到不匹配的情况,也可以利用已经匹配的部分避免从头开始匹配。这是KMP算法的主要优点,也是它比传统的字符串匹配算法(如朴素的暴力匹配)更高效的原因。