牛客网暑假训练第九场——F-Typing practice(多串并行 优化KMP详解)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kuronekonano/article/details/81876582

链接:https://www.nowcoder.com/acm/contest/147/F
来源:牛客网

时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld
题目描述
Niuniu is practicing typing.

Given n words, Niuniu want to input one of these. He wants to input (at the end) as few characters (without backspace) as possible,
to make at least one of the n words appears (as a suffix) in the text.
Given an operation sequence, Niuniu want to know the answer after every operation.
An operation might input a character or delete the last character.
输入描述:
The first line contains one integer n.
In the following n lines, each line contains a word.
The last line contains the operation sequence.
‘-’ means backspace, and will delete the last character he typed.

He may backspace when there is no characters left, and nothing will happen.

1 <= n <= 4
The total length of n words <= 100000

The length of the operation sequence <= 100000

The words and the sequence only contains lower case letter.
输出描述:
You should output L +1 integers, where L is the length of the operation sequence.

The i-th(index from 0) is the minimum characters to achieve the goal, after the first i operations.
示例1
输入
2
a
bab
baa-
输出
1
1
0
0
0
说明
“” he need input “a” to achieve the goal.
“b” he need input “a” to achieve the goal.
“ba” he need input nothing to achieve the goal.
“baa” he need input nothing to achieve the goal.
“ba” he need input nothing to achieve the goal.
示例2
输入
1
abc
abcd
输出
3
2
1
0
3
说明
suffix not substring.

【想看KMP优化的在后半部分】

题意:给出n个单词和一个输入序列,每个单词长度1e5。给出一个输入序列,长度1e5,输入序列中的’-‘表示退格,即删除字符。现在对于输入序列的每种情况,我们在后面再输入x个字符,使得输入后的序列的后缀与n个单词中的任意一个完全匹配,求最小的x是多少。

根据样例来解释,一个输入序列baa-,一开始这个序列是个空串,那么我们还需要输入a或者bab来使得该串的后缀匹配n个单词中的任意一个,因为输入a这一个字符就能匹配到,所以空串的ans为1。

接着按照输入序列的第一个字符是b,那么应该输入多少使得bxxx…来的后缀匹配单词中的任意一个?仍然是1,只需要输入一个a即能使输入a后的序列ba的后缀匹配一个单词a,接着是ba,不用输入任何字符,其后缀即匹配单词a,baa同理。

第二个样例中只有一个单词abc,对于空串,则必定输入abc三个字符才能匹配到,因此ans是3。之后同理。

题解:如果理解了题意很显然能想到的即是对一个动态变化的主串【即输入序列】匹配一个最长后缀,使其等于n个单词中的一个最长前缀,那么这样输入的字符就能最少,当然与单词长度也有关,但是对于每一个单词,我们都应该对其匹配最长前缀后缀,记录下【单词长度-匹配长度】的最小值即是主串中该位置的ans.

KMP模式串匹配刚好能胜任这个查找最长前缀后缀的功能。我们只需要构造n个单词的Next数组即可对其直接匹配出单词每个位置的最长前缀后缀。

可以用主串同时对n个单词进行匹配。设置一个指针表示输入序列的位置,另一个指针表示主串的匹配位置,很明显这是不同的,如 输入序列 b a c - - c:
其动态产生的序列有
“ ”【空串】
b
ba
bac
ba
b
bc
共7个,如果我们对其进行KMP,明明第三步已经匹配到第三个字符了,但是因为动态串的删减操作,我们必须又从第二个甚至第一个位置开始重新匹配,如何实现匹配指针的回溯。这就用到了两个指针。

可以知道,如果一旦原串输入序列中某个位置得到了结果,就可以直接输出,因为并不会收到后面的字符影响。如已经得到了bac的ans,那么bac这个序列我就再也不会判断到了,该位置的结果固定,直接输出即可,也没有必要保留串bac,因此删减操作可以移动主串的指针,回到之前的位置,如第一个字符b的位置,然后在第二个位置直接赋值新字符c,构成新串bc。

这两个指针,一个是原输入序列上遍历每一个字符的i,另一个是控制根据输入序列得到的真正主串的指针,在真正主串中不会包含删减符号。只会不断的前后移动指针表示动态的串长度。

接着,因为每个单词匹配到主串的位置都不一样,回溯的程度也各不一样,因此要记录下n个单词匹配到主串的位置。这就像某个任务进行到一半,然后中断,临时存档,再去做另一个任务,因此我们对这个记录存档位置的数组称之为checkpoint,这个存档非常有用,不仅仅用于同时进行多个串匹配的中断状态存储,方便切换,而且还用于动态匹配,因为输入序列的删除和增加操作都是连续的,不会从10个字符突然减掉5个,也不会突然更换是个字符中的最后5个,因此我们在删减后反而非常方便的,不用再重新匹配,直接输出上次匹配的最优结果即可。这就相当于一个历史记录。实现了查询回退n步操作。

同时,回退后再增加新字符,这个历史记录也能给每个串继续往后匹配一个指向,已经记录了到第i位模式串匹配到第j位,那么我们就继续用第j+1位去匹配新字符,不匹配则回退到最大前缀。

以上就是该题多串平行KMP匹配的全部过程。但是因为本题正解为Tire图上跑最短路,因此直接KMP还是比较暴力的,甚至因为单词个数较少,直接暴力多串匹配。


关于优化KMP

