题解:UVA1672 不相交的正规表达式 Disjoint Regular Expressions

· · 题解

爆肝两天+中间的夜里终于调出来了,重构若干次最后是一个 9kb 屎山。

题意:

给出包含 Alternation(『候选』)、Concatenation(『连接』)、Kleene star(不清楚中文名称,后文成为『Kleene』)的正则表达式两个,输出一个长度最短非空的可以匹配两个表达式的字符串,无解需报告。随机多测。

没有多测的弱化版:ICPC 防 AK 超级难题。可见本题应该评黑。

思路很明显,建立两个表达式的 DFA 然后想办法匹配。超级大模拟,难写难调。

Step 1

输入字符串,解释格式。讲讲我的处理方法:

更加标准的做法是先识别成语法树(这题的运算都是单目和双目,因此最终可以识别为一个二叉语法树),但是我直接边识别边建立自动机了。

一元运算符『Kleene』是左元运算符,烦人,先把串 reverse 一下,就变成了右元的单目运算符,一旦遇到,就先于后面的『表达式块』(后面有定义)进行结合。

考虑设计函数 pair<int, int> build(int l, int r) 为识别一段正则表达式的格式并建立 DFA,返回 DFA 的初始节点和汇总的接收态。

定义一个『表达式块』为:一个『括号』和里面的东西,一个『字符』,一个『Kleene』后接着一个表达式块。(下简称为『块』)

当前的状态是什么:空;单个块;若干『候选者』,每一个『候选者』是若干『块』。

空:直接新建一个节点,返回的自动机的初始节点和汇总的接收态都是这个节点。

单个块:如果是『括号』包起来的就直接 build 被括起来的串(当然代码实现的时候再父层可以直接调用括号内);如果是『Kleene』开头的,build 后面的东西然套一个『Kleene』自动机壳(后文会提到)即可(当然代码中也可以直接让父层处理)。

若干『候选者』,每一个『候选者』是若干『块』:分离『块』和候选符,依次 build 每一个块,对于每一个候选的若干块,套上一个『Concatenation』自动机壳,最后套上一个『Alternation』自动机壳。

具体逻辑看代码,码风清晰易懂,带注释。

Step 2

设计自动机。

字符:直接建立两个节点只能用这一个字符转移即可。

操作符们(我愿意称之为壳):下令 st 为旧自动机的初始态和终止态,ST 为新自动机的初始态和终止态。没有化 st 中间的边因为不是操作符管的事。边均为 eps 类转移。

『Kleene』壳:

      +-----+
      v     |
S---->s     t---->T
      |     ^
      +-----+

『Alternation』壳(大于等于两个『候选者』同理):

S---->s1    t1---->T
|                  ^
+---->s2    t2-----+
...

『Concatenation』壳(大于等于两个被连接的自动机就继续连接就对了):

s1    t1---->s2     t2...

Step 3

DFA 的复合,具体逻辑是:对于旧的两个 DFA 上各选一个点的每一对点,在新的 DFA 上建立一个点对应这个状态,DFA 的 eps 类转移是专一一个原 DFA 的 eps 类转移,而非 eps 类转移是必须同时转移。

Step 4

在复合的自动机上找串。若一个非 eps 转移最后可以通过若干 eps 转移转移到接受态,那么这个转移可以被作为最后一个字符。最后看初始状态到最近的最后一个字符的最短路即可(非 eps 类的路径长度定义为 1,eps 类定义为 0),跑多源 0-1 BFS。

Step 5

写代码,准备好呕吐袋和棺材。

