常见问题集锦

好早就想写这篇集锦了,不过因为懒总是鸽。开始写的契机还是因为自己不长记性......因为个人的粗心或浮躁多次导致了非常不应该的错误,经教练提醒后有了这篇集锦,时刻提醒自己,也能随时回顾。

这篇集锦应该会常常更新,犯了什么错误就写什么吧(当然还是尽量少犯错误)。

关于考场

编程时

由于国内noi系列竞赛无反馈,所以编程时一定要细致,尽量做到:

  • 写代码前先将关键步骤或公式在草稿纸上明确,保证方法一定没有问题,写代码时一气呵成。
  • 每道题都要写暴力(除了实在难写或太过显然无暴力做法),一是如果没能想出正解不至于爆零,二是想出了正解后能对拍排错。
  • 能对拍尽量对拍,自己手造测试数据,保证考虑到所有可能出现的情况。
  • 做题时,审题是关键,必须深入与全面,学过的知识与做过的题都是分析问题的有利武器;编写代码要细致,多写函数,便于调试;只有这样,才能达到你的期望。

结束前

后 10 分钟不要再编程,检查一下提交的文件夹中的代码是否符合要求。

  • 检查文件:文件重定向,文件名,输入输出文件名,子目录文件名。文件名最好直接从题面上复制过来,别对你的词汇量有过分自信。freopen语句最好实际编译运行一遍,确保不会出现奇奇怪怪的错误。
  • 检查代码:数据类型,数据精度,空间限制,赋初值,输入输出格式,调试时对语句的操作是否还原完全等,确保程序能正常运行,不会出现非预期的MLE,PE,RE甚至CE等。
  • 建议编写代码放入新建文件夹(D盘,关机后不会丢失数据)编写,写完后存入指定文件夹,避免出现代码覆盖等错误。
  • 一定要杜绝一切的不小心的人为错误,显然这种错误是致命的。
    (感谢来自学长wangdyakioideafakioi的总结)

关于代码

  • 写点分治板题的时候,曾经在求重心的时候犯了这样一个错误:
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。

  1. 线段树的两种形态:点和线段
  2. pushdown函数未把lazy也下放

对于区间修改的题,最容易出问题的地方是lazy标记的合并。为了保证每次区间修改操作仍为O(logN)的复杂度,我们选用了打上lazy标记,在询问到时再处理的方式。如果当前节点下放lazy时,左右儿子节点也有lazy标记,那么下放时必须考虑lazy的合并。
对于lazy的合并,有绝对正确的做法:遇到左右儿子有lazy就递归处理,先处理了左右儿子的lazy下放,再处理当前节点的lazy。然而这样会使lazy退化,putdown函数失去了O(1)的时间复杂度优势。我们必须尽可能保证putdown函数O(1)的时间复杂度(如:SCOI2010序列操作是线段树的模板题,但是如果没处理好各种类型lazy的合并就会WA)。

  1. lazy的合并很多情况下不满足交换律,如本题“合并 父亲lazy=-1与儿子lazy>0”和“合并 父亲lazy>0与儿子lazy=-1””处理方式有所不同。
  2. 考虑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,可以这样做:

    1. 若x为偶数,直接x/2;
    2. 若x为奇数,可以(x+P)/2.

该版块会根据我犯的错误实时更新。

posted @ 2020-10-22 21:47  nkxjlym  阅读(150)  评论(0)    收藏  举报