【每天一个爆零小技巧】个人用的程序小技巧及其他_自用
AFO了。
考试小策略
这一块解决的是策略问题和通用的问题排查方法。
考场上各个注意点
- 考前试机确实应该去打一点板子。效用倒不是会不会用上,而是调整心态。所以太难的或者不熟悉的就不用写了,写写SAM就够了。
- 考前试机需要打的:fastIO,Max/Min,BM
- 题目一定要全都看一遍之后再进行下一步。不这样的话,如果题目简单或者题目对于你的难度确实是1,2,3或者你能过掉前两题都没有问题,不过一旦题目1,3,2就完了。
- 个人以为需要特别注意这种问题的就是模拟赛的难度与你的成绩之间的关系和中和滴定一样有个突变的人
- 比如我,以往两天的NOIP难度的Day1,再怎么做都没关系,一般的NOIP模拟赛不一开始看全题目也总是能拿到最后一题的暴力分。但是难度略大的省选场,只要T2比较阴间或者出题人swap了T2T3,我就非常容易崩掉。表现在成绩上就是时而相当漂亮,时而拉胯得很。
- 暴力哪怕出事了(假了或者什么都过不了)也要留在程序里数据分治。因为有可能他能过一些奇怪的数据。(注意避免爆空间)
- 前一道题确定过了或者写的分没问题了再看下一道题,别着急。
- 最重要的还是long long、数组大小开够和不要爆空间。
- 不要莽,不要慌,自信一点,把自己的暴力分和力所能及的标算拿满了就进队了。贪,就会白给。慌,也会白给。
- 要记住特别做几件事:开long long和不开long long拍;看空间;纯随数据拍;稍微构造一下拍;看看会不会有为0为1的时候的特殊情况。
- 最让人难受的其实是看错题目。因为代码出锅对拍几乎肯定能检查出来,但是题目看错就没救了。一个补救方式是看样例解释,思考出题人为什么要这么讲。但是根本方法还是看仔细题面,去理解叙述思路,这样在很大程度上可以发现一些小细节。
- 总结一下不管说什么都要做的事情:
- 看文件。
- 暴力和提交的代码拍。暴力最好不要基于任何结论。
- 开define int long long和不开拍,最好再和__int128拍一下,最好再和数组使劲往大了开拍(包括线段树范围也使劲往大了开)。
- 看空间。(我再不看空间就是傻子)
- 在Linux下,开-Wall -fsanitize=address -std=c++11跑最大数据。
- 程序提交之前开文件跑样例。
- 检查数据分治的返回值、空间和输出量(不要跑了好多Subtask)。
- 树的题如果要拍,要造链、菊花、二叉树、链式堆。
关于考试策略的思考
- 最近一直遇到一个问题:我题目做得太慢了——一道数学题,比较简单的式子推导过程我会需要用很长时间;一道数据结构题,我会纠结很久一些奇怪的细节,导致我往下写一点就回去改,写一点再回去改,效率非常低。个人觉得问题在于头脑不清醒,没有想清楚整个的逻辑以至于细节完全不能获得自然方便的写法。
- 其实先把暴力写掉是一个很好的策略。其一是心态会好一点,毕竟已经有点分数保底了;其二是明确题意,避免写了半天题意错误的问题。所以我决定省选的时候不管正解是什么,先把每道题的暴力都写一下再说。
- 我进省队了!我在省选的时候确实本着这个原则救回来好多分,还是有用的。
常数小技巧
- 从部分角度观察卡常效果的网站:https://gcc.godbolt.org/
- 卡完常一定要测样例。
- 不论整数,浮点数,除法是真的慢
- 整数取模尽量避免,这也是大部分程序的卡常关键(比如20200610的T1,pcf就被两次取模卡掉了40分)。
- 要尽可能让几次对一个数组的调用地址距离近。比如\(f_{i,j,k}\)中,大部分运算是在相同的\(i,k\)下做的,那么我们应该存为f[i][k][j]。(upd20201029:这个东西真的还挺有用的,APIO2013机器人就是拿这个过的)(一个更好的例子:当我们定义ST表的时候,大部分时候我们应该定义ST[21][MAXN+1],实测很有效果。原因分析为在建立的时候这样就可以做到访问地址连续,但是查询的时候一般不管怎么写都访问地址都不会太连续)。
- 一个优秀的快读快输很有必要(但是事实上很多人的快读快输是错的,不能读-2147483648)
template<typename T> void Read(T &cn)
{
char c; int sig = 1;
while(!isdigit(c = getchar())) if(c == '-') sig = 0;
if(sig) {cn = c-48; while(isdigit(c = getchar())) cn = cn*10+c-48; }
else {cn = 48-c; while(isdigit(c = getchar())) cn = cn*10+48-c; }
}
template<typename T> void Write(T cn)
{
int wei = 0; T cm = 0; int cx = cn%10; cn/=10;
if(cn < 0 || cx < 0) {putchar('-'); cn = 0-cn; cx = 0-cx; }
while(cn)cm = cm*10+cn%10,cn/=10,wei++;
while(wei--)putchar(cm%10+48),cm/=10;
putchar(cx+48);
}
- 这里是一个我在某次训练里手打的fread,fwrite的模板,搭配上面的快读快输使用,感觉还挺好的。(程序开始的时候需要调用pre();结束时需要调用end();)
- update_2021_03_12:我曾经贴在这里的代码是错的,他不能正确处理文件读空了会发生什么。事实上系统有可能给你的数组最后面贴上一些不知道哪里来的数字。
const int MAXTMP = 1000000;
char tmp_in[MAXTMP], tmp_ou[MAXTMP];
int tmp_in_len, tmp_in_max, tmp_ou_len;
void pre_in() {tmp_in_len = MAXTMP; }
void pre_ou() {tmp_ou_len = 0; }
void end_in() {}
void end_ou() {fwrite(tmp_ou, sizeof(tmp_ou[0]), tmp_ou_len, stdout); }
void pre() {pre_in(); pre_ou(); }
void end() {end_in(); end_ou(); }
int getone() {if(tmp_in_len == MAXTMP) tmp_in_max = fread(tmp_in, sizeof(tmp_in[0]), MAXTMP, stdin), tmp_in_len = 0; return tmp_in_len >= tmp_in_max ? -1 : tmp_in[tmp_in_len++]; }
void putone(int cn) {if(tmp_ou_len == MAXTMP) fwrite(tmp_ou, sizeof(tmp_ou[0]), tmp_ou_len, stdout), tmp_ou_len = 0; tmp_ou[tmp_ou_len++] = cn; }
- 一个优秀的快速幂也比较有用
LL ksm(LL cn, LL cm) {LL ans = 1; while(cm) ans = 1ll*ans*(1+(cn-1)*(cm&1))%MOD, cn = cn*cn%MOD, cm = cm>>=1; return ans; }
- 快速乘的正解是__int128(但是比赛的时候还是要老老实实写正常的东西。)
- 循环展开
- 循环展开是最后的计策
- 循环展开的本质是缓存优化。可以参考APIO2019的课件。所以如果展开4层还没有什么效果,那基本是没救了。(除了矩乘)
- 一份比较好的示例代码
LL ans1 = 0, ans2 = 0;
int i = 1;
for(;i+3<=n;i+=4) {ans1 = ans1+(a[i]+a[i+1]); ans2 = ans2+(a[i+2]+a[i+3]); }
for(;i<=n;i++) ans1 = ans1+a[i];
ans1 = (ans1+ans2)%MOD;
- memset,最好算好要用前多少位,然后精确清零
- 正确分析好程序的复杂度瓶颈,然后只在瓶颈上优化,除非其它地方优化起来太方便了,可以顺手搞一下。毕竟卡常的时候是很容易fst的。
- memcpy也挺好用的。memcpy(destiny, source, sizeof(unit)*length)。
- 分块这种东西啊,T与不T常常就在常数之间。能用一些分块的数据结构之类的,就少用整数分块(活生生的例子:20201016_T2)。(我现在不知道我当时在写什么,完全看不懂)
- 在大规模处理的时候,少调用函数(比如你要回答1e7组询问,那还不赶快手动inline)。(例子:20201130_T3)
- ntt的常数有一部分来源于omg数组的不连续访问。可以这么写:
int erwei(int cn) {int guo = 0; while(cn) guo++, cn>>=1; return guo; }
void ntt(LL a[], int cn, int omg[])
{
int lin = erwei(cn)-2;
fan[0] = 0; for(int i = 1;i<cn;i++) fan[i] = (fan[i>>1]>>1)|((i&1)<<lin);
for(int i = 1;i<cn;i++) if(fan[i] > i) swap(a[fan[i]], a[i]);
for(int i = 2, m = 1;i<=cn;i = (m=i)<<1)
{
for(int j = 0;j<m;j++) omg2[j] = omg[Mn/i*j];
for(int j = 0;j<cn;j+=i)
for(int k = 0;k<m;k++)
{
int lin1 = a[j+k], lin2 = 1ll*a[j+k+m]*omg2[k]%MOD;
a[j+k] = lin1+lin2>=MOD?lin1+lin2-MOD:lin1+lin2;
a[j+k+m] = lin1<lin2?lin1-lin2+MOD:lin1-lin2;
}
}
}
- 别动不动就双哈希。太TM慢了。单哈希能解决几乎所有问题了。(例子:2021_02_26_T3)
- 有一个微小的卡常小技巧:如果确信一个数组中的元素相当小,可以开unsigned short或者unsigned char,也许能快一点(但是会增大fst的风险)
- swap这个东西有点好玩的。你想swap两个数组,于是你的这一句话的复杂度就变成了数组长度。这件事和时不时memset整个数组是异曲同工的。
- 事实上不开c++11的时候swap两个STL也是\(O(n)\)的复杂度。(开了之后它相当于swap指针,快得很)
- 循环展开的目的是卡缓存,也就是说在编译器的输出结果上循环展开的效果不一定非常好。而指针访问可以肉眼可见的优化循环的代码在编译器输出的汇编指令上的长度。同时,指针访问能轻松地循环展开。比如说这一段:
int *pos1 = &(h[ij][b.clen+1]);
int *pos2 = &(h[ij][b.clen]);
char *pos3 = &(b.c[b.clen]);
int j = b.clen;
for(;j>=ij;j--)
{
if((*pos2)) pos1 = pos2;
if((*pos3) == lin) (*pos1) = 0, (*pos2) = 1, pos1 = pos2;
pos2--; pos3--;
}
暴毙小技巧
这一块解决的是写完了在什么情况下会暴毙。
数组问题
- \(2^{20}\)开1e5,1e6都不够。
- 数组一定要科学清空
- 数据千万条,清零第一条;多测不清空,爆零两行泪。
- 分块内部开哈希,修改的时候一定要全面修改。
- 数据范围要好好注意一下,并不是每一道题的\(n\)和\(q\)都是相同的。
- 经典节目:数组开小(少加一,或者少开两倍)
- 要注意数据范围看对:有时候最大的数据范围会在数据范围那里的第一行,而不是最后一行————最后一行有可能是倒数第二个Subtask。
- 很多时候主席树开40MAXN没有问题,甚至可以说比较安全。但是我发现这还是有爆空间的风险。(我有一天的T1就是因为空间限制128MB,n大概是2e5,主席树一个节点里记录了四个元素,开了40MAXN,然后就光荣爆炸了)
- 假如有很多维的dp,那一定要看好每一维的数据范围是什么以及程序中dp的时候有没有爆掉。事实上这个东西造一个满数据就行了。
技术性问题
- 如何看空间
- 把所有东西都sizeof一下。有点麻烦,而且容易漏。但是还蛮准的。这个方法难以处理stl。
- 运行,并在认为空间最大的时候把程序卡住(一般是程序即将结束之时),用任务管理器看。优点在于这是实际运行内存,缺点在于你很难确定静态数组到底会不会出事,以及你的数据能不能让你的代码空间弄到最大。
- -Wall -fsanitize=address -fsanitize=undefined -std=c++11 -O2 都很有必要加上。并且要重视其中给出的所有warning。
其他问题
- 爆int和爆long long是真的自闭。我今天调题,有一大半的时间都是因为爆了int而自闭。+1+1+1
- 特别要注意的是计算几何和min25筛。计算几何的问题主要是存在叉积,而min25筛的问题是n都会爆int,直接n*n就爆long long了。
- 没有卡时间的题就直接define int long long吧。(注意空间不要爆掉)。(事实上有时候define int long long不会慢多少,在一些机器上因为一些奇怪的机制甚至会变快。)
- 以及推荐把没开long long和开long long的拍一拍。
- 名称相似而且作用相似的数组,一定要第一遍写对,并在静态查错的时候重点关注。最好名字要容易区分一点。
- 未定义行为检测真有用。-fsanitize=undefined
- 边界情况好好判啊。写完之后哪怕多花五分钟搞点边界数据卡一卡也好。
- 好好判无解之类的,千万不要默认他永远有解。
- 数据分治,如果要直接在主程序里return Sub1::main(); 那么Sub1::main()的返回值一定要是0。可以用未定义行为检测(-fsanitize=undefined)/-Wall/windows环境里的dev-c++来避免。
- 修改边权点权的时候,不仅要在维护的数据结构中改,还必须改一下原始数组。
- 读入优化好是好,就是判到EOF的时候容易出事。
- 读入的变量看清输入格式里面的顺序。不要搞反了。这个东西很多时候也是可以通过输出中间变量排查的。
- 关于一些计数题或者数学题的收敛/取0/除0,要好好讨论。很多题到最后就差这几个细节就能做对了。
- 一个例子:2021_02_24T1,我用第一个判断出最后的那一个情况无解之后,却没有将他立即杀掉,而是留下来判断第二个条件。偏偏第二个条件写得又不是很好,于是第一个条件相当于没有判。(于是一定要死刑立即执行)。
- 一道题如果要统计最多留下来几个,你能算最少扔掉几个,那很好,过题了。不过要注意不能把最少扔掉几个直接输出出去。
- 要注意如果题目中\(n,m,q\),那么对拍的时候要注意将三个数字搞得不一样来测试是否有怪事情发生。
- 注意数据分治要搞好,不要忘记return。
- 不要上头。暴力最好了。
- 注意不要爆int和爆long long。
- 如果你要搞虚树然后在虚树上dp,考虑好数组和线段树要开多大。
- 好好看题,看清楚样例解释的每一句话。
- 如果他有询问和修改,注意后效性以及当前询问附加的临时元素是否需要特殊讨论。
- 拍的时候,数组开大一点。以及不要太迷信自己推的性质,如果某个性质太过惊世骇俗但是又没有对复杂度根本提升,就扔掉吧。
- 看清题目。能写暴力的时候就先把暴力写了过掉样例。
- 开c++11的话,abs(a),其中a是一个__int128会直接CE。那不如直接不用__int128
- 少炫技,注意+=与*=如果左边long long,右边int是有可能出现爆int的情况的(但是经测试不一定会出事)。注意不要弄混==和=。
- 注意三分的两个断点不要搞混了。(经常搞混了样例也不会出错)
- 计算距离的时候,不要忘记开long double,不要直接返回long long。
- 输出调试一个比较难受的地方在于如果是在数据量大的情况下出事了,难调。这时可以善用assert。包括交互题也可以这样调。构造题也可以每构造出来新的一步就判断一下这一步是否合法。assert比较好的地方在于可以看出是第几行出错的,就很方便。
- 尽量不要忽视每一个re,要搞清底下的原因。
- 如果实在调不出来的时候,可以仔细考虑一下每个<=、+-1之类的地方。
- 如果有几个地位差不多的变量,建议测试的时候让他们不同(比如a和b,n和m和q)。这种问题也要特别注意。
- 如果有一个复杂度看着就和数据范围很贴合的算法,但是发现有一个细节处理不了,那一定要看看部分分有没有什么提示。
- 测试的时候注意调整分块长度之类的东西。
- 要特别构造数据卡一下分块和树剖看一看,别随便认为自己过了。
- 如果想利用分段打表的思想求逆元(就是每1000个左右的数字一起求个逆来去掉log),一定要注意有没有出现0。
算法碎碎念
这一块处理的是想不出来怎么办。
杂项
- 数论分块真香!
- Update 2020.06.11:他太蠢了,我昨天的T2,加个数论分块就wa了,一切都是因为我没有把一个每次都乘的数字做幂。但事实上那个数论分块不加这道题也能过。
- 模块化真香!
- 斜率优化判交点,如果直接用一些解析几何或者计算几何的方法(\((k_1-k_2)(b_3-b_2)<(k_2-k_3)(b_2-b_1)\)这种),很有可能会爆精度、爆long long,然后让你调到自闭。
- 分块真香。如果每一块中的操作极其雷同,甚至可以前缀和。
- 事实上分块可以有很大的用处。如果[区间操作单点查询]或者[单点操作区间查询],可以做到[操作根号查询O(1)]或者[操作O(1)查询根号]。除了[O(1)区间操作根号查询]如果要常数比较好看,只能做到区间必须是一个前缀或者后缀。
- 遇到[O(1)区间操作根号查询]直接zkw吧。事实上虽然可以先分一层块然后将操作转化成O(1)个长度为根号的区间上的区间操作,然后在长度为根号的块上利用ST表进行区间改单点查。
- 有些复杂的“高级数学结构”,比如线性基,多项式逆元,如果操作的原始元素相当简单(比如要求逆的多项式只有几项或者线性基的基础数字的0/1是很有规律的,使得你可以手推出来计算完成之后的多项式或者线性基长成什么样。事实上我用这个过了两道题。)但是这样子有个问题,就是容易上头,一上头就会沉迷其中出不来了。适合过了T1莽T2并且已经有了暴力的时候去做。
- 我人傻了。我今天想写一下2-SAT,很奇怪,但是更奇怪的是我竟然写完了之后还调了好一会儿。我甚至不会写tarjan了。觉得自己有必要把这些奇怪的算法都过一遍。
- 在遇到统计特定个数的某种东西的时候,不要忘记wqs二分这个技巧。
乱搞小技巧
- 也许,有些不会算的或者来不及写的东西,可以通过观察性质来给出一个在大部分时候正确的近似解(比如一个函数在大部分时候为1,当明确知道来不及算的时候,可以直接返回1).
- 爬山的时候,步长每次乘0.9之类的,然后最好加一个卡时间。
计数
- 可以一上来就莽式子,反正都是一通爆乘一通爆加,写就完事儿了。
- 考虑用dp来计数。
- 但是千万不能忘了容斥这个工具。
- 反演也挺香的。特别是二项式反演(有关选择,但是其中的组合数学部分要慎重考虑)和莫比乌斯反演(有关gcd,约数)。
- 很多时候不能一下子全都推完。这时候就要一步一步慢慢来,先用暴力写出来,看看这个式子到目前为止对不对。
- 普通的题目是推式子推到最后才要考虑套上多项式的。当然也有一上来就莽的。比如仙人掌计数。以及2021_02_17_T2我的方法。update20210722:现在大纲里没有ntt了,所以多项式题大概率变成了需要一上来就搞出多项式。
- 如果要维护形式幂级数,那么也可以维护点值,最后插值插回去。
- 有些东西写出式子来不一定能看出怎么优化。写到程序里能更清晰一点。
数学知识
- BM是个好东西。不过要注意一件事情,就是xyx的博客的代码是错的,而且算法流程中也有一点没有提到。这一点在评论区有提到(R'应该取最短的那个,而不是最近的那个)。另外zzq的博客是全部正确的。
- BM可以现推,具体好方法是把每个字母都约定好,然后一步一步考虑现在的\(r'\)会导致什么样的\(a'\),慢慢添加0和1之类的,一点点搞成需要的样子。
贪心构造
- 第一种思路:发现自己如果知道一段是有解的,可以用题目能接受的复杂度求出来。那就继续深刻理解判断有解的条件。尝试简化/形式化/用推理弱化。
- 第二种思路:发现自己如果知道一段是有解的,可以用题目能接受的复杂度求出来。考虑优化求解的方式,用这个方式来判断一个区间有无解。
关于网络流
- 建图:
- 如果是网格图,黑白染色与行列连边是非常好的思路。
- 如果一个人要用两种东西,那么把一种东西放在前面,一种放在后面跑费用流也是不错的。
- 费用流
- 我们通常跑的都是最小费用最大流。这时我们通常是要求流满的情况下费用尽可能小。但是有可能会有这种情况:我们出现了负权,每次流只是为了赚钱。我们也不一定要流满。此时就应该每次流都判断一下,如果开始亏钱就退出。参见20200929T2。
关于数据结构
- 我人傻了。我维护区间加区间乘的lct,竟然在pdown的时候搞反了乘和加的顺序(我一般都是先乘再加,因为这样处理tag的时候比较自然,但是我pdown的时候竟然先加再乘了)。
- KD-Tree的更新要很注意,不要随便更新0(当然也可以对0进行特殊处理使得更新他没用,比如一个区间最小值,0上设为INF。)其他数据结构同理。
- 一定要记住,线段树的节点要乘4。线段树分治的时候也要乘四。这太关键了。
一些很怪的东西
语法碎碎念
- c++中,class和struct的效率差不多。事实上struct的实现就是所有元素都是public的class。class的功能更强大,相应地,struct的运用更方便。用class还是struct完全是个人习惯问题。
- 我觉得struct和class的并存的原因,一个是兼容标准问题,一个是用法习惯问题。如果是一个五元组,显然用struct更符合“习惯”;而如果是一个功能比较复杂的东西,比如lct,或许使用class会更顺眼。
ntt模数(少见)
Number |
Primitive Root | max NTT |
---|---|---|
\(104857601 = 25\times 2^{22}+1\) | 3 | 4194304 |
\(104857601 = 25\times 2^{22}+1\) | 3 | 4194304 |
\(2025848833 = 483\times 2^{22}+1\) | 10 | 4194304 |
\(377487361 = 45\times 2^{23}+1\) | 7 | 8388608 |
\(2088763393 = 249\times 2^{23}+1\) | 5 | 8388608 |
\(754974721 = 45\times 2^{24}+1\) | 11 | 16777216 |
\(2130706433 = 127\times 2^{24}+1\) | 3 | 16777216 |
\(167772161 = 5\times 2^{25}+1\) | 3 | 33554432 |
\(2113929217 = 63\times 2^{25}+1\) | 5 | 33554432 |
\(469762049 = 7\times 2^{26}+1\) | 3 | 67108864 |
\(1811939329 = 27\times 2^{26}+1\) | 13 | 67108864 |
\(2013265921 = 15\times 2^{27}+1\) | 31 | 134217728 |