记人生中第一场认真打的CF——CF1000(虽然是Virtual participation)

老师说下午要让我们(来自开明的新高一同学)感受一下CF,于是下午2:20我们就集中到了机房。老师教我们用Educational Codeforces Round 46 (Rated for Div. 2开了一场Virtual participation,然后就让我们自己打了。

拿到题目先看A题,大意就是让你通过修改最少的字符(不能删除或添加),将A组字符串修改为B组(不考虑顺序,保证有解)。拿到题目我就傻眼了,一般CF的div2的题目AB都是比较的简单的呀,而这一道题拿到手尽然毫无思路,以至于丧失了理智,一上来先把A和B全都按字典序排了一下序,然后对应比较,然后成功地Wrong answer on test 7……
然后我看到红色的WA,冷静过来,开始理智地分析。因为这些字符串全是T恤衫的编号,所以一共只能有一下几种字符串:

L
M
S
XL
XS
XXL
XXS
XXXL
XXXS

题目又保证有解,所以每种长度的字符串在两组中的数量是一样的。可以发现,把通长度的一个字符串替换为另一个,代价只需要1。因此,我们只需要统计一组中每种字符串的出现次数,然后在把另一组的每一个字符串在A中找有没有相同串,有就把该串的数量-1,否则ans++。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
#include<iostream>
#include<map>
using namespace std;

const int maxn=100+7;
int n,ans;string a[maxn],b[maxn];
map<string,int>m;

int main(){
	ios::sync_with_stdio(false);cin>>n;for(register int i=1;i<=n;++i)cin>>a[i],m[a[i]]++;
	for(register int i=1;i<=n;++i){cin>>b[i];if(m[b[i]])--m[b[i]];else ++ans;}
	cout<<ans<<endl;
	return 0;
}

然后又转过来看第二题,有一台灯,这个灯在时间为 0 时打开, m 时关闭,在 0 到 m 这段时间内有 n 个时间点灯的状态会改变(即开变关,关变开),现在可以在剩余的时间点选一个让灯的状态改变一次,求这个灯最大亮着的时间。一开始没注意到可以在剩余的时间点选一个改变灯的状态,所以按样例模拟,感觉有问题,回头看题目(orz我在也不看翻译了)才注意到。于是这道题就很简单啦。我们预处理一下s1[i]与s2[i],表示i次开关往前的奇数空隙的时间和和偶数空隙的时间和,然后对于每一次关灯,比较s1[i]+s2[n]-s2[i]-1的大小(在前面放和在后面放都是一样的),更新ans即可。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int maxn=1e5+7,maxm=1e9;
int n,m,a[maxn],ans,la,s1[maxn],s2[maxn];bool flag=1;

int main(){
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=n;++i)scanf("%d",&a[i]),ans+=i&1?a[i]-a[i-1]:0;if(!(n&1))ans+=m-a[n];a[++n]=m;
	for(register int i=1;i<=n;++i)s1[i]=s1[i-1]+(i&1?a[i]-a[i-1]:0),s2[i]=s2[i-1]+(i&1?0:a[i]-a[i-1]);
	for(register int i=1;i<=n;++i)if(i&1)ans=max(ans,s1[i]-1+s2[n]-s2[i]);printf("%d\n",ans);
}

然后开始看第三题。给你n个区间,求被这些区间覆盖层数为\(k(k<=n)\)的点的个数。一开始看到题目,一眼看出可以用差分做,对于一个区间[l,r],我们可以s[l]++,s[r]--,然后从前往后累加s,就可以表示出一个点的覆盖层数了。然后发现\((0<=l<=r<=10^{18})\),可能需要离散化一下。但我想起夏令营时的惨痛教训,对于这种可能需要统计有多少个点的题目,离散化时候需要当心。未来方便统计被离散化的点之间有多少空隙,我又把l+1和r+1也都加入了离散序列,然后统计答案时,对于一个在询问中出现的位置,直接ans[s[i]]++,否则ans[s[i]]+=s[i+1]-s[i-1]-1表示这边世界上是s[i+1]-s[i-1]个点。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;

