• OI中的小技巧[Version 0.1.1b]


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

    更新日志:

    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++语言篇

    " "[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)

    对于本地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:(displaystylelim_{ ext{.vimrc} o +infty} ext{vim}= ext{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 
    1 :!./%< < %<.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-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可以在这里下载)

    尾声

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

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

  • 相关阅读:
    [CQOI2006]凸多边形
    NOIP后省选集训前文化课划水记
    FFT字符串匹配
    ghj1222的代码规范
    博弈论
    P1350 车的放置
    P2110 欢总喊楼记
    虫食算
    3192: [JLOI2013]删除物品
    逛公园
  • 原文地址:https://www.cnblogs.com/topsecret/p/OI-tricks.html
Copyright © 2020-2023  润新知