P3167 [CQOI2014]通配符匹配 题解

想了两种做法,第一种拿到了 10 分的好成绩。

而第二种做法不用前缀和,而且还跑的飞快。目前最优解第三尝试卡进最优解未果

不得不说这是一道好题,做完对 KMP 有了更深的理解。

1.(本人的) KMP 究竟哪里错了

首先,看到匹配,第一眼想到 KMP。接着就能发现实际上分隔符 * 实际上就是把一整个字符串分成了若干个小串,匹配完一个才能匹配下一个。

然后有一个显而易见的结论:每一个小串能完全匹配时就要匹配,因为之后的 * 可以匹配任意长的字符串(但要注意匹配最后一个小串终点的特判)。

于是我们可以口胡出一个看起来很对的做法:对每个小串做 KMP,遇到问号直接当做匹配,然后在询问串上跑匹配完一个小串就进入下一个小串。时间复杂度线性。

正如前言所说,你可以在我的提交记录看到夺目的 10 分。为什么错了呢?

考虑这样一个串:aca?c 。按照 KMP 数组的定义,求出的失配指针应该为 \(0,0,1,2,2\)。但实际上,如果我们按照上述步骤来求,答案会是这样:\(0,0,1,2,0\)

为什么呢?设字符串为 \(s\),当前自匹配到 \(j\),失配数组为 \(fail\),那么当我们循环到最后一个字符时,我们要将 \(s_{j+1}\) 也就是 \(s_3\)\(s_5\) 比较。发现不行,这时我们的 \(j\) 会回跳到 \(fail_j\) 也就是 \(s_0\) 处。这似乎是一个正常的 KMP 过程。

但是 KMP 的优化在此时会默认本来为 ?\(s_4\)a 。因为计算机会认为既然 \(s_2\)\(s_4\) 相匹配,那么 \(s_4 = s_2\) 也是理所因当的。这时失配指针往回跳时就没有考虑 \(s_4\) 发生变化的情况,它只会憨憨地认为 accc 不匹配,所以应该跳回起点。

至此,我们的问号直接跳计划宣告破产。

2.跑的飞快的哈希做法

一个串与一个串匹配,除了 KMP ,我们还可以想到哈希。在以 * 为分隔将大串分割为小串后,每一个小串的长度固定。于是可以直接扫询问的字符串,每次提取出当前小串长度的字符串 \(O(1)\) 求哈希值。

说具体点,你首先从起点提取出小串长度的字符串,顺便记录一下最左边乘了几次哈希的单位,然后转移时删除最左边的权值乘最左边乘过的哈希单位就行。

下面是一张图,假设小串长度为 3:

不许说我字丑!

那问号怎么办呢?我们把问号的权值置为 0,动态记录当前询问串中哪几个串与问号对应,将这些点的权值强制赋为 0(方法可参照减去最左端权值的方法),向后推进时暴力转移,将所有与问号对应的下标加一即可。

由于问号最多只有 10 个,所以复杂度就是 \(O(n)\) 带了个 10 的常数。

这一道题还有一些小细节需要注意:

  1. 开头除非有一个 * 挡着,否则第一个小串一定要与询问串前缀匹配。所以要特判,否则会在第三个点 WA。
  2. 到最后除非有一个 * 挡着,否则最后一个串一定要与询问串后缀匹配。如果最后一个串匹配了却不是与后缀匹配的,那么就不能结束,还要继续比较。
  3. 每一次向后推进时,先加上原先串被强制赋为 0 的点的权值,再减去最左边的权值乘上它的幂,(详情参照上图)并加上下一位权值,最后再将当前与问号匹配的点强制赋为 0。如果第一二步顺序乱掉的话会导致被开头是 ? 的数据 hack 掉。因为此时最左边的权值减了两遍。
#include <bits/stdc++.h>
#define ull unsigned long long
using namespace std;
#define rep(i,a,b) for(int i =(a);i <= (b);i ++)
const int maxn = 1e5 + 5;
const ull base = 19260817;
int len[maxn], tot;
bool pd[15];
string s, p[maxn];
ull H[maxn];
bool hjl, hzh;
ull qpow(ull u, ull v)
{
    ull ans = 1;
    while(v)
    {
        if(v & 1) ans *= u;
        u *= u;
        v >>= 1;
    }
    return ans;
}
void solve()
{
	int n = s.size();
	s = ' ' + s;
    ull ha = 0, now = 1;
    bool pd = 0;
    ull en[12], st = -1, cnt = 0, mi[12], big = 0;//st为最左边的下标,en与cnt存储与问号匹配的点,mi统计与问号对应的点乘了几次base,big统计最左端乘了几次base
    rep(i, 1, n)
    {
        int li = i;
        if(pd == 0)
        {
            st = i;
            int m = p[now].size() - 1;
            cnt = 0;big = 1;ha = 0;
            rep(j, 0, m)
            {
                ha *= base;
                if(j > 0) big *= base;
                if(p[now][j] != '?') 
                {
                    ha += (ull)(s[i] - 'a' + 1);
                }
                else
                {
                    en[++ cnt] = i;//顺便记录每个?的初始对应位置
                    mi[cnt] = qpow(base, m - j);
                }
                i ++;
            }
            pd = 1;
            if(ha == H[now]) //一开始提取出的串也要进行匹配 
            {
                int o = 0;
                if(now < tot) now ++, pd = 0, o = 1;
                if(o == 0 && (i >= n || hzh == 1))//即使所有串匹配完了也不一定完全匹配,具体参照上文提醒
                {
                    cout << "YES" << endl;
                    return;
                }
            }
            else if(hjl == 0 && li == 1)
            {
                cout << "NO" << endl;
                return ;
            }
            i --;
            continue;
        }
        rep(j, 1, cnt) ha += (ull)(s[en[j]] - 'a' + 1) * mi[j], en[j] ++;
        ha -= (ull)(s[st] - 'a' + 1) * big;
        ha *= base;
        ha += (ull)(s[i] - 'a' + 1);
        st ++;
        rep(j, 1, cnt) ha -= (ull)(s[en[j]] - 'a' + 1) * mi[j];//注意顺序
        if(ha == H[now])
        {
            int o = 0;
            if(now < tot) now ++, pd = 0, o = 1;
            if(o == 0 && (i >= n || hzh == 1))
            {
                cout << "YES" << endl;
                return;
            }
        }
    }
    cout << "NO" << endl;
    return ;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> s;
	int n = s.size();
	string c = "";
    rep(i, 0, n - 1)
	{
		if(s[i] == '*') 
		{
            if(i == 0) hjl = 1;
            if(i == n - 1) hzh = 1;//如果开头结尾有*,那么没有匹配前后缀的字符串也能视作完全匹配
			if(c != "") p[++ tot] = c, c = "";
		}
		else c = c + s[i];
	}
	if(c != "") p[++ tot] = c;
    rep(i, 1, tot)
    {
        int m = p[i].size();
        rep(j, 0, m - 1)
        {
            H[i] *= base;
            if(p[i][j] != '?') H[i] += (ull)(p[i][j] - 'a' + 1);
        }
    }
	int Q;
	cin >> Q;
	while(Q --)
	{
		cin >> s;
		solve();
	}
}

本篇题解就到这里了,考虑考虑点个推荐,然后去把本菜鸡从最优榜上挤下去?

posted @ 2022-11-11 21:52  _maze  阅读(61)  评论(2编辑  收藏  举报