首页 > 其他分享 >P3167 [CQOI2014]通配符匹配 题解

P3167 [CQOI2014]通配符匹配 题解

时间:2022-11-11 22:01:40浏览次数:47  
标签:匹配 P3167 题解 KMP int 权值 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();
	}
}

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

标签:匹配,P3167,题解,KMP,int,权值,CQOI2014,小串,问号
From: https://www.cnblogs.com/closureshop/p/16882153.html

相关文章

  • 题解 [ABC227F] Treasure Hunting
    简单DP,当时赛时没做出来,怎么回事呢。在DP过程中并不好维护前\(k\)大都是什么,没有办法把它放到状态里,因此我们枚举第\(k\)大数的下标\(a_{x,y}\)。然后就好办了,设......
  • CF1750E 题解
    没看懂官方社论,只好自力更生了。我们很容易知道,对于一段区间,其代价就是多余的括号数(左右括号中多出来的括号数)加上不属于多余括号,但没有匹配的左或右括号数。即左括号总量......
  • CF1252K Addition Robot 题解
    题目链接思路对于\(A=A+B\),\(B=A+B\)这种的递推式可以考虑矩阵加速递推,可得:\[\left(\begin{matrix}A+B&B\end{matrix}\right)=\left(\begin{ma......
  • P5443 [APIO2019] 桥梁 题解
    容易得出一种暴力算法:将询问按\(w\)排序,将没有修改的边按\(d\)排序。对于每个询问\((t_i,s_i,w_i)\),做两部分操作(这里\(t\)是时间的意思):将没有修改的边中满足\(d......
  • P1587 [NOI2016] 循环之美 题解
    P1587[NOI2016]循环之美这道题我推到后面推不下去了,最后还是看了题解。还是切不了这种题唉。前置知识:杜教筛开始时看不出什么,我们先用经验和手玩来找一下规律。我们......
  • P6628 [省选联考 2020 B 卷] 丁香之路 题解
    图论、贪心好题。枚举每一个朋友,设一个朋友从\(s\)出发,到\(t\)结束。那么如果用边来表示其行动轨迹,必然是\(s,t\)有奇数度,其它点均为偶数度。如果在\(s,t\)之间连......
  • CF464D World of Darkraft - 2 题解
    期望好题,可以帮助我们更加深入地了解期望。由于\(k\)种装备相同,所以只要计算卖掉一种装备得到的金币期望乘以\(k\)就行了。为了避免讨论当前状态的概率,期望DP通常......
  • CF1316E Team Building 题解
    可能更好的阅读体验题目传送门题目大意传送门你需要组建一支排球队。为了组织一支排球队,你需要为队伍里的\(p\)个不同的位置,从\(n\)个人中选出\(p\)个人,且每个位......
  • CF1735E题解
    钦定\(p_1=0,p_2>0\),不难证明如果有解则一定存在\(p_2>p_1\)的解。考虑枚举和\(d_{1,1}\)是相同楼房,则\(p_2\)对于每一种情况有两种可能的位置:\(d_{1,1}+d_{2,i}\)......
  • P3188 [HNOI2007]梦幻岛宝珠 题解
    一道不错的dp题,下令原背包容量为\(m\)。注意到\(w_i=a\times2^b\)中\(a,b\)都比较小,尝试按照\(a\)或者\(b\)分组然后合并,但是显然如果我们按照\(a\)分组就......