洛谷 P5640 【CSGRound2】逐梦者的初心

洛谷 P5640 【CSGRound2】逐梦者的初心

洛谷传送门

题目背景

注意:本题时限修改至250ms,并且数据进行大幅度加强。本题强制开启O2优化,并且不再重测,请大家自己重新提交。

由于Y校的老师非常毒瘤,要求zhouwc在csp考前最后3天参加期中考,zhouwc非常生气,决定消极考试,以涂完卡但全错为目标。现在retcarizy看zhouwc太可怜了,想要帮zhouwc解决一个问题,但他自己又太忙了,咕咕咕,于是就把问题甩给了你。

题目描述

给你一个长度为n的字符串S。

有m个操作,保证m\le nmn

你还有一个字符串T,刚开始为空。

共有两种操作。

第一种操作:

在字符串T的末尾加上一个字符。

第二种操作:

在字符串T的开头加上一个字符。

每次操作完成后要求输出有几个l \in [1,T.size]l∈[1,T.siz**e]满足以下条件:

对于\forall i \in [1,l]∀i∈[1,l]有T_{T.size-l+i} \ne S_{i}T**T.siz**el+i=S**i

Tip:Tip:字符串下标从1开始。T.sizeT.siz**e表示T的长度。

输入格式

第一行两个正整数n,mn,m

第二行n个正整数,用空格隔开,第ii个整数表示S_iS**i

接下来mm行,每行两个数字opt,chopt,c**h,opt=0opt=0表示在T的末尾加一个字符chc**h,opt=1opt=1表示在T的开头加一个字符chc**h

输出格式

共mm行,每行一个非负整数表示第m操作后的输出。

输入输出样例

输入 #1复制

输出 #1复制

说明/提示

注意:本题采用捆绑测试,只有当你通过一个subtask的所有点后,你才能拿到这个subtask的分数

对于所有的数据 n \leq 10^6,m \leq 3.3333 \times 104,|\sum|\leq103,S_i \in [1,|\sum|]n≤106,m≤3.3333×104,∣∑∣≤103,S**i∈[1,∣∑∣]。(\sum∑表示字符集)

subtask1(17%)(17%):m \leq 333m≤333

subtask2(33%)(33%):m \leq 3333m≤3333

subtask3(20%)(20%)😐\sum|\leq2∣∑∣≤2

subtask4(30%)(30%):无特殊条件

样例解释:

第一次操作后,T="1"T="1",

l=1l=1时T[1]=S[1]T[1]=S[1],所以答案为0

第二次操作后,T="21"T="21",

l=1l=1时,T[2]=S[1]T[2]=S[1]

l=2l=2时,T[1]!=S[1]T[1]!=S[1],T[2]!=S[2]T[2]!=S[2]所以答案为1

第三次操作后,T="213"T="213",

l=1l=1时,T[3]!=S[1]T[3]!=S[1];

l=2l=2时,T[2]=S[1]T[2]=S[1];

l=3l=3时,T[3]=S[3]T[3]=S[3]所以答案为1

题解:

在考场上一开始发现这道题是道绿题,但是自己却没什么成形的思路,顿觉一身冷汗...不说匹配,就前面的那个在头尾加入字符串我就有些束手无策的感觉。果然还是蒟蒻太菜了。

越到这种时候越要冷静。(哈哈哈)在我冷静之后一段时间内,我灵光乍现(前一段时间刚总结完STL,详情请见下面的博客:)

C++STL容器全解

发现在头尾加元素可以用deque这种容器来实现。(哇哈哈哈哈哈)而且deque还是STL中为数不多支持随机访问的容器,简直不要太好用!

考场思路一开始的思路是暴力枚举匹配。

期望50分,但只能拿17pts.

代码:

#include<cstdio>
#include<deque>
using namespace std;
const int maxn=1e6+10;
int n,m,tot,flag;
char *p1,*p2,buf[100000];
#define nc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++)
int read()
{
	int x=0,f=1;
	char ch=nc();
	while(ch<48){if(ch=='-')f=-1;ch=nc();}
	while(ch>47)	x=(((x<<2)+x)<<1)+ch-48,ch=nc();
	return x*f;
}
int s[maxn];
deque<int> q;
int main()
{
	n=read(),m=read();
	for(int i=0;i<n;i++)
		s[i]=read();
	while(m--)
	{
		tot=0;
		int opt=read();
		int ch=read();
		if(!opt)
			q.push_back(ch);
		else
			q.push_front(ch);
		for(int i=0;i<q.size();i++)
		{
			flag=0;
			for(int j=0;j<=i;j++)
				if(q[q.size()-i+j-1]==s[j])
				{
					flag=1;
					break;
				}
			if(!flag)
				tot++;
		}
		printf("%d\n",tot);
	}
	return 0;
}