#include <bits/stdc++.h>
#define all(x) (x).begin(), (x).end()
using namespace std;
using pii = pair<int, int>;
using vi = vector<int>;
using vvi = vector<vi>;
using vpii = vector<pii>;
struct regular_expression {
    int bracket_match[1005], bracket_stack[1005], begin, end, cnt;
    vi eps_move[1005];
    vector<int> mv[1005][128];
    string s;
    regular_expression(string &S) : s(S) {
        memset(bracket_match, 0, sizeof(bracket_match));
        bracket_stack[0] = 0;
        for (int i = 0; i < s.size(); i++) { // 处理括号
            if (s[i] == ')') {
                s[i] = '(';
                bracket_stack[++bracket_stack[0]] = i;
            } else if (s[i] == '(') {
                s[i] = ')';
                bracket_match[bracket_stack[bracket_stack[0]--]] = i;
            }
        }
        pii build_ans = build(0, s.size());
        begin = build_ans.first;
        end = build_ans.second;
    }
    void print() { // 调试用的输出
        for (int i = 1; i <= cnt; i++) {
            for (char c = 'a'; c <= 'z'; c++) {
                for (int j : mv[i][c]) {
                    cout << i << " " << j << " " << c << endl;
                }
            }
            for (int j : eps_move[i]) {
                cout << i << " " << j << endl;
            }
        }
    }
    // 后面的是简单的辅助函数
    int new_node() { // 建立新节点
        return ++cnt;
    }
    int first_not_kleene(int x) { // 一段连续的 * 是与一个 * 等价的,直接找到后面的第一个非 * 就是这些 * 复制的『块』
        while (s[x] == '*') {
            x++;
        }
        return x;
    }
    int get_block_end(int x) { // 通过『块』首找『块』尾
        if (s[x] == '(') {
            return bracket_match[x] + 1; // 括号,提前预处理
        }
        return x + 1; // 字符,长度是 1 的『块』
    }
    // 后三个是题解中所谓的『套一个自动机壳』
    pii kleene(int b, int e) { // 将自动机转为带『kleene』的自动机器
        eps_move[e].push_back(b);
        eps_move[b].push_back(e);
        int new_b = new_node();
        int new_e = new_node();
        eps_move[new_b].push_back(b);
        eps_move[e].push_back(new_e);
        return {new_b, new_e};
    }
    pii concatenate(vpii &block) { // 将若干自动机前后拼接
        if (block.empty()) {
            int x = new_node();
            return {x, x};
        }
        for (int i = 0; i + 1 < block.size(); i++) {
            eps_move[block[i].second].push_back(block[i + 1].first);
        }
        return {block[0].first, block.back().second};
    }
    pii alternate(vpii &alternates) { // 将若干『候选』整合为一个自动机
        if (alternates.empty()) {
            int x = new_node();
            int y = new_node();
            return {x, y};
        }
        if (alternates.size() == 1) {
            return alternates[0];
        }
        int x = new_node();
        int y = new_node();
        for (pii &i : alternates) {
            eps_move[x].push_back(i.first);
            eps_move[i.second].push_back(y);
        }
        return {x, y};
    }
    pii build(int l, int r) {
        if (l == r) { // 『空』
            int x = new_node();
            return {x, x};
        }
        if (l + 1 == r) { // 『字符』
            int x = new_node();
            int y = new_node();
            mv[x][s[l]].push_back(y);
            return {x, y};
        }
        vector<vpii> block{{}};
        while (l < r) {
            if (s[l] == '|') { // 遇到『候选』标识符,新建一个『kleene』『块』组
                block.push_back({});
                l++;
            } else if (s[l] == '*') { // 遇到『kleene』,找到这一段『kleene』控制的『块』并套一个『kleene』壳当做普通块
                l = first_not_kleene(l);
                pii block_ans = build(l, get_block_end(l));
                block.back().push_back(kleene(block_ans.first, block_ans.second));
                l = get_block_end(l);
            } else if (s[l] == '(') { // 遇到『括号』,正常递归
                block.back().push_back(build(l + 1, bracket_match[l]));
                l = bracket_match[l] + 1;
            } else { // 遇到『字符』,建立一个自动机
                block.back().push_back(build(l, l + 1));
                l++;
            }
        }
        vpii alternates;
        for (vpii &i : block) {
            alternates.push_back(concatenate(i)); // 分别套上『链接』壳
        }
        return alternate(alternates); // 套上『候选』壳
    }
} *pa1, *pa2;
bool vis[1005][1005]; // 访问过
bool all_eps[1005][1005]; // 有能力全 eps 到达终点
pair<pii, char> nxt[1005][1005]; // 下一个转移
int len[1005][1005]; // 非 0 min len
deque<pii> q;
vector<pair<pii, char> > e[1005][1005]; // 复合的自动机
void dfs(int pos1, int pos2) {
    if (vis[pos1][pos2])
        return;
    vis[pos1][pos2] = 1;
    if (pos1 == pa1->end && pos2 == pa2->end) {
        all_eps[pos1][pos2] = 1;
        return;
    }
    for (int i : pa1->eps_move[pos1]) {
        dfs(i, pos2);
        e[i][pos2].push_back({{pos1, pos2}, 'E'});
        if (all_eps[i][pos2] && !all_eps[pos1][pos2])
            all_eps[pos1][pos2] = 1;
    }
    for (int j : pa2->eps_move[pos2]) {
        dfs(pos1, j);
        e[pos1][j].push_back({{pos1, pos2}, 'E'});
        if (all_eps[pos1][j] && !all_eps[pos1][pos2])
            all_eps[pos1][pos2] = 1;
    }
    for (char c = 'a'; c <= 'z'; c++) {
        for (int i : pa1->mv[pos1][c]) {
            for (int j : pa2->mv[pos2][c]) {
                dfs(i, j);
                e[i][j].push_back({{pos1, pos2}, c});
                if (all_eps[i][j]) { // 走一个有意义转移可以到达一个一直走无意义转移走到 {end, end},那么这个转移走完之后可以直接作为结束字符
                    len[pos1][pos2] = 1;
                    nxt[pos1][pos2] = {{pa1->end, pa2->end}, c};
                    q.push_back({pos1, pos2});
                }
            }
        }
    }
}
void bfs() {
    while (!q.empty()) {
        pii t = q.front();
        q.pop_front();
        for (pair<pii, char> &i : e[t.first][t.second]) {
            if (len[i.first.first][i.first.second] > len[t.first][t.second] + (i.second != 'E')) {
                len[i.first.first][i.first.second] = len[t.first][t.second] + (i.second != 'E');
                nxt[i.first.first][i.first.second] = {t, i.second};
                if (i.second == 'E')
                    q.push_front(i.first);
                else
                    q.push_back(i.first);
            }
        }
    }
}
signed main() {
    string str1, str2;
    while (cin >> str1) {
        cin >> str2;
        reverse(all(str1));
        reverse(all(str2));
        regular_expression a1(str1), a2(str2);
        pa1 = &a1;
        pa2 = &a2;
        for (int i = 0; i <= a1.cnt; i++) {
            for (int j = 0; j <= a2.cnt; j++) {
                vis[i][j] = all_eps[i][j] = 0;
                len[i][j] = 100000;
                e[i][j].clear();
            }
        }
        q.clear();
        dfs(a1.begin, a2.begin); // dfs 找出所有的末字符,同时建立复合自动机的反图
        bfs(); // bfs 找出 {begin, begin} 到任意一个可以作为末字符的转移的最段路
        if (len[a1.begin][a2.begin] > 10000) { // 无解,输出 Correct
            cout << "Correct" << endl;
        } else {
            cout << "Wrong" << endl;
            string ansans;
            pii now = {a1.begin, a2.begin};
            while (now != pii(a1.end, a2.end)) { // 根据搜索结果爬回串
                ansans += nxt[now.first][now.second].second;
                if (len[now.first][now.second] == 1 && nxt[now.first][now.second].second != 'E' && nxt[now.first][now.second].second != 'O' && nxt[now.first][now.second].second != 'F')
                    break;
                now = {nxt[now.first][now.second].first.first, nxt[now.first][now.second].first.second};
            }
            reverse(ansans.begin(), ansans.end()); // 倒腾半天发现串是反的,reverse 一下就对了
            for (char i : ansans)
                if (i != 'E' && i != 'O' && i != 'F')
                    cout << i;
            cout << endl;
        }
    }
    return 0;
}