朴素的KMP按照这个操作写会超时,原因在与可能后台数据有大量的类似于aaaaaaaa的相同字符长串,此处我们用到KMP优化。
可以知道,朴素的KMP中NEXT数组构造,其意义在匹配过程中表示了,如果子串在该位置失配,那么子串匹配指针j应该回溯到的位置。这样就保证了只回溯子串指针,而主串指针一直是增加的。

引用网上的图表即朴素KMP对于模式串aaac的NEXT数组构造如下:
这里写图片描述

我们发现,这个看似优秀的,能够直接将子串指针飞跃回最大前缀的NEXT数组,实际上在连续相同字母中效果不佳,如果有连续1e5个字符’a’,那么根据NEXT的回溯,当匹配主串到aaaaaaa…….aaaaab时【假设中间省略1e4个a】,这一个突如其来的b,将使子串根据NEXT数组以龟速,一个一个回溯迭代O(n)的速度,回退到第0位。我们明知道子串第i个字符和第i+1个字符已经相等了,那么如果在第i+1位不匹配,第i位也不可能匹配的,因此为什么还要回退到i,直接回退到next【i】才是飞速跳跃式的回退。
这里写图片描述
如上,这个不匹配字符b要判断多次才能回到最初位置。
多次冗余比较导致了KMP的低效。

对于一个朴素KMP中构造NEXT数组的模板,我们考虑做一些改动,就像并查集中的路径压缩一样。路径压缩考虑将一个节点的父亲直接牵引至其父亲的父亲,这样避免了多次迭代查找父亲。

我们考虑相邻相同字符的next值,应该是一样的,那么第i位为next【i】,若第i+1位字符等于第i位字符,则next【i+1】=next【i】

对比朴素模板和优化模板:

void kmp_pre(char x[],int m,int next[])
{
    int i,j;
    j=next[0]=-1;
    i=0;
    while(i<m)
    {
        while(j!=-1&&x[i]!=x[j])j=next[j];
        next[++i]=++j;///朴素KMP
    }
}

优化后:

void kmp_pre(char x[],int m,int next[])
{
    int i,j;
    j=next[0]=-1;
    i=0;
    while(i<m)
    {
        while(j!=-1&&x[i]!=x[j])j=next[j];
        ++i,++j;
        next[i]= x[i]==x[j]?next[j]:j;///优化KMP
    }
}

值得注意的是,这个优化已经改变了next数组的本质,即最大前缀后缀匹配,也就是说,我们只是为了查询得更快而进行数据存储方式的优化,对于一些利用next本质的题目,这样的优化可能会出现意想不到的错误,甚至反而会超时。如 POJ2752 ,因此优化KMP不能完全替代朴素KMP

但是不管怎样,在该题中这样的优化有明显效果。没有这个优化就会TLE,加了不仅能过还过的很快。

#include<bits/stdc++.h>
#define LL long long
#define M(a,b) memset(a,b,sizeof b)
#define pb(x) push_back(x)
using namespace std;
const int maxn=100008;
const int inf=0x3f3f3f3f;
int n;
char str[6][maxn],mastr[maxn];
int nxt[6][maxn],len[6],checkpoint[6][maxn];///checkpoint数组记录主串每一位对于不同子串的匹配记录,因为是并行同时匹配,因此不知道每个子串在不同位置的回退过程
///一一记录,这样也保证动态主串前半部分不变信息能够充分利用
inline void kmp_pre(char x[],int m,int num)///优化next数组构造
{
    int i,j;
    j=nxt[num][0]=-1;
    i=0;
    while(i<m)
    {
        while(j!=-1&&x[i]!=x[j]) j=nxt[num][j];
        ++i,++j;
        nxt[num][i]= x[i]==x[j]?nxt[num][j]:j;///说是优化但是已经改变了内部构造所以有些情况反而可能超时
    }
}
int main()///要变化的主串的后缀尽可能匹配字典中单词的前缀,使得加入的字母越少越好,即KMP前缀后缀匹配,每次找到对子串的最大前缀后缀匹配即当前的一个ans
{///即使利用next回退的过程也是寻找最大前缀匹配的过程
    while(scanf("%d",&n)!=EOF)
    {
        int ans=inf;
        for(int i=0;i<n;i++)
        {
            scanf("%s",str[i]);
            kmp_pre(str[i],len[i]=strlen(str[i]),i);
            ans=min(ans,len[i]);
        }
        scanf("%s",mastr);
        int mlen=strlen(mastr);
        printf("%d\n",ans);///对于空串输出最短单词
        int pt=0;///动态主串指针,指向的不是主串中的哪个字符,而是主串变化的长度上的下标位
        for(int i=0;i<mlen;i++)///遍历主串的每一个字符,用于比较和退位指针
        {
            ans=inf;
            if(mastr[i]=='-')///退位,直接取之前的答案最小值输出即可
            {
                if(pt!=0)pt--;
                for(int j=0;j<n;j++)ans=min(ans,len[j]-checkpoint[j][pt]);
                printf("%d\n",ans);
            }
            else///字符增加的时候进行多串并行的KMP匹配
            {
                for(int j=0;j<n;j++)///对每个串比较取取最小值做ans,主体其实和KMP的匹配函数完全一样,只是多个个for循环进行N个串的同时匹配,并继承或记录上一个位置的匹配状态
                {
                    int tmj=checkpoint[j][pt];
                    while(tmj!=-1&&str[j][tmj]!=mastr[i]) tmj=nxt[j][tmj];
                    checkpoint[j][pt+1]=tmj+1;
                }
                pt++;
                for(int j=0;j<n;j++)ans=min(ans,len[j]-checkpoint[j][pt]);
                printf("%d\n",ans);
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/kuronekonano/article/details/81876582