我的脚步就到此为止了么?不可能!然后我就深入地想了一下,并且在原暴力的基础上进行了一些优化。但是暴力优化并没有起到太大的效果。那么就一定是思路的问题。于是我又重新思考了一下。我的思路是这样的:

既然时间不允许每次都暴力匹配,而匹配过程还不能进行优化,那么最终的正解不可能是每次都匹配。一定有什么玄妙的规律。

于是我在草纸上随便画了画...

(这里插一嘴,我在写题解的时候注重对思路历程的剖析。也就是不止讲怎么做是对的,还要讲如果是一道新题,怎么想才能想到正解。但是有些细节真的很难口述明白,比如草纸画图这种东西,恳请读者见谅并自己老老实实拿张草纸边看题解边模拟。这样事半功倍。)

发现,每一道题答案是否合法是可以通过之前的答案转移过来的。

具体一点就是,当元素往前面插,那么插入之前的老数列的答案是不会被影响的。只需要扫描一遍这个新数列和给定的数列\(S\)是否是全部不等的,如果是的话就说明有了一个合法答案。

当元素往后面插的时候,就会影响之前的老数列的答案。也就是说,需要一一匹配前面合法的答案是否还能保持合法性

思路是对的,但是代码不会写了,考试剩下的两个小时全都在尝试着实现这个思路,但是失败了。

于是最终还是17分。

看了和我思路相似的题解,我才恍然大悟。因为向后插入的时候需要匹配前面所有的合法答案。那么怎么知道前面都有哪些答案合法呢?很简单,再开一个\(vector\)存一下就可以了。

大约是这样:

#include<cstdio>
#include<deque>
#include<vector>
#pragma GCC optimize(2)
using namespace std;
const int maxn=1e6+10;
int n,m;
int s[maxn];
bool flag;
char *p1,*p2,buf[100000];
#define nc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++)
int read()
{
    int x=0,f=1;
    char ch=nc();
    while(ch<48){if(ch=='-')f=-1;ch=nc();}
    while(ch>47)    x=(((x<<2)+x)<<1)+ch-48,ch=nc();
    return x*f;
}
deque<int> q;
vector<int> vec;
vector<int>::iterator it;
int main()
{
    n=read();m=read();
    for(int i=0;i<n;i++)
        s[i]=read();
    while(m--)
    {
        int opt=read();
        int x=read();
        if(opt)
        {
            q.push_front(x);
            flag=0;
            for(int i=0;i<q.size();i++)
                if(q[i]==s[i])
                {
                    flag=1;
                    break;
                }
            if(!flag)
                vec.push_back(q.size());
        }
        else
        {
            q.push_back(x);
            it=vec.begin();
            while(it!=vec.end())
            {
                (*it)++;
                if(s[*it-1]==x)
                    vec.erase(it);
                else
                    ++it;
            }
            if(s[0]!=x)
                vec.push_back(1);
        }
        printf("%d\n",vec.size());
    }
    return 0;
}

本来这样的话开一开\(O2\)就过了。但是出题人又改了数据、压了时限,于是这个思路变成了\(70\)分代码。

那么我们看一下正解:

每个位子和每种字符都是独立的,对每种字符都记录一下位子。

\(f[i]=0 \,\,\,or\,\,\,1\)表示长度为\(i\)的后缀可不可以,0表示可以,1表示不行。

考虑\(f\)只有\(0\)\(1\),可以用\(bitset\)优化,对每种字符都开一个\(bitset\)记录是不是该字符。

在末尾加一个字符时,左移后做\(or\)运算。

在开头加一个字符时,直接\(or\)上该字符出现的状态左移长度减一位。

答案就是范围内\(0\)的个数。

于是传说中的双端队列变成了bitset......

posted @ 2019-11-11 18:57  Seaway-Fu  阅读(402)  评论(0)    收藏  举报