OI中的小技巧[Version 0.1.1b]

OI中的小技巧[Version 0.1.1\(\beta\)]

更新日志:

0.1.1:

本文主要介绍了我作为一个OIer在退役前使用的一些小技巧。由于内容繁多,建议先看一遍目录,找到自己需要/感兴趣的部分。(如果刚刚入门,可以考虑从头看到尾)

由于时间仓促,可能会有一些笔误。如有发现,请不吝指出,谢谢!

注:这里说的都是一些比较基本的方法和技巧,如果想要知道更多,请自行百度。(这里假设你是在NOI Linux下的命令行上)

更好的阅读体验可以下载这个pdf

0. 杂篇

在读这篇文章前,请确认自己对以下内容有所了解:(如果不了解,建议随便百度一篇"Linux命令行入门",比如说这篇博客。对命令行熟悉之后,可以用help <shell命令>man <程序>info <程序>进行查找。顺便,对man而言,在打开时会提示“按q退出”,请不要忽略!):

  • mkdir <文件夹名>:在当前目录下创建文件夹。
  • cd <文件夹>:移动到文件夹里去。
  • g++ <文件> <选项>:编译器/编译命令。
  • vim <文件>:一个文本编辑器。
  • cp <文件> <文件/文件夹>`:复制一个文件到指定文件(夹)。
  • 等等……

编译器篇

这里介绍一下g++的一些很有帮助的编译选项。(这里假设你是在NOI Linux下的命令行上):

需要说的一点是,这些选项都最好在文件之前输入。

  • -std=c++11:支持C++11选项!(NOI2020评测默认开启)
  • 调试神器-fsanitize=address:在数组越界时或者递归层数过深时会报错并输出错误信息!!!
  • -ftrapv会检测整数溢出!!!在溢出时会自动终止程序并提示已放弃。
  • -DONLINE_JUDGE:相当于在程序里加了一句#define ONLINE_JUDGE!!!

最后一条很有用。由于主流OJ上(如Atcoder、Codeforces、LOJ等)都会在编译时-DONLINE_JUDGE我们可以利用这一点方便调试

假设我们正在编辑的a.cpp文件,在调试时需要有时文件IO(Input/Output,即输入输出),有时标准IO,但是在交到网上去时使用标准IO,我们可以这样:

#include <bits/stdc++.h>
using namespace std;

int main() {
#ifndef ONLINE_JUDGE
  freopen("a.in", "r", stdin);
  freopen("a.out", "w", stdout);
#endif
  // Insert Code Here
}

其中,#ifdef ONLINE_JUDGE的意思是if define(d) ONLINE_JUDGE,即其条件是define了ONLINE_JUDGE就freopen

显然,有if就可以有else:我们可以在#endif之前插入一个可选的#else来做一些其他的操作。

注:在算法竞赛中可以通过-DONLINE_JUDGEmakefile.vimrc的组合,编译出一个标准输入输出,一个文件输入输出的程序来方便调试。

C++语言篇

" \n"[i==n]

假如要输出\(a_0\dots a_{n-1}\)\(n\)个数,两个数之间要有空格,行末要有换行符,但不能有空格,可以这么做:

for (int i = 0; i < n; ++i) {
  printf("%d%c", a[i], " \n"[i==n-1]);
}

其中," \n"表示的是一个字符串,[]表示的是取字符串中的元素。在\(i\ne n-1\)时," \n"[i==n-1]返回的是" \n"[0],即' '(空格)。否则,是一个换行符。

#define FOR(i,a,b)

有时候,打两个for循环时会有类似这样的错误:

for (int i = 0; i < n; ++i)
  for (int j = 0; i < n; ++j)

于是程序就爆零了。

解决方法:在程序的开头定义

#define FOR(i,a,b) for (int i = (a); i < (b); ++i)

就可以用

FOR(i,0,n) FOR(j,0,n)

来避免这样的错误了。(还少打了不少字!)

signed main()

使用这个后,再用#define int long long,编译就不会报错了!(适合临时发现数据范围过大时的补救QAQ)

调试

  • #define debug(x) cout << #x << " = " << x << endl

    其中,#x的意思是x所替换的变量的名字。这样,若a[i] = 1,就可以用debug(a[i]);输出a[i] = 1的语句,方便调试。美中不足的是,它并不会一并输出i的值。这种方法与#ifdef结合更加有效:

    #ifdef DEBUG
    #	define debug(x) cout << #x << " = " << x << endl
    #else
    #	define debug
    #endif
    

    这样,如果没有-DDEBUG的话,就不会输出调试语句,就不用每次都注释一遍了。

  • #define meow(args...) fprintf(stderr, args):实际上,因为一般评测都会忽略stderr,我们也可以利用它来帮助调试。(当然,实际交上去时,输出调试语句也需要时间。如果输出太多的话会TLE!

    实际使用和printf一样,如可以meow("i = %d, %d %d %lld", i, j, a[i], b[i][j]);这样。

对拍

作为检查程序错误的一种方法,对拍在比赛中几乎是必不可少的。对拍,即写两个程序,生成随机数据,计算答案并核对答案是否相同。如果不相同,那么肯定有一个程序出现了问题。

常见的对拍有两种姿势,不过都是利用shell的命令实现的。一般,对拍都包括这样一些命令:(相信学过C++的你能大概猜出它是什么意思):

for (i=1;;i++); do
	echo testcase$i
	./a < a.in > a.out
	./brute < a.in > a.ans
	if diff a.out a.ans > diff.log; then
		echo AC!
	else
		echo WA!
		break
	fi
done

需要指出的一点是:因为返回值0是程序正常退出的标志,所以if实际上检查的是返回值是否为0。(即:若程序返回值为0则进入if,否则进入else)

当然,如果你不熟悉shell,你还可以用C++文件来实现这一功能。这归功于C++中的system()函数,它可以调用shell来完成shell的一些操作,如编译,运行程序等,并返回该命令的返回值。

while (1) {
    system("./a < a.in > a.out");
    system("./brute < a.in > a.ans");
    if (!system("diff a.out a.ans")) {
        printf("AC!");
    } else {
        printf("WA!");
        break;
    }
}

makefile的使用

你可能有过这样的经历:你修正了程序,但是忘记编译了,运行对拍脚本的时候使用的仍然是之前的程序。于是你对着相同的(错误)结果百思不得其解:诶我明明改了程序啊,怎么还是有错?

这时候,你就可以求助makefile了。使用它,只要在对拍脚本最前面加一句make命令就可以方便的把所有更改过的程序重新编译啦!(make非常聪明:它不会重新编译没有更改过的程序)

以下是一个makefile的基本格式:(可执行文件名+":"+编译成可执行文件的文件名)

all: a gen brute

a: a.cpp
	g++ -std=c++11 -g -O2 -Wall -DONLINE_JUDGE -fsanitize=address -o a a.cpp

gen: gen.cpp
	g++ -O2 -Wall -o gen gen.cpp

brute: brute.cpp
	g++ brute.cpp -o brute -O2 -Wall

与vim中的autocmd:s[ubstitute]命令搭配,可以事倍功半:用autocmd在打开makefile时将以上模板复制进去,再使用:s命令替换。

make命令实际上相当于make all。而all: a gen brute就会

1. Typora

相信大家都对这个简约的跨平台Markdown编辑器不陌生。

然而,它除了能用数学公式做笔记之外,还可以用超链接功能整理/索引自己做过的题!甚至可以用全文搜索功能找到自己曾写过的笔记!俨然如一个微型的私人博客!

索引方式

创建一个文件,将所有的题目都放进去,可以加一些关键词方便搜索。

对于在网站上交的题目,以Codeforces为例,可以将提交记录页面的题目名称复制下来,再复制进Typora时会自动加超链接。效果如下:

VP:Codeforces Round #659 (Div. 2)

对于本地pdf文件(或者其他非txt,md,doc,docx的文件),拖入(正在编辑索引文件的)窗口便可以创建,效果如下:

7/16

2020-07-16-NOI模拟 problem.pdf solution.pdf

(题目略)

题目在../exam/目录下(上一层目录的名为exam的文件夹)时,效果如下。

 [2020-07-10-NOI模拟](../exam/2020-07-10-NOI模拟)  [problem.pdf](../exam/2020-07-10-NOI模拟/down/problem.pdf)  [sol.pdf](../exam/2020-07-10-NOI模拟/sol.pdf) 

对于自己的笔记,同样可以索引(这里假设索引放在了笔记文件夹内):

  • 先显示侧边栏(在“显示”选项下,也可以用快捷键Command(Ctrl) + Shift + L打开),并设置文件树视图(Command(Ctrl) + Control(Shift) + F),再找到侧边栏中的Markdown文件/文件夹,将其拖入(正在编辑索引文件的窗口)即可。

在Finder/资源管理器下将Markdown文件或是文件夹拖入会导致新打开一个Typora窗口(或是标签页,如果你进行了设置的话)编辑这个笔记,将窗口顶上的Typora图标拖入即可。

搜索

震惊!Typora居然支持对当前目录下的所有文件进行搜索!妈妈再也不用担心我找不到整理的模板啦!

Command(Ctrl) + F是对当前文件进行搜索,用Command(Ctrl) + Shift + F即可启用全局搜索。

如果之前做索引的时候设置了关键词,搜索时会异常方便。(Typora不支持标签,可以用#文本的形式手动加入并搜索)

2. vim篇

[前言]为什么使用vim?

如果你是一个国赛选手(或者有志于成为一个),在NOI考场上是只能用Linux的。这时,你可以使用一些其他的文本编辑器,如gedit、vim、emacs等。笔者强烈推荐使用vim。

有了vim,你可以:

  • 基本上实现Dev-C++能够提供的(除经常崩溃的调试外)的所有功能:
    • 括号补全
    • 一键编译&运行
  • 代码自动缩进更加舒适
  • 用fold折叠代码
  • 分屏查看代码/输入输出文件,方便
  • 每次打开一个C++文件(或者其他文件类型)就自动加载模板——虽然Dev-C++也可以做到这点。
  • 支持可持久化的撤销(树)
  • 与更多……

教程

这里假设你已经打开了NOI Linux的终端,并且已经用cd命令回到了主目录下。

这里还假设你已学会基本的vim操作,如不会可以百度或参考这篇文章搜视频教程在终端下使用vimtutor命令进行学习。

有一些vim选项是可以在平时练习的时候给予很大便利,但是在考试的时候输入需要耗费很多时间。还有一些即使在考试时也很容易准备好。

这里讲的都只是一个大概,因此强烈建议用vim自带的帮助查看选项命令的含义以做更多了解::help 'number'会查看number选项的含义;:help map会查看map命令的含义。(注意前面的选项有引号,后面的命令没有)

  • 如要了解更多查找的方法,请输入:help help-context(或者:help之后向下滚动一些)

如果你想偷懒,也可以用:h命令来达到同样的目的。(:h:help的简写)

有意思的(普通模式)命令列表:(:h

  • q@:录制与回放宏
  • yp:复制与粘贴。
  • CTRL-P与CTRL-N:代码补全。
  • CTRL-U与CTRL-D:滚动半个屏幕。
  • A与I:进入插入模式并把光标放在行首/行末。
  • S:删除整行内容,保留缩进,并进入插入模式。
  • C:删除光标之后的内容,并进入插入模式。
  • J:合并多行。
  • {}:向前和向后移动到一个空行——如果你在不同的地方有意识空行的话,这会帮助你快速跳到代码的不同地点!
  • :tag <function>:在用命令行中的ctags命令处理文件之后,可以用它来快速定位到函数的位置。

你可以通过:h quickref来根据你的需要找到更多命令或选项。

配置简单的.vimrc:\(\displaystyle\lim_{\text{.vimrc}\to +\infty}\text{vim}=\text{IDE}\)

由于vim尽管默认有代码高亮,但是有不显示行号,一个tab是8个空格等等问题。我们需要通过编辑vim的配置文件,才能把vim配置得像一个IDE的编辑模式。

打开.vimrcvim ~/.vimrc甚至是gedit ~/.vimrc),输入:

set number tabstop=4 shiftwidth=4 cindent mouse=a

由于vim的每个选项都有简写的版本,上述命令还等价于:

set nu ts=4 sw=4 cin mouse=a

注意:由于在保存后并不会source(重新读取).vimrc,所以你需要退出后再进入,或者用:so ~/.vimrc命令重新读取。(又或者用autocmd使得每次保存.vimrc后都会source一下)

注:如果不想用:help搜索的话,这些命令应该其他博客会有讲解,可以随便百度一篇”OI中vim的使用“之类,比如洛谷的这篇日报,以及知乎问题这篇博客

附加:如果你有兴趣,可以尝试(或者搜索)一下以下这些选项:(或者直接:help 05.9来查找以下大部分选项的解释)

  • relativenumberrnu
  • shoucmd
  • wildmenu
  • incsearch
  • ignorecase
  • wrap
  • scrolloff
  • list
  • listchars=tab:>-,trial:-
  • cmdheight

如果你不喜欢vim本身的配色,可以用colorscheme命令,如:

colorscheme evening

(evening配色是NOI Linux自带vim的配色中少有的所有字体都加粗的配色)

括号补全

inoremap ( ()<esc>i
inoremap [ []<esc>i
inoremap { {}<esc>i

有关撤回

有时,你退出了vim又回去时,会想要撤回(普通模式下的u命令——重做是CTRL-R)一些操作。这时,你可能会沮丧地发现vim并不会在退出后自动保存你的历史操作。然而,这个“可持久化撤销”的行为是可以被设置的:

set undofile

其简写为:

set udf
复盘

在考砸后,如果没有特地记录,我们会无从得知在一道题目上面花费了多久,这时可以通过:earlier:later来按时间顺序查看修改记录!(如果打开了undofile或者没有退出文件)

当然,你也可以直接用uCTRL-R来查看你的所有更改(右下角会显示发生更改的时间)。这样,你就会发现自己写代码和调试分别花了多久了!

分屏

:sp:vsp即可分屏。如没有参数,则默认是对目前正在编辑的文件分屏。

实际使用

假如有一道题是a,你正在编辑a.cpp,你可以使用:vsp a.in:sp a.out来做到同时看到a.cppa.ina.out三个窗口并进行编辑。因为分屏实际上相当于创建了一个窗口,也可以用常规的:q等命令关闭。

如果开了mouse=a,那么就可以用鼠标调整分屏大小、与在窗口中点击来切换当前活跃的窗口。(否则你可能需要参考一下vim的帮助,并记忆许多命令才能做到同样的事情……)

可以结合之后讲到的map命令将这个过程自动化,例子如下:

nmap \s :vsp %<.in<cr>:sp %<.out<cr>

可能遇到的问题

在分屏并运行程序之后,你可能会看到这样一条信息:

W11: Warning: File "a.out" has changed since editing started

这是因为vim会保护你正在编辑的文件不被其他程序更改。

你可以通过在.vimrc里面加入这样一句话:

set autoread

来使得它(基本)会每次帮你自动加载被更改过的内容。

有关多个输入文件

如果有多个输入文件,建议这么做:

:!cp a1.in a.in

而不建议更改freopen中的文件名或者在.vimrc中输入多个

nmap \s1 :vsp %<1.in<cr>:sp %<.out<cr>
nmap \s2 :vsp %<2.in<cr>:sp %<.out<cr>
nmap \s3 :vsp %<3.in<cr>:sp %<.out<cr>
nmap \r1 :!./%< < %<.in
...

其原因在于:

  • 如果更改了freopen中的文件名,有可能会忘记改回来——我省选时曾犯过这样的错,本来可以拿100分的D1T1直接爆零QAQ……
  • 尽管可以用CTRL-ACTRL-V来加快.vimrc文件的输入,这件事本身是非常繁琐且完全可以避免的……

编译

我们编辑a.cpp时会用g++ a.cpp -o a这样的命令来编译它,那么这样的功能应该怎么在vim中实现呢?

答:在另一个终端里面输入这个命令或是在Normal Mode下输入:!g++ a.cpp -o a后按Enter即可。

但是每次编译都输入一遍的话太费劲了,有没有一个能一劳永逸的办法呢?

使用map命令!

在.vimrc文件下增加如下内容。

nmap <F8> :!g++ % -o %< <cr>

map的意思是映射,nmap <F8>的意思是把<F8>这个按键映射都后面的命令。

众所周知,:在vim里是可以跟随wwrite)或者rread)这样的vim命令。同样,:!在vim里后面跟的是命令行下的命令,如lsmkdirg++等。(可以去vim里尝试输入:!ls并按下回车,你会发现它调用命令行,正确执行了ls命令)

%的含义是“当前文件名”(a.cpp),%<的含义是去掉扩展名之后的文件名(a)。<cr>的意思是回车(如果不加的话,实际只会输入:!g++ a.cpp -o a这一行字,还需要按回车才能执行)。

这样,就设置好按<F8>(键盘上的F8,不是<+F+8+>!)就自动编译了!

可以将其他的键也映射到不同的编译选项中,如:

nmap <F7> :!g++ % -o %< && echo Compiled! && time ./%< <cr>
nmap <F6> :!g++ % -o %< -Wall -std=c++11 -fsanitize=address -ftrapv -DONLINE_JUDGE  <cr>

实现了按<F7>编译并执行,按<F6>编译时带一些额外的选项等。

模板

如果你希望在打开一个.cpp文件时就自动加载进一个模板的话,你可以用vim做到这一点!

autocmd:自动执行命令

你可以在打开文件/新建文件/写入文件前/写入文件后等等{event}后执行一个自动命令,格式为:(详情可用:help了解)

autocmd [group] {event} {pat} [++once] [++nested] {cmd}

常用的一些{event}有:

  • BufNewFile:开始编辑新文件时。
  • BufWritePost:保存文件时(写入文件内容后)。

假设你在~/a.cpp处保存了你的模板文件,你可以在.vimrc内加入:

autocmd BufNewFile *.cpp 0r ~/a.cpp 

其中*是通配符,*.cpp表示匹配所有以.cpp结尾的文件名。0r ~/a.cpp表示在第0行后(第1行前)插入~/a.cpp文件的内容。

用fold折叠过长的模板

如果你习惯在模板里定义一大堆这样的东西:

#include <bits/stdc++.h>
#define pb push_back
#define mp make_pair
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
#define rall(x) (x).rbegin(), (x).rend()
#define FOR(i,a,b) for (int i = (a); i < (b); ++i)
#define ROF(i,a,b) for (int i = (b)-1; i >= (a); --i)
#define mset(x,c) memset(x, c, sizeof(x))
#define mem0(x) mset(x,0)
#define mem1(x) mset(x,-1)
#define memc(x,y) memcpy(x, y, sizeof(x));
#define P(a,n) FOR(_,0,n) _W(a),printf("%c", " \n"[_==n-1])
#define print(a,n) cout << #a << " = ";FOR(_,0,n) _W(a),printf("%c", " \n"[_==n-1])
using namespace std;

template<class T> void _R(T &x) { cin >> x; }
void _R(signed &x) { scanf("%d", &x); }
void _R(int64_t &x) { scanf("%lld", &x); }
void _R(double &x) { scanf("%lf", &x); }
void _R(char &x) { scanf(" %c", &x); }
void _R(char *x) { scanf("%s", x); }
void R() {}
template<class T, class... U> void R(T &head, U &... tail) { _R(head); R(tail...); }
template<class T> void _W(const T &x) { cout << x; }
void _W(const signed &x) { printf("%d", x); }
void _W(const int64_t &x) { printf("%lld", x); }
void _W(const double &x) { printf("%.16f", x); }
void _W(const char &x) { putchar(x); }
void _W(const char *x) { printf("%s", x); }
template<class T,class U> void _W(const pair<T,U> &x) {_W(x.fi); putchar(' '); _W(x.se);}
template<class T> void _W(const vector<T> &x) { for (auto i = x.begin(); i != x.end(); _W(*i++)) if (i != x.cbegin()) putchar(' '); }
void W() {}
template<class T, class... U> void W(const T &head, const U &... tail) { _W(head); putchar(sizeof...(tail) ? ' ' : '\n'); W(tail...); }
#ifdef LOCAL
 #define debug(...) {printf(" [" #__VA_ARGS__ "]: ");W(__VA_ARGS__);}
#else
 #define debug(...)
#endif

typedef vector<int> vi;
typedef pair<int,int> pii;
typedef long long ll;

那么你可能需要每次复制模板的时候把它折叠起来,方便移动。vim本身就支持这么做。(详见:help usr_28.txt

在.vimrc里面加一句:

set fdm=marker

再在代码块的前后加入{{{}}}标记:

/*{{{*/
// Code Here...
/*}}}*/

你会发现它会把标记之间的代码折叠起来!瞬间感觉清爽多了!

有关在保存.vimrc后自动source的事

如果你只是用au BufWritePost .vimrc so %来这么做,随着保存.vimrc的次数增加,你的vim 会 逐 渐 变 卡。

这是因为你每source一次又会新加载一句au BufWritePost .vimrc so %,使得每一次保存都会source若干遍!

我们可以使用augroup来阻止这件事情的发生(:h)。

augroup VIMRC
	au!
	au BufWritePost .vimrc so %
augroup END

这个命令的主要意思是:把自动source的命令包含在了一个组里,每次source .vimrc的时候都会先把组里的autocmd清空。

自动定位到第一个编译出错的位置

前置知识:makefile的使用

你知道吗?在vim里面就可以执行make命令:在普通模式键入:make并按回车即可!

你可能会想:这有什么大不了的,不就是一种新的编译方法吗?这个在讲编译的时候不是已经讲过了吗?

其实,:make还真有不一样的地方!

如果你在vim中:make,在编译之后,vim会把光标自动定位到第一个编译出错的位置!这可以大大方便你改错!

当然,有时候它的定位会有些笨拙,但大部分时候它是可以指望的。

你问如果一次要改多个错怎么办?那好办,只要在改完错后在普通模式输入:cnext即可到下一个编译错误的地方!

注::cnext可以简写为:cn:make可以简写为:mak。你可能会想要把它们map一下。

拓展阅读

如果想要学到更多,建议阅读位于:help中的User Manual(有关vim的一本已经有些过时的书可以在vim官网找到,中文翻译版的User Manual可以在这里下载)

尾声

本篇只是一份草稿,虽然基本涵盖了我用的大部分技巧,但还有许多未完善和待补充的地方。

希望各位读者能向作者指出发现的错误或者分享想法。

posted @ 2020-12-04 19:42  frank3215  阅读(685)  评论(0编辑  收藏  举报