新年了,工厂 BOSS 要给底下人发新年礼物,其中有一份神秘大奖,但却不知道应该发给谁。
于是,工厂 BOSS 打算让大家玩一个游戏。 一共有 n个字符串排成一排,小明需要从中按顺序选取一部分字符串,使得选出来的字符串顺
序和原顺序一致(也就是从中选出一个子序列),且靠前的字符串 xi 和靠后的字符串 xj 之间
均同时满足如下要求:
• xi 是 xj 的前缀
• xi 是 xj 的后缀
小明需要从中按顺序选取最多的字符串,并且满足如上的要求。工厂 BOSS 最后会给选出最多
字符串的人平分神秘大奖。你能算出选取的最大个数么? 样例输入:第一行输入一个整数 N,紧接着输入 N 行字符串,每个字符串仅包含小写或大写字
母。输入数据总共少于 2×106个字符。 样例输出:输出一个整数,表示最大个数。 样例输入1 5
A
B
AA
BBB
AAA
样例输出1
3
样例输入2
5
A
ABA
BBB
ABABA
AAAAAB
样例输出2
4
初步分析
首先这道题的题目意思很明了,就是要求包含最多字符串的一个子串且第一个字符串得是第二个字符串的前缀和后缀。如果这道题我们用朴素算法暴力求解,枚举所有两个字符串去判断是否满足的话。那么时间复杂度就是O(mN^2) ,m是总共的匹配次数。并且我们需要O(Nk)的全局空间。所以如果当N和k都比较大的时候。肯定会爆内存的。所以我们应该另想办法,其实像这种关于许多字符串的匹配题目,许多都是用Trie树来做的,我们想想这道题可不可以。
首先,我们得预处理一下,我们用int loc_p[i]来记录第i个字符串在Trie树中的节点位置。
然后我们用Trie树去存储所有字符串。用Trie树来做有什么优点呢?没一个终端点所表示的字符串都是它的子树中所有终端点所表示的字符串的前缀(这句话很好理解,想想trie树的insert过程)。但是这样只能解决两个字符串匹配的前缀问题,那后缀的匹配又该怎么办呢?如果我们依然用朴素算法(将两个字符串直接匹配判断后缀的话,其实是很难实现的。因为我们只能知道两个字符串loc_p[i],loc_p[j]值。很难找到这个节点的之前节点的字母是什么,而且依然很慢)。所以我们采用一个比较巧妙的方法,就是再创建一个trie2树按同样的字符串顺序存,不过每次存的时候存的是字符串的反转。(这样就可以把后缀匹配又变为了前缀匹配,才能利用Trie树的特点)。所以任意两个字符串第i个和第j个。如果分别在两个树中节点loc_p[i]是loc_p[j]的父亲节点。就表示i是j的前缀,同时i是j的后缀。
具体实现
怎么去判断两个节点属于祖孙(父子)关系,我们都知道树的结构中每一个节点可以有多个子节点,却只有一个父节点。而Trie树里面是没有记录当前节点p的父节点的信息记录的。所以我们自己用一个fa[i]模拟父子树。用fa[i]表示节点i的父亲结点。然后通过深度递归去判断一个节点是否是另一个节点的祖先。而这个数组我们是在insert时就更新的。
直接上代码
#include <iostream>
#include <string.h>
using namespace std;
const int MAX_N=10000;
const int MAX_C=52;
//记录每个节点的父亲结点
int fa1[MAX_N];
int fa2[MAX_N];
//记录第i个字符串在trie树里最后一个字符的节点位置
int loc_p1[MAX_N];
int loc_p2[MAX_N];
typedef struct trie{
int* ch[MAX_N];
int tot;
int cnt[MAX_N];
void init(){
memset(ch,NULL, sizeof(ch));
memset(cnt,0, sizeof(cnt));
tot=0;
}
int inser(char* str,int pre_loction,int* fa) {
int p = 0;
for (int i = 0; str[i]; ++i) {
if (ch[p] == NULL) {
ch[p] = new int[MAX_C];
memset(ch[p], -1, sizeof(int) * MAX_C);
}
if (ch[p][str[i] - 'a'] == -1) {
ch[p][str[i] - 'a'] = ++tot;
}
p = ch[p][str[i] - 'a'];
//这里来更新每一个节点的fa[]值。
fa[p]=pre_loction;
pre_loction=p;
}
cnt[p]++;
return p;
}
//用递归来判断节点p1是否p2的祖先(父亲)节点
bool find(int p1,int p2,int* fa){
if(p2==-1) return false;
if(p2==p1) return true;
else return find(p1,fa[p2],fa);
};
}Trie;
int main() {
int n;
cin>>n;
char s[1000000];
Trie mood1;
Trie mood2;
mood1.init();
mood2.init();
for(int i=0;i<n;++i){
scanf("%s",s);
if(isupper(s[0])){
for(int i=0;s[i];i++){
s[i]=s[i]-'A'+'a';
}
}
loc_p1[i]=mood1.inser(s,-1,fa1);
//将字符串s反转
strrev(s);
loc_p2[i]=mood2.inser(s,-1,fa2);
}
int max=0;
for(int i=0;i<n-1;i++){
if(max>=n-i)
break;
for(int j=i+1;j<n;++j){
if(j-i+1>max && j-i+1!=n ){
if(mood1.find(loc_p1[i],loc_p1[j],fa1) && mood2.find(loc_p2[i],loc_p2[j],fa2)){
max=j-i+1;
}
}
}
}
cout<<max<<endl;
return 0;
}
最后分析一下时间复杂度,反转字符串O(kN),判断任意两个字符串是否满足前后缀匹配O(k).所以整体的时间复杂度大概是O(N^2)的。空间复杂度O(tot),tot表示Trie树里的总节点数。