常见问题集锦
好早就想写这篇集锦了,不过因为懒总是鸽。开始写的契机还是因为自己不长记性......因为个人的粗心或浮躁多次导致了非常不应该的错误,经教练提醒后有了这篇集锦,时刻提醒自己,也能随时回顾。
这篇集锦应该会常常更新,犯了什么错误就写什么吧(当然还是尽量少犯错误)。
关于考场
编程时
由于国内noi系列竞赛无反馈,所以编程时一定要细致,尽量做到:
- 写代码前先将关键步骤或公式在草稿纸上明确,保证方法一定没有问题,写代码时一气呵成。
- 每道题都要写暴力(除了实在难写或太过显然无暴力做法),一是如果没能想出正解不至于爆零,二是想出了正解后能对拍排错。
- 能对拍尽量对拍,自己手造测试数据,保证考虑到所有可能出现的情况。
- 做题时,审题是关键,必须深入与全面,学过的知识与做过的题都是分析问题的有利武器;编写代码要细致,多写函数,便于调试;只有这样,才能达到你的期望。
结束前
后 10 分钟不要再编程,检查一下提交的文件夹中的代码是否符合要求。
- 检查文件:文件重定向,文件名,输入输出文件名,子目录文件名。文件名最好直接从题面上复制过来,别对你的词汇量有过分自信。freopen语句最好实际编译运行一遍,确保不会出现奇奇怪怪的错误。
- 检查代码:数据类型,数据精度,空间限制,赋初值,输入输出格式,调试时对语句的操作是否还原完全等,确保程序能正常运行,不会出现非预期的MLE,PE,RE甚至CE等。
- 建议编写代码放入新建文件夹(D盘,关机后不会丢失数据)编写,写完后存入指定文件夹,避免出现代码覆盖等错误。
- 一定要杜绝一切的不小心的人为错误,显然这种错误是致命的。
(感谢来自学长wangdyakioi和deafakioi的总结)
关于代码
- 写点分治板题的时候,曾经在求重心的时候犯了这样一个错误:
void dfs1(int u,int f)
{
//......
mxsz[u]=max(mxsz[u],n-sz[u]);
if(mxsz[u]<mxsz[rt])rt=u;
}
这里换根时用n减出了错,应该使用当前子树大小去减。于是看了眼题解,题解是这么写的:
void dac(int u)//分治
{
vis[u]=1;
for(R i=lst[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(!vis[v])
{
sum=sz[v];
//......
}
}
}
void dfs1(int u,int f)
{
//......
mxsz[u]=max(mxsz[u],sum-sz[u]);
if(mxsz[u]<mxsz[rt])rt=u;
}
然而这种找子树大小的做法是错的,原因是当前的sz是以上次点分重心(也是上次点分中心的某个儿子)\(r_1\)为根的sz,而这次点分重心 \(r_2\) 的某个儿子在以 \(r_1\) 为根时会是 \(r_2\) 的父亲,子树sz就求错了。但玄学的是,这样做正确性和时间复杂度并不会假,具体证明见 一种基于错误的寻找重心方法的点分治的复杂度分析。
-
写FFT题的时候,若FFT要用多次,尽管最后卷出来虚部为零也要主动清零,避免误差累积。
-
126271可以把unordered_map/unordered_set的复杂度卡到大致n^2级别,慎用
-
匈牙利算法:
正常写法mch数组在每次匈牙利算法执行前应该赋值为0,但是如果对点的编号是从0开始的话,mch数组就要赋值为-1。
- 线段树的两种形态:点和线段
- pushdown函数未把lazy也下放
对于区间修改的题,最容易出问题的地方是lazy标记的合并。为了保证每次区间修改操作仍为O(logN)的复杂度,我们选用了打上lazy标记,在询问到时再处理的方式。如果当前节点下放lazy时,左右儿子节点也有lazy标记,那么下放时必须考虑lazy的合并。
对于lazy的合并,有绝对正确的做法:遇到左右儿子有lazy就递归处理,先处理了左右儿子的lazy下放,再处理当前节点的lazy。然而这样会使lazy退化,putdown函数失去了O(1)的时间复杂度优势。我们必须尽可能保证putdown函数O(1)的时间复杂度(如:SCOI2010序列操作是线段树的模板题,但是如果没处理好各种类型lazy的合并就会WA)。
- lazy的合并很多情况下不满足交换律,如本题“合并 父亲lazy=-1与儿子lazy>0”和“合并 父亲lazy>0与儿子lazy=-1””处理方式有所不同。
- 考虑lazy的合并时,除了当前节点和左右儿子,还要消去对儿子的儿子节点操作正确性造成的影响。这样其实已经有递归的意思,但是没有递归那么多的时间。
原文链接:https://blog.csdn.net/rgnoH/article/details/77581505
-
动态开点线段树易MLE,如果可以用离散化还是尽量离散化,实在不行可以尝试位域乱搞......
(但是我不会) -
【莫队】 歴史の研究
回滚莫队版题,但是用普通莫队写。错误原因:滥用奇偶排序优化。当遇到区间为负(右端点在某一时刻在左端点的左边)的情况不允许存在的题目,不能使用奇偶排序优化。只用右端点单调上升的排序方式可以保证不会出现区间为负的情况。
-
dijkstra算法求最短路,结果重载小于号时符号写反,变成了大根堆,然后华丽地T掉了...
-
差分约束算法判无解时,若加了超级源点,记得将点数+1
-
dfs求叶子的dfs序(只算叶子),我的写法是:
dfn[u][0]=min(dfn[u][0],dfn[v][0]);
dfn[u][1]=max(dfn[u][1],dfn[v][1]);
即自己的左边界是儿子们最小的左边界,右边界是儿子们最大的右边界。此时注意把dfn[u][0]赋为inf.
-
树换根时,考虑以u为根的内子树(新根在子树外)和外子树(新根在子树内且不为u)的dfs序,内子树显然是\([dfn[u][0],dfn[u][1]]\),但是注意到外子树并不是简单的\([1,dfn[u][0]]\cup (dfn[u][1],n]\),因为根据新根的不同,不同的u的儿子也可能在外子树里。设新根到u的路径上u的儿子为v,则外子树的dfs序为\([1,dfn[v][0])\cup (dfn[v][1],n]\)
-
两数相加取模,为了卡常改模为减,必须保证两数都为非负数。
或者这么写:
int pls(int a,int b)
{
a+=b;
if(a>=P)return a-P;
if(a<=-P)return a+P;//if(a<0)return a+P;
return a;
}
-
计数题,计算过程中允许负数出现,输出结果时记得转为正数。
-
memset指针不要sizeof
-
卡空间司马。对于一些卡空间的题,可能考虑改递归为循环。
int getf(const int &x){return x==f[x]?f[x]:f[x]=getf(f[x]);}
1e6并查集这么写被卡了,可以改成下面这样:
int getf(int x){
if(f[x]==f[f[x]])return f[x];
int u;
for(u=f[f[x]];f[u]!=u;u=f[u]);
for(int v=x;f[v]!=v;v=f[v])f[v]=u;
return f[x];
}
一些技巧
-
对于某些卡精度的题,要求整数精度和小数精度都较高,可以考虑把整数部分和小数部分分开存。
-
卡常:要求答案对质数P取模,有大量x/2操作,不用2的逆元和long long,可以这样做:
- 若x为偶数,直接x/2;
- 若x为奇数,可以(x+P)/2.
该版块会根据我犯的错误实时更新。
浙公网安备 33010602011771号