8.14 模拟赛记录
复盘
考字符串,基本上最有把握的就是字典树,KMP比较容易写错,哈希有很多时候看不出来怎么用,所以一上来就先看哪些题可以字典树解。
T1一开始考虑KMP,但是求的是多组的前后缀,用不了KMP,字典树确实可以,正着反着各踹 trie一遍,然后通过判断一个点是否有多个子节点来判断哪一段存在同样的前缀后缀对。说的容易,但是我的解法比较复杂,调了相当久,大约到十点才把这题做出来(中途因为闹肚子上了相当长时间的厕所)。T2应该是KMP,但是拿不准应该怎么做,所以先过掉。T3首先能看出来用dfs可以暴力,然后试了一下能否用trie树,经过很长时间发现不太好办(考试的时候我并不会trie树合并),所以还是写暴力。经过漫长的调样例,发现自己低估了问题的难度,我只能判断子节点重复,但是接下来的一段都可能是重复的,这时候距考试结束只有20分钟了,经过一顿挣扎,最终为了体现一下对自己的尊重还是交上去了。
期望得分:100+0+0+0.
复盘分析
得分:10+0+20+0
首先我没想到T3真的能硬骗20分,有dalao写map暴力存储(map可以去重)然后查map().size得了70分,只能说我对STL的应用了解的还不够透彻。
T1有一部分的分数是忘记了后缀仍然是顺序的,变成了完全意义上的从后往前取,改一改虽然仍然不完全对但也能改到80分。T2其实是对KMPnxt数组的一个活用,明白做法不难,但是很考验技巧,不是很好写。T3T4涉及到字符串哈希等等一系列操作,不太好做,还得琢磨琢磨。
总之还是尽快回去复习一下字符串,顺带着复习一下STL,毕竟字符串学完没有多久刚刚能用,基础太弱了,集训结束之后的工作又多了一项2333,希望搞完这一次模拟赛对字符串问题的理解能更好一些,毕竟NOIP阶段考察的范围不大,搞起来或许不会像别的那样费劲。
题解
T1其实思路上是对的,正反各trie一遍,前缀没什么可讲的,只要1~26遍历就行了。但是后缀就麻烦一点,假设还是从1 ~26遍历,那么此时就必须要把长度的问题考虑进来。具体的来说,如果当前的长度比已存的答案长,直接更新;如果一样长,比较两者的最高位,如果后来的最高位小则后来的字典序小,否则一定大(因为存的答案是先遍历到的,一定保证判断这一位之前它的字典序更小)。余下的就是普通的trie树了。
贴一下这段的代码:
void dfs2(int p,int dep){
if(!sum2[p]) return;
int i;
if(ans2 <= dep){
if(ans2 < dep){
ans2 = dep;
pos2 = p;
minn = cha2[p];
}
else{
if(cha2[p] < cha2[pos2]){
pos2 = p;
minn = cha2[pos2];
}
}
}
for(i = 1;i <= 26;i++){
if(tr2[p][i]) dfs2(tr2[p][i],dep + 1);
}
}
T2是一道考察KMP活用的问题。
这题要找最长的前后缀,那么显然是用KMP处理的nxt数组。问题来了,如何在中间找一个非前后缀?
首先,根据我和一众大佬讨论的结果,KMP主要是用来匹配一个完整的串,用来匹配前后缀只能用nxt数组。事实上,匹配一个非前后缀,实际上就是找一下nxt[2]~ nxt[n-1]哪一个可以与前后缀匹配。我们首先确定一下nxt[2]~nxt[n-1]最长的一个,这个就是可以匹配的最长前缀和最长非前后缀,然后在nxt[n]当中寻找与之长度相等的部分,这可以用跳nxt实现,也是一个常见的操作。总之这题代码量很小,但是我觉得很考验对nxt数组的理解,算是比较有意思的一道题。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
char a[1000010];
int nxt[1000010],n,maxx = 0;
void Getnxt(){//求nxt数组
int i,j;
nxt[1] = 0;
for(i = 2,j = 0;i <= n;i++){
while(j && a[i] != a[j + 1])j = nxt[j];
if(a[i] == a[j + 1]) j++;
nxt[i] = j;
if(i != n)maxx = max(nxt[i],maxx);//找nxt[2]~nxt[n-1]最大值
}
}
int main(){
int i,j,x;
scanf("%s",a + 1);
n = strlen(a + 1);
Getnxt();
x = nxt[n];
if(!x) printf("Just a legend\n");
else{
while(x > maxx) x = nxt[x];
if(!x){//不能匹配上
printf("Just a legend\n");
return 0;
}
for(i = 2;i < n;i++){
if(x == nxt[i]){
for(j = i - nxt[i] + 1;j <= i;j++){
printf("%c",a[j]);
}
printf("\n");
return 0;
}
}
}
return 0;
}
T3这题有一个性质:同一点引出的子串加上他们的前缀部分相对大小不变,所以可以dfs的过程中求出这点从根节点到现在生成的串的Hash,统计的就是对应子树区间有多少个不同的值。
这题如果上线段树维护这个信息,我第一时间想到的是KIN。显然这样是可做的,只是稍微麻烦了一点,这里用我刚学到的另一个做法启发式合并来做。
首先特别感谢学哥忍受了我爆降的智商花了一小时给我反复解释什么是启发式合并以及纠正我的错误概念,没有他的帮助,这篇博客根本无从更新。
所谓启发式合并,指的就是在更新父节点的时候,把轻儿子的信息更新到重儿子上,从而节省更新的时间的一种合并方式。重儿子指的是子树大小最大的儿子,在不同的题目当中也可指代为包括答案最大的儿子之类。
为了实现这个功能,我们需要记一个集合,表示一个点存储的信息的集合的编号(就像一个仓库那样),我们更新父节点,实际就是更新这个集合,为此,我们直接把编号变成重儿子的编号,这也就拿到了重儿子的信息,又是一步时间的节约。
此题判重可以利用map的自动去重功能,map的第一维下标就是集合编号。
此题在hash上非常离谱的地方在于,我mod取1e9+7甚至998244353都能卡掉4个点,用ull却过了…
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
const int mod = 99824353;
const int P = 13331;
const int N = 3e5 + 1;
char s[N];
struct yjx{
int nxt,to;
}e[N << 1];
int ecnt = -1,head[N],hson[N],id[N];
unsigned long long Hash[N];
long long res[N];
void save(int x,int y){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
head[x] = ecnt;
}
map<int,map<unsigned long long,bool> > mp;
map<unsigned long long,bool>::iterator it;
void dfs(int now,int fa){
int i,temp;
Hash[now] = Hash[fa] * P + (s[now] - 'a' + 1);
res[now] = 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
dfs(temp,now);
if(!hson[now] || res[hson[now]] < res[temp]) hson[now] = temp;//找重儿子
}
if(!hson[now]) id[now] = now;
else id[now] = id[hson[now]];//修改集合编号获得信息
mp[id[now]][Hash[now]] = 1;//把当前的字符串压进map
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == hson[now] || temp == fa) continue;
for(it = mp[id[temp]].begin();it != mp[id[temp]].end();it++) mp[id[now]][(*it).first] = 1;//遍历轻儿子集合的所有字符串,放入自己的集合
}
res[now] = mp[id[now]].size();//返回不同的字符串个数
}
int main(){
int i,n,p,x,y;
memset(head,-1,sizeof(head));
scanf("%d",&n);
scanf("%s",s + 1);
for(i = 1;i < n;i++){
scanf("%d %d",&x,&y);
save(x,y),save(y,x);
}
dfs(1,0);
for(i = 1;i <= n;i++) printf("%lld ",res[i]);
return 0;
}
/*
10
cacabbcddd
1 2
6 8
7 2
6 2
5 4
5 9
3 10
2 5
2 3
*/
T4这两个操作,首先明确,括号肯定要少加,能使左右括号数相等就行了。为了字典序小,左括号加左边,右括号加右边就行了,关键是现有序列怎么通过操作1搞一个字典序最小的。
操作1是一个环的操作,所以直接把序列加倍取n次,哈希求前缀和,用来O(1)求当前这段的哈希值,然后和存储的答案比较字典序。这个比较的过程可以二分可以匹配的长度,比较不能匹配的第一位就行了,如果原来存的答案是右括号就更新。维护这个答案正常应该用单调队列,当然multiset也完全可以。
用这种方法做需要卡一波常,最好的一个办法就是用ull的自然溢出代替取模。真是离谱…
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<string>
using namespace std;
const int N = 1e6 + 1;
const int P = 131;
int p = 1,a[N],sum[N];
unsigned long long mi[N],Hash[N];
char s[N];
multiset<int> Multi;
void pre(int m){
int i;
mi[0] = 1;
for(i = 1;i <= m;i++) mi[i] = mi[i - 1] * P;
for(i = 1;i <= m;i++) Hash[i] = Hash[i - 1] * P + (s[i] - '(' + 1);
}
unsigned long long Gethash(int y,int x){
return Hash[x] - Hash[y - 1] * mi[x - y + 1];
}
int main(){
int i,j,n,m,add = 0,ad,temp,l,r,mid;
scanf("%s",s + 1);
n = strlen(s + 1);
for(i = 1;i <= n;i++){
s[i + n] = s[i];
if(s[i] == '(') a[i] = a[i + n] = 1,--add;
else a[i] = a[i + n] = -1,++add;
}
for(i = 1;i <= (n << 1);i++) sum[i] = sum[i - 1] + a[i];
ad = max(add,0);
pre(n << 1);
for(i = 1;i <= n;i++){
Multi.insert(sum[i]);
}
for(i = 1;i <= n;i++){
if(i ^ 1){//i^1指的是i!=1,别写反了,反正我一开始写反了...
Multi.erase(Multi.find(sum[i - 1]));
Multi.insert(sum[i + n - 1]);//更新当前的区间
}
temp = *Multi.begin();
if(temp - sum[i - 1] + ad < 0) continue;
//注意这里要判断的是当前的这一段补上左括号是否合法,temp是一个前缀,所以必须要去掉前面的一部分
l = 1,r = n;
while(l < r){
mid = (l + r) >> 1;
if(Gethash(i,i + mid - 1) == Gethash(p,p + mid - 1)) l = mid + 1;
else r = mid;
}
if(a[i + l - 1] > a[p + l - 1]) p = i; //直接更新起点
}
if(add >= 0){
for(i = 1;i <= add;i++) putchar('(');//为了卡常真的是会的啥都用上了
for(i = p;i <= p + n - 1;i++) printf("%c",s[i]);
putchar('\n');
}
else{
for(i = p;i <= p + n - 1;i++) printf("%c",s[i]);
for(i = 1;i <= -add;i++) putchar(')');
putchar('\n');
}
return 0;
}
Thank you for reading!