假期来了,天天“开心赛”的日子也来了。
复盘
到机房的时候是8:30,3个半小时,看了眼大标题,是大考的模拟,而且还是专题练习,觉得时间上应该是很标准的(直到考完我才知道默认的考试时间是4个小时,而什么初赛复赛考试时长好像都是4个半小时……)。
老套路,直接把四道题遍历一遍。
这模拟是“搜索和枚举”的专题,肯定4道题都会考察到这个知识点,然而看到T1的时候满脑子想不到搜索和枚举,而只能想到打表二字。我认为这题打表确实能打到50分,但是分类讨论很多,很耗精力,不太好写。联想到去年初赛某大模拟,于是看T2.
T2看完题干就放弃了,根本没个做。
T3看了一下,乍一看以为是最小生成树(这时候大概由于前两题整的我一头雾水,已经忘记了这是一次专题模拟),但是数据解释不通,过了大约5分钟我才终于明白这题跟图论半毛钱关系都没。想到了BFS,但是BFS一是n<=3的时候并不保证横纵坐标的范围,很容易被卡,二是不好处理答案。想了一段时间也没想到什么规律,于是看T4.
T4第一反应是并查集经典团伙问题,环状也能处理。然而这题要统计个数等等一堆东西,我顺着并查集基础操作想是越想越乱,不知道怎么做。于是最终还是回头打T1,顺带思考一下T4怎么写。这个时候大约是9:10。
T1打到n<=11的时候突然有了点灵感。鉴于n=11的时候表已经打起来非常费劲,我估计的没错,我的精力基本快空了,所以决定试着做一做T4.思路就是完全放弃并查集,用操作一把一个环分成若干个块。很明显,所有的操作一最多只能有一个为真,枚举每一个为真的情况加上全为假的情况就可以n2的完成,如果进行一轮去重还可以更快。虽然有点麻烦,过不去1e6,但起码能写,所以转战T4(大伏笔)。
大约花了2个多小时在写T4,写了足有120行,但是一次过编了(我的准确度只有在这种时候才能体现出来吗),改了五分钟就成功过了样例。此时大约是11:30,上卫生间的时候想到了一种解决T3的办法,不算很复杂,可以一写,于是回去就开始写。但是大约还有10分钟的时候觉得可能写不完,于是先给T2写了个-1固输(大概是太久不打比赛,开始遍历的时候完全没想到去固输),然后继续写T3。很遗憾,到最后一刻也还没来得及把分类讨论都打完。
预期:30+10+10+50.
复盘分析
结果:20+10+0+0.
首先T1 T3表打一半(尤其还是打一大半)绝对是不明智的,因为这四道题全是多测,但凡少打一个很大可能这一段数据是过不去了,白白浪费时间,这在T1上体现的尤其明显。这充分说明,多测一定要把数据范围打满。
其次就是T2,T2其实不会做应该上来直接特判-1.理论上时间足够其实可以把0和1的情况特判出来,但考虑到这情况实在太少加之这题又是多测,其实这个时间花不花无所谓。
另外就是答题的时候遍历完四道题还是应该先明确一下到底先干什么,而不是遍历的时候就琢磨搞个n2之类的50+做法出来。一方面可能费很大劲也没啥思路(毕竟一口气想四道题确实有点费劲),T3我一开考就想了大约20分钟,但是啥也没想出来,最终还是就扔在那儿了。而且四道题一点头绪都没有潜意识里面多多少少还是有一点压力,这样很容易就忘了自己该干啥。所以上来除了判断一下大致的难度,应该先明确一下干点什么,由易到难比较好。同理一道题做一半想到另一个的做法,觉得可能做不完就扔一边也不明智,做一半的题很难捡起来(尤其T1还是打表,说实话我考试后打n=12的时候就非常非常慢,甚至比后面13 14更复杂的还慢,错误率还高,这说明答这题的状态都没了),而且扔着一个没做完的题又是增加心理压力,这样做题越做越着急。结果T1白给30分,T4一个最简单的情况没想清楚,白给50+分,这不能不说是翻车级别的失误。以及T4我已经是整个机房最接近正解的了(毕竟我看了大标题没在并查集上一直纠结),其实就差一点点优化就可以做出理论上能AC的方法,大概也是太急了,没想到,潜在的又亏一堆分。
总之这次考试从遍历之后摸到键盘的那一刻就已经爆亏,做T1想T4,做T4想T1,T2 T3暴力一个差点打不完一个差不点打完,假设真的冷静的把T1 T2的表打完(T3的暴力我开工的时候没想明白怎么做还),做个T4,有剩的时间做T3或者时间充分莽一波T1,这样得到的分数绝对远高于30.(T1要想打表AC确实要花特别多时间,但如果做完了T4,回来打T1打不完这时间其实也亏不到哪儿去)
另外,即使着急如此,我还是不得不佩服自己的调节能力,能够非常清晰的想出T4最复杂的主线做法,而且一次过编译,两次过样例 ,不禁让我想起无编译器一次过编译矩阵游戏的往事 。如果不是多测,大概真的可以得一点分吧。
考场解题思路总结
对于调题,我打算分两步做,第一天把自己期望的分数改出来,第二天尽可能改正解。改出自己期望的分数不是很难的事情,所以估计应付得来。课后题只能随缘了,考试优先,这肯定的。
今天一是整一下T1的打表AC,二是整一下T3的25分,三是整一下T4正解。
这三道题我写出来的解法没有半点搜索,估计无搜索也就能写到235分了(捂脸)
T2思路已经说过了,固输。
T3对于n<=3有一个显然的做法:取两个点(x1,y1)(x2,y2),显然在以这两点为两对角的矩形中所有的路线都是两点间最短路径(应该叫最短曼哈顿距离),然后计算一下另一个点到达这个矩形外侧需要几步就可以(如果在矩形内,显然就不用走了)。这个计算无非分两类:走到横边上,走到纵边上(走到角上本质上还是走到边上)。如果可以走直线到边上就走直线,不然就走到最近的角边上。考场上没想清楚到底三对的答案是不是一样 ,毕竟在厕所想的,没法集中注意力想一个比较大的数据 ,但是可以三对都来一遍然后取个最小值,总有一个是正确答案。
我认为这个正确性显然:假设第三点在矩形外更优,那么另两个点走到第三个点旁,一是一个点走比两个点走相同的路肯定走的少,二是即使走过去,也总有两个点走的是最短的一条路径,不可能全部绕到外面,这样绝对是劣的 (这正确性在厕所里都能验出来) 。
事实上,上述过程还有一个问题:如果一个点走到另一个矩形的角边上,转换参考系,那么还是可以相当于另一个点走到了另一个矩形边上,走到角的情况可以忽略了。
所以其实我在考试的时候写出来的解并不缺少讨论,但是我犯了个非常愚蠢的错误,那就是在绝对值里面加减常数了。这显然是不对的。
附上一段分类讨论:(另外两种同理)
maxx = max(x[1],x[2]),minx = min(x[1],x[2]);
maxy = max(y[1],y[2]),miny = min(y[1],y[2]);
if(x[3] <= maxx && x[3] >= minx && y[3] <= maxy && y[3] >= miny){
res = min(res,abs(x[1] - x[2]) + abs(y[1] - y[2]) - 2);
}//在矩形内
else{
if(x[3] <= maxx && x[3] >= minx){
res = min(res,abs(x[1] - x[2]) + abs(y[1] - y[2]) - 1 + min(abs(y[3] - y[2]),abs(y[3] - y[1])) - 1);
}//能竖直走到矩形
else if(y[3] <= maxy && y[3] >= miny){
res = min(res,abs(x[1] - x[2]) + abs(y[1] - y[2]) - 1 + min(abs(x[3] - x[2]),abs(x[3] - x[1])) - 1);
}//能水平走到矩形
}
T1的一个思路其实还真就是打表。
这题数据保证了k<=1e9,也就是说如果位数足够多,只可能出现hzwer1+若干个0+(m-1)。在打表的过程中我们很快发现,这个位数到15就可以了。而n<=5的情况完全没意义,全是-1,所以我们真正要处理的部分只有[6,14],事实上n<=12的部分其实都不算难打,状态在线的话我估计40分钟足矣。13 14的时候分类讨论会剧增(尤其是13->14),这部分表很容易打错,不过积累了前面的经验,没出什么大错。n>=15的话最好是自行出一个样例,不然0的个数确实容易加错。
T1我在考场上打完了n=11的表,最后又排出了n=12的情况,觉得不应该浪费这个机会,所以就打算试着打一下表。事实证明我naive了,n=11以前的部分只占了整个代码的40%,打完剩下的部分净用时大约70分钟,这一道题大约花了2h。u1s1,这对于一道正解算是可以接受了(看看我T4做了多久,不是因为T4的话,这篇总结本应该昨天晚上就发出去的),所以对于这题,也不失为一种可取的方案。我也姑且把这题当做是练我的打表精确度了。
虽然估计不会有第二个人像我这样写,但还是分享一点经验:这题数字合法范围就是1000…0~9999…9,所以可取的情况其实就是1,9,90,900,…。最容易错的是两个地方:一个是位数的问题,反正我经常把四位数当三位数就用了;二是后面插入一位数的时候,这时候取值范围应该是[l,l+8],非常容易顺手写成+9.当然这题有补救方案,因为我们很容易算出来总共合法的取法有多少种,可以拿这个验证,看最后求得的最大值是否与之相同。如果讨论太复杂了,最好是写在纸上,不然确实容易串或者丢掉一些情况。另外,不要忘记加\n.
代码我不贴了,估计片段都没人愿意看(一共236行10000多个字符,n=14自己占将近80行)。姑且分享一个n=14的分类讨论:
T4是我认为考场上写的最接近于正解的一道题,事实证明,也只有思路一致而已。
直接说结果吧:根据我和一晚上就AK了这场考试的fxj巨神(惭愧,我那时候在打表)的聊天记录,我从11:30开始给T4debug,仅仅debug了三个点(剩下的没有spj),以及解决了TLE的问题,就一直干到2:00(T1那段开始才是我debug完T4以后写的)。乍一看时间并不长,但是要考虑到这可是在已经有了完整架构的代码的基础上改的……说明此题或许真的上搜索或者也许能用的并查集会好一些。
以下只是提供一种个人解法,过程极其繁琐,不见得就是最优解,但是确实是我目前能理解的唯一解法了。注意:做法和代码是否完全正确有待验证。
对于此题的基本思路复盘的时候已经有所提及了,但是时间不够优秀。为了使时间更优秀一些,我们先假设所有的操作一全部为假话,记录一下此时为真话的数。然后遍历所有的块(全部为假建议特判),每次把这个块由假话变成真话,看一下变化后真话数是否等于这个块中操作一要求的数目,如果等于,直接输出结果;如果到最后都没有合法的,输出无解。如果根本没有操作一,只要看一下最后的下一句话是否与第一句相符就可以了(这句话的真假是由上一句推出来的)。
思考一下怎么实现。
设一个结构体a维护块内的信息,res表示块内操作一要求的真话数,num表示块内说话总数,right表示块中操作一为真的时候的真话数目(以下简称伴随真话数),same表示块内操作一和第一句话是否同真或同假。用另一个数组judge维护每句话的真假。考虑到最终我们还得从块具体到每一个点,所以用一个belong数组维护每个点属于哪个块,很像分块的操作。
现在出现一个难点:如果最后一个不是操作一,怎么把首尾相接?
对于一个环,我们可以从任意位置开始,任意位置结束。所以可以把最后这一段接在第一段前面,然后就可以打开环成为一个序列了。
如果judge[n+1]=1(我们初始设定judge[1]=1),说明两段真假性质相同,可以直接连上。然而,如果第一段首尾并不同真假(即a[1].same=0),按照定义,第一段增加的伴随真话数应该为最后一段的伴随假话数,否则正常加伴随真话数。
如果judge[n+1]=0,说明两段真假性质不同。很显然对伴随真话数的更新与刚才相反,首尾同真假的性质也应该取反。在更新judge的时候为了统一,我们总是令一块的第一句为真话,以此判断剩下的真假。现在1已经不是第一块的第一句了,所以我们还要把从1开始所有第一块的judge取反,这段的长度额外用一个变量标记就可以了。
这段分类讨论或许不难理解,但是2.5个小时的debug很大部分都卡在这一段,因为我一开始对res,right,same,judge四者的关系是混乱的,有错误也不容易发现。
附上这一段的代码:
scanf("%d",&n);
//memset(a,0,sizeof(a));
//memset(b,0,sizeof(b));
cnt = 1,vis = 0,mark = 0,fmark = 0,k = 0;
judge[1] = 1;//初始化
for(i = 1;i <= n;i++){
scanf(" %c",&z);
if(z == '$'){
scanf("%d",&x);
if(k == 1) fmark = mark;//记录第一块的长度
a[++k].num = i - mark;
a[k].right = cnt;
a[k].res = x;
a[k].same = (judge[i] & 1);
if(!a[k].same) a[k].right = a[k].num - a[k].right;
mark = i;
cnt = 1;
judge[i + 1] = 1;//令第一块的为真话
belong[i] = k;
continue;
}
if(i < n){
if(z == '+') judge[i + 1] = (judge[i] & 1);
if(z == '-') judge[i + 1] = (judge[i] ^ 1);
if(judge[i + 1]) ++cnt;
belong[i] = k + 1;//注意:此时这一段还没有编号
}
if(i == n){
if(!k){//无操作一
vis = 1;
if(z == '+') temp = (judge[i] & 1);
if(z == '-') temp = (judge[i] ^ 1);
if(judge[1] != temp) printf("inconsistent\n");//前后矛盾
else{
printf("consistent\n");
for(i = 1;i <= n;i++){
if(judge[i]) printf("t");
else printf("f");
}
printf("\n");
}
}
else{//有操作一
a[++k].right = cnt,a[k].num = i - mark;
if(z == '+') judge[i + 1] = (judge[i] & 1);
if(z == '-') judge[i + 1] = (judge[i] ^ 1);
if(judge[i + 1] == judge[1]){
if(a[1].same) a[1].right += a[k].right;
else a[1].right += a[k].num - a[k].right;
}
else{
if(a[1].same) a[1].right += a[k].num - a[k].right;
else a[1].right += a[k].right;
a[1].same ^= 1;
for(j = 1;j <= fmark;j++) judge[j] ^= 1;
}
a[1].num += a[k].num;
k--;//去掉这个块
for(j = mark + 1;j <= n;j++) belong[j] = 1;//容易忘记
}
}
}
现在我们已经得到了所有的块,接下来就是去重了。
显然我们需要保留原始序列,所以用一个c来代替a进行排序,然后再用b存储c去重后的结果。去重后,记录下所有操作一全为假时的伴随假话数sum。
接下来就到了查询。首先特判全为假的情况,枚举每一个res是否与sum相同,相同则直接弹出,否则继续下去。如果确实未被弹出,就可以输出这种解了。
这个输出的过程是有讲究的:如果某点所在的这一段same=0,我们取res为假的时候第一句话性质相反,应该为真,所以顺推下来,judge为真输出t,为假输出f;反之亦然。
如果排除掉全为假的情况,枚举不必多说,关键还是在于判断输出的内容。仿照刚才的讨论,如果某点所在的这一段操作一与枚举到的要求数目相同且这段首尾性质相同,那么要求操作一为真,首也为真,所以judge为真输出t,judge为假输出f。同理,如果某点所在的这一段操作一与枚举到的数目不同且这段首尾性质不同,那么要求操作一为假,而首为真,还是judge为真输出t,judge为假输出f。另外两种情况也同理。
这段的判断如下:
if(sum == b[i].res){
vis = 1;
printf("consistent\n");
//for(j = 1;j <= n;j++){
// printf("%d %d\n",a[belong[j]].res,a[belong[j]].same);
//}
for(j = 1;j <= n;j++){
if(a[belong[j]].res == b[i].res){
if(a[belong[j]].same){
if(judge[j]) printf("t");
else printf("f");
}
else{
if(judge[j]) printf("f");
else printf("t");
}
}
else{
if(a[belong[j]].same){
if(judge[j]) printf("f");
else printf("t");
}
else{
if(judge[j]) printf("t");
else printf("f");
}
}
}
printf("\n");
}
这一段debug的时候确实逻辑很容易混乱,不好想,如果对那几个变量维护内容定义不清楚非常容易出问题。所以这题的经验就是做之前必须要搞清楚变量的意义。
贴一个完整代码作为结尾:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e6 + 1;
struct yjx{
int right,num,res,same;
}a[N + 1],b[N + 1],c[N + 1];
bool cmp(yjx x,yjx y){
if(x.res != y.res) return x.res < y.res;
return 0;
}
bool judge[N + 1];
int belong[N + 1];
int main(){
freopen("truth.in","r",stdin);
freopen("truth.out","w",stdout);
int i,j,k,l,m,t,x,n,cnt,mark,fmark,res,sum,temp;
char z;
bool vis;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
//memset(a,0,sizeof(a));
//memset(b,0,sizeof(b));
cnt = 1,vis = 0,mark = 0,fmark = 0,k = 0,m = 0,temp = 0;
judge[1] = 1;
for(i = 1;i <= n;i++){
scanf(" %c",&z);
if(z == '$'){
scanf("%d",&x);
if(k == 1) fmark = mark;
a[++k].num = i - mark;
a[k].right = cnt;
a[k].res = x;
a[k].same = (judge[i] & 1);
if(!a[k].same) a[k].right = a[k].num - a[k].right;
mark = i;
cnt = 1;
judge[i + 1] = 1;
belong[i] = k;
continue;
}
if(i < n){
if(z == '+') judge[i + 1] = (judge[i] & 1);
if(z == '-') judge[i + 1] = (judge[i] ^ 1);
if(judge[i + 1]) ++cnt;
belong[i] = k + 1;
}
if(i == n){
if(!k){
vis = 1;
if(z == '+') temp = (judge[i] & 1);
if(z == '-') temp = (judge[i] ^ 1);
if(judge[1] != temp) printf("inconsistent\n");
else{
printf("consistent\n");
for(i = 1;i <= n;i++){
if(judge[i]) printf("t");
else printf("f");
}
printf("\n");
}
}
else{
a[++k].right = cnt,a[k].num = i - mark;
if(z == '+') judge[i + 1] = (judge[i] & 1);
if(z == '-') judge[i + 1] = (judge[i] ^ 1);
if(judge[i + 1] == judge[1]){
if(a[1].same) a[1].right += a[k].right;
else a[1].right += a[k].num - a[k].right;
}
else{
if(a[1].same) a[1].right += a[k].num - a[k].right;
else a[1].right += a[k].right;
a[1].same ^= 1;
for(j = 1;j <= fmark;j++) judge[j] ^= 1;
}
a[1].num += a[k].num;
k--;
for(j = mark + 1;j <= n;j++) belong[j] = 1;
}
}
}
if(k){
for(i = 1;i <= k;i++) c[i] = a[i];
sort(c + 1,c + k + 1,cmp);
//for(i = 1;i <= k;i++) printf("%d %d %d %d\n",c[i].num,c[i].right,c[i].res,c[i].same);
//printf("%d\n",m);
for(i = 1;i <= k;i++){
l = 0;
++m;
while(i + l <= k && c[i].res == c[i + l].res){
b[m].res = c[i].res;
if(!l) b[m].num = c[i + l].num,b[m].right = c[i + l].right;
else b[m].num += c[i + l].num,b[m].right += c[i + l].right;
//printf("%d %d %d\n",i + l,b[m].num,b[m].right);
++l;
}
i += (l - 1);
}
for(i = 1;i <= m;i++){
//printf("%d %d %d %d\n",b[i].num,b[i].right,b[i].res,b[i].same);
temp += b[i].num - b[i].right;
}
sum = temp;
vis = 1;
//printf("%d\n",temp);
for(i = 1;i <= m;i++){
if(b[i].res == temp) vis = 0;
}
if(vis){
printf("consistent\n");
//for(j = 1;j <= n;j++) printf("%d %d\n",belong[j],judge[j]);
for(j = 1;j <= n;j++){
if(a[belong[j]].same){
if(judge[j]) printf("f");
else printf("t");
}
else{
if(judge[j]) printf("t");
else printf("f");
}
}
printf("\n");
}
for(i = 1;i <= m && !vis;i++){
sum = temp + 2 * b[i].right - b[i].num;
//printf("%d\n",sum);
if(sum == b[i].res){
vis = 1;
printf("consistent\n");
//for(j = 1;j <= n;j++){
// printf("%d %d\n",a[belong[j]].res,a[belong[j]].same);
//}
for(j = 1;j <= n;j++){
if(a[belong[j]].res == b[i].res){
if(a[belong[j]].same){
if(judge[j]) printf("t");
else printf("f");
}
else{
if(judge[j]) printf("f");
else printf("t");
}
}
else{
if(a[belong[j]].same){
if(judge[j]) printf("f");
else printf("t");
}
else{
if(judge[j]) printf("t");
else printf("f");
}
}
}
printf("\n");
}
}
}
if(!vis) printf("inconsistent\n");
}
return 0;
}
/*
3
3
+
+
$ 3
3
+
-
$ 3
1
-
*/