OI 中的小技巧(工具)

caijianhong發表於2024-05-09

OI 中的小技巧(工具)

目錄
  • OI 中的小技巧(工具)
    • ref
    • bash 基礎語法
      • 可執行檔案
      • 變數
      • 管道
      • if-else
      • for
      • while
    • bash 應用
      • time
      • ulimit
      • 批次過樣例
      • 花絮:freopen 開關
      • 對拍器
      • 備份工具
    • makefile
      • 基礎語法
      • 傳統題寫法
      • 互動題寫法
      • 打掃檔案
      • 結合 vim

ref

https://zhuanlan.zhihu.com/p/494668063

bash 基礎語法

可執行檔案

在 NOI Linux 或其它 Linux 環境下開啟終端,可以在終端中輸入命令:

you@localhost:~/test$ g++ a.cpp -o main
you@localhost:~/test$ ./main
1 2
3

這裡我們編譯了 a.cpp 檔案為 main,執行,發現它正確實現了計算 \(a+b\) 的功能。g++./main 是可執行檔案,在一條命令的開端,後面緊跟的都是引數。g++ 在環境變數中,main 在當前目錄下,在 bash 中執行不在環境變數中的可執行檔案需要寫出其路徑,最簡單的方法就是寫為 ./main. 表示當前目錄(.. 表示上一級目錄),/ 是資料夾名與檔名之間的分隔符。以上內容建議百度“Linux 命令列入門”進一步瞭解。

變數

bash 中可以定義變數。

you@localhost:~/test$ a=1
you@localhost:~/test$ echo ${a}
1

a=1a 是變數名,1 這裡當作一個字串。讀取變數 a 的內容,寫作 ${a}同樣這些東西建議百度瞭解

管道

bash 中存在名為管道的東西,作用是將上一個命令的標準輸出傳遞給下一個命令的標準輸入,符號是 |

you@localhost:~/test$ echo 1 2 | ./main
3

例如可以 echo 1 2 輸出 1 2,將其傳遞給 ./main 作為它的標準輸入。更有用的地方是,部分命令如 diff grep 可以透過指定檔名為 -(或其它,可以傳遞引數 --help 檢視)以讀取標準輸入作為檔案內容。因此可以幹出這樣的事情:

you@localhost:~/test$ echo 1 2 > in
you@localhost:~/test$ echo 3 > ans
you@localhost:~/test$ ./main < in | diff - ans
you@localhost:~/test$ echo 4 > out
you@localhost:~/test$ ./main < in | diff - out
1c1
< 3
---
> 4

Linux 檔名可以無字尾名。這裡依次建立了三個檔案。第一次執行 ./main,輸入 1 2 輸出 3,與檔案 ans 比較相同;第二次執行 ./main,輸入 1 2 輸出 3,與檔案 out 比較不同,輸出了錯誤資訊。

if-else

大概這樣寫:if {{condition_command}}; then {{echo "Condition is true"}}; fi

注意:{{condition_command}} 返回 0 的時候進入後面的語句塊,返回 0 一般表示這個命令成功執行。這一點與其它語言不同。

可以百度或者 help if 獲得更多幫助。

for

for {{variable}} in {{item1 item2 ...}}; do {{echo "Loop is executed"}}; done

for {{variable}} in {{{from}}..{{to}}..{{step}}}; do {{echo "Loop is executed"}}; done

for {{variable}} in */; do (cd "${{variable}}" || continue; {{echo "Loop is executed"}}) done

可以 sudo apt install tldr 然後 tldr for 獲得更多幫助。

while

while {{condition_command}}; do {{command}}; done

寫在同一行的時候需要分號。do 前面換行可以去掉 do 前分號。done 前換行可以去掉 done 前分號。for 同理。

bash 應用

time

作為內建命令時,time 後面直接加一條命令可以測量其執行時間。

you@localhost:~/test$ time ./main
1 2
3