const int maxn=2e5+7;
int n,tot,dis,p[maxn<<2];bool check[maxn<<2];ll l[maxn],r[maxn],a[maxn<<2],ans[maxn];

inline int Find(ll x){int l=1,r=dis;while(l<r){int m=(l+r)>>1;if(a[m]>=x)r=m;else l=m+1;}return l;}
inline void Discrete(){
	sort(a+1,a+tot+1);for(register int i=1;i<=tot;++i)if(i==1||a[i]!=a[i-1])a[++dis]=a[i];
	for(register int i=1;i<=n;++i)check[l[i]=Find(l[i])]=1,check[r[i]=Find(r[i])]=1;
}

int main(){
	scanf("%d",&n);
	for(register int i=1;i<=n;++i){scanf("%I64d%I64d",&l[i],&r[i]);a[++tot]=l[i],a[++tot]=r[i],a[++tot]=l[i]+1,a[++tot]=r[i]+1;}
	Discrete();for(register int i=1;i<=n;++i)p[l[i]]++,p[r[i]+1]--;
	for(register int i=1;i<=dis;++i){
		p[i]+=p[i-1];
		if(check[i])ans[p[i]]++;else ans[p[i]]+=a[i+1]-a[i-1]-1;
	}
	for(register int i=1;i<=n;++i)printf("%I64d%c",ans[i],i==n?'\n':' ');return 0;
}

然后看D题。这道题可有意思了。如果一个数组$ [a_1,a_2,a_3,...,a_n]a_1=n-1\(并且\) a1>0a1>0 $,这个数组就被叫为好数组,如果一个序列能正好分为多个好数组,ta就被叫为好序列,现在给定一个序列,求这个序列有多少好子序列,答案对 998244353998244353 取模。
一开始以为子序列必须是连续的,所以我就把它抽象成了一棵树,做树形dp(其实直接dp也可以)。然后发现样例二死活过不去,随重新读题,看到样例二的解释,才知道子序列可不连续。于是随手把树形dp改成了一个“图形dp”,然后感觉这样很没意思,反正是个DAG,还不如直接dp好呢。于是写了一个普通的dp。附dp方程。

\[dp[i]:\text{表示在i..n的区间内,以i为开头的好子序列的个数(i位置必须选)} \]

\[dp[i]=\sum \limits_{j=i+a[i]}^n C_{j-i-1}^{i-a[i]-1} \cdot (1+\sum \limits_{k=j+1}^n dp[k]) \]

解释一下:j枚举的是以i开头的好数组的尾坐标,由于i,j的位置已经固定,所以统计组合数的时候需要把\(i-a[i]+1\)以及\(j-i+1\)都减2。后面的\((1+\sum \limits_{k=j+1}^n dp[k])\),1表示如果该好数组后面没有继续接其他好数组,也就是只有一段的好子序列。\(\sum \limits_{k=j+1}^n dp[k]\)表示可以继续接在后面的好子序列。
然而我们发现这样做的时间复杂度为\(O(n^3)\),对于n<=1000的数据肯定会超时。于是,我们可以把dp[i..n]的和在计算dp值得时候先开一个sumv[i]数组处理一下。这样可以把时间降到二维的,1000就可以过了。最后我们的dp值还只是自己开头的好子序列的个数,我们需要把dp值求一下和。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int maxn=1e3+7,MOD=998244353;
int n,a[maxn],dp[maxn],ans,sumv[maxn],c[maxn][maxn];

int main(){
	scanf("%d",&n);
	for(register int i=0;i<=n;++i){
		c[i][0]=1;
		for(register int j=1;j<=i;++j)
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%MOD;
	}
	for(register int i=1;i<=n;++i)scanf("%d",&a[i]);
	for(register int i=n;i;--i){
		for(register int j=i+a[i];a[i]>0&&j<=n;++j)
			(dp[i]+=1ll*c[j-i-1][a[i]-1]*(1+sumv[j+1])%MOD)%=MOD;
		(sumv[i]=sumv[i+1]+dp[i])%=MOD;(ans+=dp[i])%=MOD;
	}
	printf("%d\n",ans);
}

