OI 中的小技巧(工具)
目录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=1
中 a
是变量名,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.sh
,source 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.cpp
与 bf.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
也是,main
和 snow
的区别是调试选项的力度。调试时使用 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.o
与 stub.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]
到下一个地方。不知道考场上能不能用。