OI中的小技巧[Version 0.1.1(eta)]
更新日志:
0.1.1:
- 增加了[
" "[i==n]
](#" "[i==n]
)- 增加了自动定位到第一个编译出错的位置
- 增加了复盘
- 做了很多微小的改动。
本文主要介绍了我作为一个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_JUDGE
与makefile
或.vimrc
的组合,编译出一个标准输入输出,一个文件输入输出的程序来方便调试。
C++语言篇
"
"[i==n]
假如要输出(a_0dots a_{n-1})这(n)个数,两个数之间要有空格,行末要有换行符,但不能有空格,可以这么做:
for (int i = 0; i < n; ++i) {
printf("%d%c", a[i], "
"[i==n-1]);
}
其中,"
"
表示的是一个字符串,[]
表示的是取字符串中的元素。在(i
e n-1)时,"
"[i==n-1]
返回的是"
"[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)。
- [x] A - Common Prefixes:水构造题。
- [x] B1 - Koa and the Beach (Easy Version):简单DP题。
- [x] B2 - Koa and the Beach (Hard Version):DP优化/贪心。
- [x] C - String Transformation 1:贪心/转图论问题。
- [x] D - GameGame:拆位+简单博弈论。
对于本地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
与@
:录制与回放宏y
与p
:复制与粘贴。- CTRL-P与CTRL-N:代码补全。
- CTRL-U与CTRL-D:滚动半个屏幕。
- A与I:进入插入模式并把光标放在行首/行末。
- S:删除整行内容,保留缩进,并进入插入模式。
- C:删除光标之后的内容,并进入插入模式。
- J:合并多行。
{
和}
:向前和向后移动到一个空行——如果你在不同的地方有意识空行的话,这会帮助你快速跳到代码的不同地点!:tag <function>
:在用命令行中的ctags
命令处理文件之后,可以用它来快速定位到函数的位置。你可以通过
:h quickref
来根据你的需要找到更多命令或选项。
配置简单的.vimrc:(displaystylelim_{ ext{.vimrc} o +infty} ext{vim}= ext{IDE})
由于vim尽管默认有代码高亮,但是有不显示行号,一个tab是8个空格等等问题。我们需要通过编辑vim的配置文件,才能把vim配置得像一个IDE的编辑模式。
打开.vimrc
(vim ~/.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
来查找以下大部分选项的解释)
relativenumber
(rnu
)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
或者没有退出文件)
当然,你也可以直接用u
和CTRL-R
来查看你的所有更改(右下角会显示发生更改的时间)。这样,你就会发现自己写代码和调试分别花了多久了!
分屏
用:sp
和:vsp
即可分屏。如没有参数,则默认是对目前正在编辑的文件分屏。
实际使用
假如有一道题是a
,你正在编辑a.cpp
,你可以使用:vsp a.in
和:sp a.out
来做到同时看到a.cpp
、a.in
、a.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
1 :!./%< < %<.in
...
其原因在于:
- 如果更改了
freopen
中的文件名,有可能会忘记改回来——我省选时曾犯过这样的错,本来可以拿100分的D1T1直接爆零QAQ…… - 尽管可以用
CTRL-A
与CTRL-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里是可以跟随w
(write
)或者r
(read
)这样的vim命令。同样,:!
在vim里后面跟的是命令行下的命令,如ls
、mkdir
、g++
等。(可以去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-1])
#define print(a,n) cout << #a << " = ";FOR(_,0,n) _W(a),printf("%c", "
"[_==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) ? ' ' : '
'); 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可以在这里下载)
尾声
本篇只是一份草稿,虽然基本涵盖了我用的大部分技巧,但还有许多未完善和待补充的地方。
希望各位读者能向作者指出发现的错误或者分享想法。