然后就是E题,当时在比赛里没写完,后来又订正好了。给定一个 n 个点 m 条边的无向图,找到两个点 s,t,使得 s 到 t 必须经过的边最多(一条边无论走哪条路线都经过ta,这条边就是必须经过的边),\(2<=n<=3*10^5,1<=m<=3*10^5\)
拿样例1画个图:

图中红边就是从4到5必须经过的边。途中1,2,3构成了一个无向环(不知道这样叫恰不恰当,因为在无向图中其实只要有边就有环),可以发现,只要是环,环中的边就一定不是必须经过的。我们可以通过跑一遍Tarjan(因为这是无向图,如果直接跑Tarjan的话会发现整个图都是一个环,所以我们需要禁止从子节点跑到父节点(这不是一棵树,所以父节点指的是从哪个节点跑过来的)),找出所有的环,把它们都各自缩成一个点,然后这样的话,整个图就成了一棵树。那么问题就转化成了,在树中找两个点,使它们之间的路径经过的边最多。这不就是求树的直径吗?

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int maxn=3e5+7;
int n,m,x,y;

struct{int v,Next;}g[maxn<<1],p[maxn<<1];int head[maxn],head2[maxn],tot,tot2;
inline void addedge(int x,int y){g[++tot].v=y;g[tot].Next=head[x];head[x]=tot;}
inline void addedge2(int x,int y){p[++tot2].v=y;p[tot2].Next=head2[x];head2[x]=tot2;}//错误笔记:注意addedge2里面是tot2不是tot 

int scc[maxn],pre[maxn],low[maxn],dfn,stk[maxn],top,sccno;
inline void Tarjan_dfs(int x,int fa){
	pre[x]=low[x]=++dfn;stk[++top]=x;
	for(register int i=head[x];i;i=g[i].Next){
		int y=g[i].v;if(y==fa)continue;
		if(!pre[y]){Tarjan_dfs(y,x);if(low[y]<low[x])low[x]=low[y];}else if(!scc[y]&&pre[y]<low[x])low[x]=pre[y];
	}
	if(pre[x]==low[x]){++sccno;while(1){int y=stk[top--];scc[y]=sccno;if(x==y)break;}}
}
inline void Tarjan(){for(register int i=1;i<=n;++i)if(!pre[i])Tarjan_dfs(i,0);}
inline void Shrink(){for(register int i=1;i<=n;++i)for(register int j=head[i];j;j=g[j].Next)if(scc[i]!=scc[g[j].v])addedge2(scc[i],scc[g[j].v]);}

int dp[maxn],ans;bool visit[maxn];
inline void dfs(int x){
	visit[x]=1;
	for(register int i=head2[x];i;i=p[i].Next){
		int y=p[i].v;if(visit[y])continue;dfs(y);
		ans=max(ans,dp[x]+dp[y]+1);
		dp[x]=max(dp[x],dp[y]+1);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=m;++i)scanf("%d%d",&x,&y),addedge(x,y),addedge(y,x);
	Tarjan();Shrink();dfs(1);printf("%d\n",ans);
	for() 
}

