1. 程式人生 > 實用技巧 >OI中的小技巧[Version 0.1.1b]

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可以在這裡下載)

尾聲

本篇只是一份草稿,雖然基本涵蓋了我用的大部分技巧,但還有許多未完善和待補充的地方。

希望各位讀者能向作者指出發現的錯誤或者分享想法。