real    0m0.383s
user    0m0.002s
sys     0m0.000s

作為一個可執行檔案 /usr/bin/time(等價於 /bin/time)時,也是同樣用法,但是輸出不同:

you@localhost:~/test$ /bin/time ./main
1 2
3
0.00user 0.00system 0:02.34elapsed 0%CPU (0avgtext+0avgdata 3576maxresident)k
0inputs+0outputs (0major+135minor)pagefaults 0swaps

只需要知道評測時看的是 user time。測量空間時,使用 /bin/time -v

you@localhost:~/test$ /bin/time -v ./main
1 2
3
        Command being timed: "./main"
        User time (seconds): 0.00
        System time (seconds): 0.00
        Percent of CPU this job got: 0%
        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.07
        Average shared text size (kbytes): 0
        Average unshared data size (kbytes): 0
        Average stack size (kbytes): 0
        Average total size (kbytes): 0
        Maximum resident set size (kbytes): 3424
        Average resident set size (kbytes): 0
        Major (requiring I/O) page faults: 0
        Minor (reclaiming a frame) page faults: 134
        Voluntary context switches: 2
        Involuntary context switches: 0
        Swaps: 0
        File system inputs: 0
        File system outputs: 0
        Socket messages sent: 0
        Socket messages received: 0
        Signals delivered: 0
        Page size (bytes): 4096
        Exit status: 0

Maximum resident set size (kbytes): 3424 是所測量的。

ulimit

修改終端的資源限制,其中最有用的是修改棧空間:ulimit -s unlimited 表示無線棧空間。unlimited 可以改為任意一個 KB 為單位的數字,如 ulimit -s 1048576 使得棧空間為 1G。

批次過樣例

想象你正在參加一場 OI 比賽,本題的英文名為 station,所以你編寫了 station.cpp 作為提交的程式碼,現在想要測試之。已經去掉了 freopen。

可以編寫 test.sh