F题的话,我一开始写了一个暴力,就是用莫队来维护,写一个treap来记录有哪些数。然后发现在第八个点就TLE了。
于是开始认真地想。总感觉这道题会跟[SDOI2009]HH的项链有点相似之处。于是我也像那道题一样,思考对于每一个点维护它的pre值,也就是这个点上的数上一次出现的位置。然后发现这样似乎不大好求出哪个数只出现一次?于是我用那道题的另一种做法来思考,先把询问按r来分开存储一下,然后把数组里面的每一个数一次加入到线段树里面去。那道题是维护的是“该位置是否是当前最后一次出现这个数”,是就是1,否则就是0,但是这样做似乎也不大行得通。想要判断一个数是否出现一次,应该还是跟pre值有点关系的。于是我试着去把两种做法结合起来:线段树维护到目前为止,1..i位置的pre值,然后统计最小的pre值是否小于l。然后为了防止前面重复的一个数的pre搞鬼,所以我们每加入一个数a[i],都要把pre[i]的位置在线段树里面清为INF,以防这个数<l,但是它并不是只出现1次的。这样的话,总结一下我们的做法:将所有询问按r坐标分类存储起来,然后从1到n,将每一个数的pre值放入线段树,同时将pre[pre[i]]清为INF,同时对于以i为r坐标的询问,我们统计l..r区间内最小的pre是否小于l,是我们就输出那个最小的pre的位置上的数值,否则输出0。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define lc o<<1
#define rc o<<1|1
using namespace std;

const int maxn=5e5+7,INF=5e5+1;
int n,m,x,y,a[maxn],pre[maxn],lst[maxn];

struct{int v,Next,id,ans;}l[maxn];int head[maxn],tot;
inline void Add(int x,int y,int id){l[++tot].v=y;l[tot].id=id;l[tot].Next=head[x];head[x]=tot;}

struct Node{int minv=INF,mink=INF;}t[maxn<<2];struct Pair{int a,b;inline bool operator<(const Pair&B)const{return a<B.a;}};
inline void Set(int o,int L,int R,int x,int k){
	if(L==R)t[o].minv=k,t[o].mink=x;
	else{
		int M=(L+R)>>1;
		if(x<=M)Set(lc,L,M,x,k);else Set(rc,M+1,R,x,k);
		if(t[lc].minv<t[rc].minv)t[o].minv=t[lc].minv,t[o].mink=t[lc].mink;else t[o].minv=t[rc].minv,t[o].mink=t[rc].mink;
	}
}
inline Pair Min(int o,int L,int R,int l,int r){
	if(l<=L&&R<=r)return Pair{t[o].minv,t[o].mink};
	else{
		int M=(L+R)>>1;Pair ans={INF,INF};
		if(l<=M)ans=min(ans,Min(lc,L,M,l,r));if(r>M)ans=min(ans,Min(rc,M+1,R,l,r));//错误笔记:把l<=M打成了x<=M
		return ans;
	}
}

int main(){
	scanf("%d",&n);for(register int i=1;i<=n;++i)scanf("%d",&a[i]),pre[i]=lst[a[i]],lst[a[i]]=i;
	scanf("%d",&m);for(register int i=1;i<=m;++i)scanf("%d%d",&x,&y),Add(y,x,i);
	for(register int i=1;i<=n;++i){
		if(pre[i])Set(1,1,n,pre[i],n+1);Set(1,1,n,i,pre[i]);
		for(register int j=head[i];j;j=l[j].Next){
			int ql=l[j].v,qr=i;Pair k=Min(1,1,n,ql,qr);
			if(k.a<ql)l[l[j].id].ans=a[k.b];else l[l[j].id].ans=0;
		}
	}
	for(register int i=1;i<=m;++i)printf("%d\n",l[i].ans);
}

至于G题嘛,由于我比较傻,英语也不好,到现在连题目都没看懂,所以我就暂时留坑待补了,以后A掉了就来填坑。

洛谷的难度评级有毒啊,这次比赛哪有那么难,A能是普及/提高-?BC竟然是提高+/省选-?DE是省选/NOI-?F竟然能评到NOI难度?有毒吧!

这次CF给我的教训就是:一定要看清题目再思考!遇到不好做的题目时不能慌!冷静思考!学会用之前写过的类似题目的方法来辅助思考,尝试把多个题目的过个方法结合起来!嗯,还有,要努力提升英语水平!

posted @ 2018-06-29 22:05  hankeke303  阅读(790)  评论(1编辑  收藏  举报