OI中的小技巧[Version 0.1.1b]
OI中的小技巧[Version 0.1.1\(\beta\)]
更新日誌:
0.1.1:
- 增加了[
" \n"[i==n]
](#" \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++語言篇
" \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)。
- [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:\(\displaystyle\lim_{\text{.vimrc}\to +\infty}\text{vim}=\text{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 \r1 :!./%< < %<.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"[_==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可以在這裡下載)
尾聲
本篇只是一份草稿,雖然基本涵蓋了我用的大部分技巧,但還有許多未完善和待補充的地方。
希望各位讀者能向作者指出發現的錯誤或者分享想法。