#!/bin/bash
ln -s ../../down/station/* .
g++ station.cpp -o station -std=c++14 -O2
for i in {1..2}; do
	time ./station <station$i.in | diff - station$i.out -Bsbq
done

第一行稱為 shebang,指定使用 bash 解析這個檔案。

第二行將所有樣例檔案連結到當前目錄,可以搜尋一下什麼是軟連結。這裡將存放在 ../../down/station/ 下形如 station1.in/out 一共兩祖樣例連結到當前目錄。

第四行是 for 迴圈,\(i\)\(1\)\(2\)

第五行,首先 time ./station <station$i.in 執行 station,將輸出透過管道傳遞給 diff,與答案檔案進行比較。對於 diff,一共傳遞了四個選項:-b 選項意在忽略行末空格,-B 忽略文末換行(實際上 -Bb 忽略了更多東西,diff --help|grep '\-b' 檢視),-s 選項指出在檔案相同時輸出 ... are identical-q 選項指出在檔案不同時僅輸出 ... differ。本題輸出檔案很大,如果輸出量少,可以去除 -q 選項觀察不同之處。diff 還拿到了兩個檔案,檔案 - 是標準輸入,從管道中獲得;檔案 station$i.out 是樣例輸出。這裡 station$i.out 實際上是個 format string,將 $i 替換成一個數字,寫成 ${i} 也對,這裡變數名只有一個字母,也可以寫 $i

第六行結束 for 迴圈。現在你就寫了一個 OI 中可以用的工具指令碼。

寫完以後給他新增執行許可權:chmod u+x test.sh。然後 ./test.sh 執行。

或者可以去掉第一行的 shebang,使用 bash test.sh 執行,或者 . test.shsource test.sh。保留第一行 shebang,make test(此時目錄下不要有名為 test 的檔案,包括程式碼),接著 ./test。首選 chmod

執行效果:

$ ls ../../down/station/
station1.in  station1.out  station2.in  station2.out
$ vim test.sh
$ chmod u+x ./test.sh
$ ./test.sh
station.cpp: In function ‘void work(int, int, int, mint)’:
station.cpp:153:10: warning: structured bindings only available with ‘-std=c++17’ or ‘-std=gnu++17’
  153 |     auto [u, w] = q.front();
      |          ^
Files - and station1.out are identical

real    0m0.016s
user    0m0.001s
sys     0m0.012s
Files - and station2.out are identical

real    0m0.024s
user    0m0.002s
sys     0m0.017s

花絮:freopen 開關

你用這個 test.sh,需要去掉 freopen。OI 比賽怎麼能隨意去掉 freopen 呢?考慮複製一下檔案:

#!/bin/bash
g++ station.cpp -o station -std=c++14 -O2 
for i in {1..2}; do
  cp station$i.in station.in
  time ./station
  diff station.out station$i.out -Bsbq
done

我常用的還有另一種方法是將 freopen 寫成這樣:

#ifndef NF
  freopen("station.in", "r", stdin);
  freopen("station.out", "w", stdout);
#endif

然後在 g++ 這一行的引數(編譯選項)中加入 -DNF 表示 define 名為 NF 的 macro,以去掉 freopen。之所以名字是 NF,因為 do not finish 的縮寫是 DNF。

對拍器

當前我們有兩份程式碼 main.cppbf.cpp,前者因未知原因掛了,後者是樸素程式。寫了一個 dt.py 用於生成一組資料,輸出到標準輸出(可以用 c++ 寫 data.cpp 編譯為可執行檔案,這裡是用 python 寫的)。現在可以進行對拍:

#!/bin/bash
cnt=0
g++ main.cpp -o main -O2 -DNF -g
g++ bf.cpp -o bf -O2 -DNF -g
while :; do
  echo Testcase $((++cnt)) is running...
  ./dt.py > in
  ./bf <in >ans
  ./main <in >out
  if diff out ans -Bbq; then :
  else echo WA; break; fi
done

這裡,: 是一個無效果的命令,始終返回表示”成功執行“的 0。while :; 就是無線迴圈。

\(cnt\) 指示了當前是第幾組資料,應該以 $((++cnt)) 的方法使它自增,小心其它方法可能使得 \(cnt\) 變成字串,可以自己嘗試。

第 10 行 if diff out ans -Bbq; then : 這裡比較兩個輸出檔案,相同時返回 0,此時沒有操作。否則,輸出 WA,並中斷迴圈。此時可以檢視輸入檔案 in,輸出檔案 out,答案檔案 ans。去掉 diff 的 -s 是個人喜好,看著加。

備份工具

有時候可能有儲存這場比賽所有程式碼的需求。這樣可以將暴力程式碼留作備份並刪掉,而且檢查程式碼是否編譯透過,還有方便最終提交,最重要的是防止誤刪(曾經試過將一份暴力程式碼 copy 成正解程式碼,導致正解程式碼被覆蓋,無法找回)。

可以編寫 backup.sh

#!/bin/bash
tim=$(date|awk -F " " '{print $4}')
dst=$(dirname $0)/tmp/${tim//:/-}
echo start backup at ${tim//:/-}
mkdir $dst
for p in seq butterfly hoshi; do
  src=$(dirname $0)/$p/$p.cpp
  if [ -e $src ]; then
    cp $src $dst
    if g++ $src -o $dst/main -O2 -std=c++14; then
      echo Problem $p: ok \(remember to check freopen via warnings\)
    else
      echo Problem $p: CE
    fi
  else
    echo Problem $p: not found
  fi
done
rm -f $dst/main

這個有一點長,會有一點難記。可以轉寫成 python 版本,會比較好寫,就是字多一點。下面是逐行解釋:

第一行 shebang。

第二行,提取了當前的時間,data 返回 Thu May 9 21:21:34 CST 2024 這樣一個東西,這東西當然不能直接用作檔名。所以使用 awk -F 分割了這個字串,取出第 4 項(0-indexed),寫法可參考或百度。這時 tim=21:21:34

第三行,dst 是 destination,指出備份程式碼存放在 dst 中。$0 是指令碼自己所在的路徑,dirname 取出這個路徑的資料夾名。${tim//:/-} 這裡,因為不能以冒號做檔名,將冒號替換成連字元,和 vim 替換比較像,注意 tim 後面兩個正斜槓表示全部替換。dst 這時就是 ./tmp/21-21-34,程式碼存放到這個路徑下。

第四行確認一下 tim 是對的,因為有些系統的時間取出來可能是第 3 項、第 5 項,因為中文的原因。自己列舉一下。

第五行新建資料夾。

第六行遍歷 p 為題目名稱,這次比賽有三道題,分別是 seq、butterfly、hoshi。

第七行找出將要備份程式碼的路徑,記為 src,是 source 的縮寫。

第八行,[ -e $src ] 判斷檔案 $src 是否存在,注意兩邊空格不能扔掉。if 判斷之。

第九行,檔案存在,複製到 dst。

第十行,進行編譯測試,只保留 -O2 -std=c++14 這兩個最基本的。

第十一行,編譯透過,輸出資訊。這時如果有 freopen 存在,會對 freopen 報”無返回值“的警告,觀察你的 freopen 對不對。如果沒有就慘了。如果想進一步檢查,可以使用 grep:grep $p $src --color=auto,看他找不找的到。找不到會無輸出或者只輸出一個,此時你檔名寫錯;找到了有刺眼紅色提醒。對於這個 freopen 檢查,因為傳引數有很多雙引號在,其實是很不好寫的,建議別寫。如果檔名寫對了是很整齊的。

第十三行,發現編譯錯誤,及時提醒。

第十六行,未找到檔案,輸出未找到。

第十九行,清理剛才編譯的可執行檔案。

然後和 test.sh 一樣賦予它執行許可權,然後就可以執行了。

考場上 .vimrc 和 backup.sh 兩個寫完需要 10~20 分鐘,取決於手速,如果發現寫的慢了去讀一下題緩解緊張。

makefile

make 是一個古老的構建工具,幫助我們構建專案。在 OI 中可以幫我們編譯,做到一個一鍵編譯的效果,既有靈活性又少打很多字元。

基礎語法

以下內容都寫在當前目錄下名為 makefile 的檔案中。

目標檔名: 依賴檔案1 依賴檔案2
	命令

注意命令前面是一個 TAB。

如:

bf: bf.cpp
	g++ bf.cpp -o bf -O2 -DNF -g
main: snow.cpp
	g++ snow.cpp -o main -O2 -g -fsanitize=undefined,address -DNF
snow: snow.cpp
	g++ snow.cpp -o snow -O2 -g -std=c++14 -DNF -Wall -Wextra -Wconversion

終端輸入 make snow 不是造雪,而是編譯可執行檔案 ./snow,可以 make snow && ./snow 一個組合技。make bf 也是,make main 也是,mainsnow 的區別是除錯選項的力度。除錯時使用 main 版本,測樣例使用 snow 版本。優勢在於檔案沒有修改時,不會觸發重新編譯,節省時間。

關於變數:makefile 的變數在每個目標外面,大概這樣寫:

SRC = snow.cpp bf.cpp

和 bash 變數不一樣的是等號兩邊可以加空格,訪問變數是 $(SRC) 圓括號,轉義美元符號是 $$。這裡面 string 的雙引號規則和 bash 一樣(不用寫)。這個變數其實因為開在全域性,沒得修改,是個常量,所以應該大寫。

$() 圓括號裡面可以呼叫 makefile 的函式,呼叫 bash 函式是 $(shell ls) 這樣子。

傳統題寫法

main: seq.cpp
	g++ seq.cpp -o main -DNF -g -O2 -DLOCAL -fsanitize=undefined,address
%: %.cpp
	g++ $< -o $@ -DNF -g -std=c++14 -Wall -Wextra -Wconversion -Wshadow

main 就是除錯力度很大的版本,並且 make 呼叫如果沒有任務引數,預設呼叫第一個,可以寫 make && ./main 編譯並執行,make && gdb main -q 編譯並進入除錯狀態。

%.cpp 是一個萬用字元,將 %.cpp 編譯成去掉 .cpp 的可執行檔案,每個匹配到的檔案都是分開的規則。$< 表示第一個依賴檔案(也只有一個),$@ 表示目標檔名。然後後面跟著一大堆引數就是編譯警告拉到很滿,可以參考一下,輸出一車警告的,可以仔細看,不看也沒關係。反正真正的編譯檢測是 backup.sh 做的。

這樣以後,對拍器第三、四行可以寫 make -j main bf-j 表示兩個編譯任務並行,會快),test.sh 編譯部分改成 make station,少寫很多的。每個題的 makefile 都不一樣,需要複製幾次。

互動題寫法

main: stub.o toxic.o
	g++ $^ -o $@ -fsanitize=undefined,address
%.o: %.cpp toxic.h
	g++ $< -o $@ -I. -c -DNF -O2 -g -DLOCAL

這一題提交程式碼是 toxic.cpp,互動庫是 grader 互動庫 stub.cpp,有共同標頭檔案 toxic.h。首先用 g++ -c 將檔案編譯為 .o 但是不連結,引數 -I. 指出標頭檔案在當前目錄(也可以是別的自己寫),後面是常規的編譯選項。構建 main 需要 toxic.ostub.o,將他們最終連結輸出成可執行檔案。$^ 是所有依賴檔案。注意 -fsanitize=undefined,address 寫在連結這一步。

另記:不應該把互動 grader 寫在標頭檔案裡……另外互動題最好把自己的全域性變數扔到 namespace 裡面,防止對面互動庫神操作撞名。

打掃檔案

希望 make clean 刪掉垃圾檔案,包括可執行檔案等。這樣寫:

.PHONY: clean
clean:
	rm -f main $(patsubst %.cpp, %, $(wildcard *.cpp))
	rm -f $(patsubst %.cpp, %.o, $(wildcard *.cpp))

wildcard *.cpp 是萬用字元匹配,找到所有 .cpp 檔案。patsubst 是字串替換,將 %.cpp 替換成 %,也就是所有可能的可執行檔名。還有一個 main 也一起刪掉。如果是互動題生成了 .o 也刪掉。

還可以加這四條,把輸入輸出資料刪了(資料剛才說是是軟連結的,所以真實的資料不會有事):

.PHONY: clean
clean:
	rm -f main $(patsubst %.cpp, %, $(wildcard *.cpp))
	rm -f $(patsubst %.cpp, %.o, $(wildcard *.cpp))
	-find . -name '*~'|xargs rm -rf
	-find . -name '*in'|xargs rm -rf
	-find . -name '*out'|xargs rm -rf
	-find . -name '*ans'|xargs rm -rf

.PHONY: clean 表明 clean 是個偽目標,即我們不是真的要生成名為 clean 的檔案。還有另一個常見偽目標 ALL,這個才是 make 無任務引數呼叫的,預設是第一個而已。

前面有 - 號表示發生錯誤時(如找不到檔案)忽略繼續。rm -f 也會幹這樣的事。

但是在賽場上你是不會想到刪檔案的,所以不用背。

結合 vim

vim 的命令模式也有 :mak[e] 命令(中括號表示可以省略),就是呼叫外面的 make,關鍵是如果編譯錯誤,vim 會將游標定位到第一個出錯的地方,可以 :cn[ext] 到下一個地方。不知道考場上能不能用。

相關文章