作为杭电三队,我和大三学长陈钦况,还有大一便进过WF的周贤杰组队。踏上了前往沈阳的行程。本来以平时训练的成绩,我们的期望是争金,最后却只打了个铜回来。着实是很意外,很遗憾。然而却有很多值得总结的经验和许多要吸取的教训,为我今后的比赛做准备。
比赛重现:
第20分钟,队友AC掉D题。
D题——
【题意】
共有T([1,500])组数据。
每组数据有1~n([2,20000])共n个正整数。
一开始对于已有数字集合,除了a,b(1<=a,b<=n && a≠b),其他数字都是不存在的。
对于每次成功的操作,我们是从已有数字集合之中,任选两个不同数x,y,得到z=x-y或者z=x+y,如果z是在[1,n]范围,而且z这个数字当前不存在,那我们就可以得到这个新数z,并把其放入已有数字集合之中。
Yuwgna先手,Iaka后手,谁无法操作谁就输了。问你最后的winner是谁。
【类型】
签到 博弈 gcd
【分析】
首先应该想的问题是,数轴的范围是[1,n],但是我们能选的数究竟有什么呢?
样例中的{3 1 3}{8 6 8}这样组数据,前者可以取遍1,2,3,后者却只能取得2,4,6,8,让我们很快想到实际能选取的数字范围要由gcd判定。
我们先令g=gcd(x,y),那显然我们能取的数,必然是g的倍数。
于是求出,接下来能取的数的个数num=n/g-2
所以答案就是——
【时间复杂度&&优化】
O(Tlogn)
【trick&&吐槽】
这道题之所以给了这么多样例,就是为了让这题的结论更容易观察,让这题更算得上是签到题。
【数据】
input
16
2 1 2
3 1 3
67 1 2
100 1 2
8 6 8
9 6 8
10 6 8
11 6 8
12 6 8
13 6 8
14 6 8
15 6 8
16 6 8
1314 6 8
1994 1 13
1994 7 12
output略
【代码】
int gcd(int x,int y)
{
return y==0?x:gcd(y,x%y);
}
int main()
{
scanf("%d",&casenum);
for(casei=1;casei<=casenum;casei++)
{
int n,a,b;
scanf("%d%d%d",&n,&a,&b);
printf("Case #%d: %s\n",casei,(n/gcd(a,b))&1?"Yuwgna":"Iaka");
}
return 0;
}
===========================================================
第50分钟,我AC掉M题。
M题——
【题意】
两个人A,B想要见面。
有n([2,1e5])个点,A初始在1点,B初始在n点。
有m个集合关系,第i个集合有Si个点,这些点两两之间移动所花费的时间都为dis([1,1e9]),有∑Si<=1e6。
让你输出A和B在哪些点碰面,使得他们能在最早时间相遇。
输出这个最早相遇时间以及所有满足的点。
【类型】
最短路
【分析】
这题很显然是一个最短路模型。然而如果暴力建边,边数可达1e12条,爆炸。
问题是怎么处理集合关系。比较好想,我读完题的瞬间就想到了做法——拆点。
对每个集合构造两个点,入点和出点,之间连一条边权为dis的边,
把集合内的每个点向这个集合的入点连边,边权为0,
把集合的出点向这个集合内的每个点连边,边权为0。
这样就实现了,对于每个集合,利用2Si+1条边,改变其内任意两个点的边权都为dis。
然后分别以1和n为起点,跑最短路,然后扫描一下即可。
【时间复杂度&&优化】
O(mlogm)
【trick&&吐槽】
点数N=n+2*最大集合数=1e5+2e6
边数M=3*最大集合数=3e6
【数据】
input
2
5 4
1 3 1 2 3
2 2 3 4
10 2 1 5
3 3 3 4 5
3 1
1 2 1 2
output
Case #1: 3
3 4
Case #2: Evil John
【代码】
const int N=1e5+2e6+10,M=3e6+10,Z=1e9+7,ms63=1061109567;
int casenum,casei;
int id;
int first[N],w[M],c[M],nxt[M];
LL f[N],F[N];
bool e[N];
struct node
{
int x;LL v;
node(){}
node(int x_,LL v_){x=x_;v=v_;}
bool operator < (const node& b)const{return v>b.v;}
};
void ins(int x,int y,int z)
{
id++;
w[id]=y;
c[id]=z;
nxt[id]=first[x];
first[x]=id;
}
priority_queue<node>q;
void inq(int x,LL dis)
{
if(dis>=f[x])return;
f[x]=dis;
q.push(node(x,dis));
}
void dijkstra(int st)
{
MS(f,63);
MS(e,0);
inq(st,0);
q.push(node(st,0));
while(!q.empty())
{
int x=q.top().x;q.pop();
if(e[x])continue;e[x]=1;
for(int z=first[x];z;z=nxt[z])inq(w[z],f[x]+c[z]);
}
}
int main()
{
scanf("%d",&casenum);
for(casei=1;casei<=casenum;casei++)
{
MS(first,0);id=1;
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int dis,g,x;
scanf("%d%d",&dis,&g);
int in=n+i;
int out=n+m+i;
ins(in,out,dis);
while(g--)
{
scanf("%d",&x);
ins(x,in,0);
ins(out,x,0);
}
}
dijkstra(1);
MC(F,f);
dijkstra(n);
LL ans=1e18;
int ed;
for(int i=1;i<=n;i++)
{
gmax(F[i],f[i]);
if(F[i]<=ans)
{
ans=F[i];
ed=i;
}
}
if(ans==1e18)printf("Case #%d: Evil John\n",casei);
else
{
printf("Case #%d: %lld\n",casei,ans);
for(int i=1;i<ed;i++)if(F[i]==ans)printf("%d ",i);
printf("%d\n",ed);
}
}
return 0;
}
===========================================================
然后队友卡在B题半个小时,他们开始让我一起思考B题。
因为这场比赛总共的题数高达13个,高级数据结构和算法又是他们俩写,他们想了很久都没想出做法,而当时AC的人数又很多。于是我想也许采取一定方向的暴力做法就能AC,便开始敲
不过暴力做法有两种。
首先,最直接最暴力的做法是——
int bf()
{
for(int i=n;i>=1;i--)
{
for(int j=1;j<i;j++)
{
if(!strstr(s[j],s[i]))return i;
}
}
return -1;
}
这个时间复杂度是O(Tnnlen),可达50*500*500*2000=250e8,即250亿,爆炸。
但是转念一想,这种数据所对应的输出可达50*500*2000=5e7,即50MB,是不可能的,读入就爆炸了。于是,如果常数小,这样做是可以AC的。
而事实上,对于随机数据,这种暴力其实比下面的暴力,效率更高,更容易AC。
然后,另外一种暴力,是我想要针对特定构造数据(其实是针对了自己TwT)所写。
以每个串为子串筛后面的所有串:
如果它不是后串的子串,那么break掉。答案不会比当前后串的编号小。
如果它是后串的子串,那么我们可以把后串删掉。
(就是这里想错了。我们应当删掉的串不是后串,而是这个串——如果顺着这里想下去,改变for循环的顺序,for j=i+1 to n,也许就能很快做出来了。唉,还是自己思考的时间太少了,思维太不灵活了。)
正确的做法是什么呢?
参照下文代码,先升序枚举i,再升序枚举j。
如果j是i之后的第一个满足要求的串,使得s[i]是s[j]的子串,那么s[i]对于后串的意义s[j]都能起到。使s[i] break即可。
否则s[i]不是s[j]的子串,那么s[j]就是满足要求的串,以后就不用再比较”某个串是不是s[j]的子串”了。
这个时间复杂度是什么呢?
对于每次(s[i]是不是s[j])的子串:如果是,break掉,s[i]不再匹配;如果不是,s[j]被确定满足要求,s[j]不再匹配。所以每次比较都会有一个串失去后序匹配功能。
于是时间复杂度是O(T(n^2+nlen)),最大不过50*500*2000=5e7,就是一个完全可以AC的时间复杂度了。
【时间复杂度&&优化】
O(Tnnlen)->O(T(n^2+nlen))
【trick&&吐槽】
1,题目:做题要看题目名称暗示。B题题目bazinga是”逗你玩”的意思,然后我们真的被这题捉弄了。
2,读题:不要太依赖队友的读题,做一道题之前一定要自己读一遍,形成一个独立、系统的认知。很多时候,水题做不出来,都是队友开题,然后甩给我,我的思维附带了他们之前的错误思路,也就很难走出去。
3,策略:不要让队友卡题,尤其这种傻X题,不如让自己来卡。不要对自己生疏的算法有所恐惧,要挑起队伍的旗帜。
4,思维:思维要灵活。这题其实关键就是两个for循环的顺序,只要我都试着思考下,尝试下,很快就能做出来的。
5,我一开始的暴力做法,是想要剪枝的,但是在细节地方没有想清楚,可是如果抓住问题,思维严谨有序地想下去,也会很快出解。思考时间(而不是编码时间)应该是解决问题的大头,想清楚细节再做题是非常重要的。
6,strstr(母串,子串)返回的是NULL或者母串的匹配首位点指针。这个实际比KMP都要快。用这个的话这道题也不会卡住了。
总结——所以对于水题:
1,我来做
2,重新系统读遍题
3,灵活地做思维转化
【数据】
input
4
5
ab
abc
zabc
abcd
zabcd
4
you
lovinyou
aboutlovinyou
allaboutlovinyou
5
de
def
abcd
abcde
abcdef
3
a
ba
ccc
output
Case #1: 4
Case #2: -1
Case #3: 4
Case #4: 3
【代码】
char s[505][2020];
bool e[505];
int solve()
{
MS(e,1);int ans=-1;
for(int i=1;i<n;i++)//枚举子串
{
for(int j=i+1;j<=n;j++)if(e[j])//枚举还不一定是满足串的母串
{
//如果j是i之后的第一个满足要求的串,使得s[i]是s[j]的子串,那么s[i]对于后串的意义s[j]都能起到。使s[i] break即可。
if(strstr(s[j],s[i]))break;
//否则s[i]不是s[j]的子串,那么s[j]就是满足要求的串,以后就不用再比较了。
else {e[j]=0;gmax(ans,j);}
}
}
return ans;
}
int main()
{
scanf("%d",&casenum);
for(casei=1;casei<=casenum;casei++)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%s",s[i]);
printf("Case #%d: %d\n",casei,solve());
}
return 0;
}
队友卡了3个小时B题。他们尝试做F和I都失败了。(其实F很接近了>_<)。
这个时间我都是在做G题的。
虽然G题在封榜之前只有2个队AC,但是鉴于尝试的队伍有一些,而且这题题目很长,有模拟题的模样,于是我选择了它。
事实上,赛后,AHdoc也说这场比赛最水的4道题便是D,B,M,G,然而G实在是因为题意太坑=w=,最后我们还是以史上最差战绩3题收场。
===========================================================
G题——
【trick&&吐槽】
1,这题让我懂得了:对于很多坑B题,要一句句全部读懂才能做。
这些作者多多少少有些心理变态,重点积极分散,使得题目像是阅读理解一样来考你。
2,有的题目,对于重要信息,题目上下文会多次重复提及。
然而也有题目,上下文看似相同的两处信息,其实要有不一样的理解。
3,噢。在我整理这道题题解的时候,从出题人处得到了中文题。发现这题认真理解题意的话,就是个翻译错的题。
需要脑补才能AC,脑补才能AC,脑补才能AC……你要多读,多猜,多想,自行发现哪里可能有歧义。
翻译是把reach和touch搞错了。而且原文消除歧义的一大段文字,在翻译中一字未现。
所以才导致了各种无限wa的情况,甚至包括做CodeChef擅长阅读理解的朝鲜人12wa未AC。
中文原文:
英文翻译:
It is also assumed that if A and S touch the buoy in the same time,the point will be given to A and A could also fight with S at the buoy.
We assume that in such scenario,the dogfighting must happen after the buoy is touched by A or S.
【题意】
一个正方形球场。每个角都有一个浮标。按照顺时针方向,依次编号为#1,#2,#3,#4。
两个运动员开始都在1号球杆,他们要先后触碰2号,3号,4号,1号球杆,绕圈一周。
游戏规则:
1,对于任何一个浮标,如果你触碰的时间比对手早,那么你可以得到1分。但是触碰浮标的顺序必须严格是#2,#3,#4,#1。
2,如果你和对手同时到达了一个位置。你们可以打一场,赢的人得一分。但是为了保证比赛的平衡,2个运动员不允许在#2被触碰之前打架。
我们的速度是v1,对手的速度是v2,满足:0< v1<=v2<=2000。
对手会严格按照矩形的边,即按照1->2->3->4->1的顺序移动。
我们一定能打过对手,于是我们决定触发有且仅有一次战斗。这会使得我们多得到1分,并且使得对手立刻眩晕T秒。
如果2个人同时到触碰一个点,这个点的得分会给与我们,且我们还可以发动与对手的战斗。
如果2个人恰好在一个点相遇,如果对手碰过上一个浮标,那这个点的得分会给对手。
让你输出是否我们有可能得分比对手高。可能便输出Yes,不可能则输出No。
【类型】
阅读理解坑题TwT 解方程or二分
【分析】
这题的逻辑可以很清楚。
1,如果速度相同,那么我们必胜。
2,如果速度不同,我们有意义的阻截位置,要不是在2->3,要不是在3->4。
因为我们最终肯定要沿着#2->#3->#4->#1的顺序走,所以肯定满足——触碰位置越早越好。
所以存在三个判定点,
1,速度相同,我方必胜。
2,[#2 ~ #3)之间相遇,看我方能否先到达#4。
3,[#3 ~ #4)之间相遇,看我方能否先到达#1。
4,[#4 ~ #1)相遇,我方必败。
【时间复杂度&&优化】
O(T*二分)
【数据】
Input
2
1 10 13
100 10 13
Output
Case #1: No
Case #2: Yes
specail(恰好相遇于#4):
2000 100 300
我们和对方都同时在第3s到达,我方必败。
special(恰好相遇于#4前):
2000 100.01 300
眩晕时间很长,我方胜。
special[1+]
12.25 100.01 300
恰好胜
special[2+]
12.24 100.01 300
恰好败
【代码】
double T,v1,v2,T1,T2;
double K(double x){return x*x;}
void YES(){printf("Case #%d: Yes\n",casei);}
void NO(){printf("Case #%d: No\n",casei);}
void solve()
{
if(v1==v2)YES();
else if(v1*v1*2>v2*v2)//最早能在#3之前相遇,二分相遇点的横坐标
{
double l=0;
double r=300;
for(int tim=1;tim<=100;tim++)
{
double m=(l+r)/2;
double l1=sqrt(K(300)+K(m));
double t1=l1/v1;
double l2=300+m;
double t2=l2/v2;
t1>t2?l=m:r=m;//如果来不及,那么相遇点横坐标变大,否则横坐标变小。
}
//如果我们想要获胜,必须至少要触碰#3和#4。
double t1=sqrt(K(300)+K(l))/v1+l/v1+2*T1;//我们触碰#4的时间
double t2=3*T2+T;//对手触碰#4的时间
t1<=t2?YES():NO();
}
else if(v1*3>v2)//最早能在#4之前相遇,二分相遇点的纵坐标
{
double l=0;
double r=300;
for(int tim=1;tim<=100;tim++)
{
double m=(l+r)/2;
double l1=sqrt(K(300)+K(m));
double t1=l1/v1;
double l2=900-m;
double t2=l2/v2;
t1>t2?r=m:l=m;//如果来不及,那么相遇点纵坐标变小,否则纵坐标变大。
}
//如果我们想要获胜,必须至少要触碰#4和#1
double xx=sqrt(K(300)+K(l))/v1;
double yy=sqrt(K(300)+K(300-l))/v1;
double t1=sqrt(K(300)+K(l))/v1+sqrt(K(300)+K(300-l))/v1+3*T1;//我们触碰#1的时间
double t2=4*T2+T;//对手触碰#1的时间
t1<=t2?YES():NO();
}
else NO();
}
int main()
{
scanf("%d",&casenum);
for(casei=1;casei<=casenum;casei++)
{
scanf("%lf%lf%lf",&T,&v1,&v2);
T1=300/v1;
T2=300/v2;
solve();
}
return 0;
}
G题因为题意问题,加上我太蠢没有学会潜在坑点分析+脑补。
加上B题我没有担当起责任,没有解放队友,导致最终打了个铜。
主要原因还是自己太弱。
争取在上海的时候拿个金,至少正常水平发挥拿到ECfinal参赛权。
队友一个进过WF没了干劲,另外一个想要找工作,也没了热情。虽然他们水平很厉害,但希望在明年,能够不再和”马上打比赛的时候嘴里却喊着退役,什么结果都无所谓的人”组队,消极的队友,会带给你一种无力感,真的很累很累。其实还不如单挑,虽然名次会变低,但是至少充满斗志和热情。
加油!冲击明年的WF。一切都会变好!