M. Subsequence(The Preliminary Contest for ICPC China Nanchang National Invitational 2019)

题目描述:

Give a string S and N string Ti​ , determine whether Ti​ is a subsequence of S.

If ti is subsequence of S, print YES,else print NO.

If there is an array {K1,K2,K3,⋯ ,Km},so that 1≤K1<K2<K3<⋯<Km≤N1  N1≤K1​<K2​<K3​<⋯<Km​≤N and Ski=Ti(1≤i≤m), then Ti is a subsequence of S.

Input

The first line is one string SSS,length(S) ≤100000

The second line is one positive integer N,N≤100000

Then next n lines,every line is a string Ti length(Ti​) ≤1000

Output

Print N lines. If the Ti​ is subsequence of S, print YES, else print NO.

样例输入

abcdefg
3
abc
adg
cba

样例输出

YES
YES
NO

分析:

扫描二维码关注公众号,回复: 6097239 查看本文章

本题属于字符串匹配问题。不同于经典的字符串匹配,子串只要按顺序匹配于主串,而不用是真正意义上的子串(连续)。

本题是在线查询题,对时间要求特别高,暴力做法三秒内必定无法完成。

方法一:(通过百分之七十样例)

通过哈希映射提高匹配速度,大约比暴力做法快26倍。因为还是超时了,所以不过多介绍,只是将各个字符出现的位置都与该字符构建一个键值对,匹配时提高速度而已。

#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;
unordered_map<char,vector<int> > m;
unordered_map<char,int> m1;
int main(){
	string s;
	cin>>s;
	for(int i = 0;i < s.size();i++){
		m[s[i]].push_back(i);
	}
	int N,t;
	cin>>N;
	while(N--){
		string p;
		cin>>p;
		int pre = -1;
		bool flag = true;
		for(int i = 0;i < p.size();i++){
			if(!m.count(p[i])){
				flag = false;
				break;
			}
			bool f = true;
			if(!m1[p[i]])	t = m1[p[i]];
			else	t = 0;
			for(int j = t;j < m[p[i]].size();j++){
				if(m[p[i]][j] > pre){
					pre = m[p[i]][j];
					m1[p[i]] = j;
					f = false;
					break;
				}
			}
			if(f){
				flag = false;
				break;
			}	
		}
		if(flag)	cout<<"YES"<<endl;
		else	cout<<"NO"<<endl;
		m1.clear();
	}
	return 0;
} 

 方法二:(AC版本)

观察下方法一,每次匹配还是要用二重循环,虽然第二层循环遍历哈希表执行次数并不多,但是超时了说明相比于一重循环还是多了很多操作,于是想办法优化成一重循环。

暴力做法的思路是,遍历模式串,找到主串中第一个与遍历到的字符相等的位置,继续下一次匹配。比如主串acbdce,模式串abc。遍历abc,a在第一个位置上匹配了,继续在主串上寻找b,第三个位置上找到了,再寻找c,第五个位置发现了c,于是匹配过程结束。经典字符串算法的优化策略是一旦不匹配快速右移模式串,本题也可采用类似办法。在a位置上匹配了,如果可以快速找到主串上b的位置,而不用一个个遍历,那么速度必然可以提高很多。于是可以记录下主串s上的每个位置后面各个字符第一次出现的位置,空间换时间。

虽然题目没说,但是所有测试用例出现的字符串仅仅涉及到小写的26个字母。

第一个问题,如何记录下每个位置后各个字符首次出现的位置。

可以采用二维数组存储,next1[maxn][26],第二维从0-25分别表示a到z的26个字母。(为啥数组名不直接写next?貌似c++有个库函数就叫next,求排列用的,命名为next会报错)。主串s前面需要有个字符作为哨兵,如果定义为char*类型,直接从第二个位置开始读入字符即可,定义为string在主串s前面随便添加一个字符即可。为什么需要哨兵?因为我们在第一次匹配时,需要知道第一次匹配成功的位置在哪,没有哨兵结点,只能另外定义一个数组记录下各个字符首次出现的位置了。

第二个问题:如何高效的给next1数组赋值。

本题maxn可达十万,所以效率很重要。第一种方法,动态规划。从后往前,每次将后面一个字符的映射复制给前一个字符,再更改后一个字符对于前面字符的映射即可。由于第二重循环每次仅执行26次,复杂度就是线性级别的。

for(int i = n - 2;i >= 0;i--){
    for(int j = 0;j < 26;j++){
        next1[i][j] = next1[i+1][j];
    }
    next1[i][s[i+1]-'a'] = i + 1;
}

第二种方法。本质也是动态规划,方向从前向后,每次遍历一个字符,都将该字符上一次出现位置之后所有字符对该字符的映射设置为当前字符下标。第二重循环最多执行26次,应该比方法一更加高效。

for(int i = 1;i < n;i++){
    int id = s[i] - 'a';
    for(int j = i - 1;j >= ls[id];j--){
        next1[j][id] = i;
    }
    ls[id] = i;
}

本题的难点就在于动态规划构建next表,字符串匹配的过程一重循环即可,具体见代码。

需要注意的是,使用cin读入字符,最坏情况下需要读入1000次,每次字符长度十万,所以只能通过百分之九十的样例,加上ios::sync_with_stdio(false);语句后便可顺利ac,否则,用字符数组存储字符串然后用scanf读入也可以。scanf读入的速度有人测试了是cin的七倍左右(具体多少倍未曾验证)。拷贝下网上的说法:

cin慢是有原因的,其实默认的时候,cin与stdin总是保持同步的,也就是说这两种方法可以混用,而不必担心文件指针混乱,同时cout和stdout也一样,两者混用不会输出顺序错乱。正因为这个兼容性的特性,导致cin有许多额外的开销,如何禁用这个特性呢?只需一个语句std::ios::sync_with_stdio(false);,这样就可以取消cin于stdin的同步了。

#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 100005;
int next1[maxn][26];
int first[26],ls[26];
int main(){
    ios::sync_with_stdio(false);
    string s,str;
    s = "a";//哨兵
    cin>>str;
    s += str;
    int n = s.size();
    memset(next1,0,sizeof(next1));
    for(int i = 1;i < n;i++){
        int id = s[i] - 'a';
        for(int j = i - 1;j >= ls[id];j--){
            next1[j][id] = i;
        }
        ls[id] = i;
    }
    int N;
    cin>>N;
    while(N--){
	string p;
	cin>>p;
	int i,j = 0;
        for(i = 0;i < p.size();i++){
            int id = p[i] - 'a';
	    j = next1[j][id];
	    if(!j)	break;
        }
	if(i == p.size())	cout<<"YES"<<endl;
	else	cout<<"NO"<<endl;
    }
    return 0;
} 

猜你喜欢

转载自blog.csdn.net/qq_30277239/article/details/89434516