本篇代码严格遵守一致的代码风格。我们也建议您在算法练习中,养成良好的书写习惯。风格清楚的代码就像写得一手好字,使人赏心悦目,大大提高了易读性。尽管在编译器看来别无两样,代码同时承担着程序员间沟通的使命,就像本篇不遗余力地使用大量代码一样,它们能最客观、最清晰地表达程序。
笔者代码风格参考 Google 风格,根据个人习惯和竞赛实际需要有一定调整。
Google 风格参考: C++ 风格指南 - 内容目录
7.3.1 命名规范
竞赛中,程序编写速度固然重要;但真正需要时间的是思考。优秀的变量声明简短,又具有一定的含义。
我们对于常见算法中的变量,有习惯上的命名,能够充分体现含义,详见本篇各段代码。如无特殊情况,应遵守这些约定俗成的命名。(如,常数 N 表示数据最大范围)
对于题目特定的变量,可直接用题目给定的命名,或是自己习惯的命名。可以在声明时,在一旁注释上变量的含义,以免混淆。
// 摘自本篇 最短路 SPFA 一节struct Edge {int y, w; // 终点,边权Edge() {}Edge(int a, int b): y(a), w(b) {}} e;
对于函数的命名,推荐使用 动词 [+ 宾语] 结构:
int getSum() {} // 推荐
bool check() {} // 推荐
int sum() {} // 不推荐,易和变量名混淆
7.3.2 在语句间添加空格
看下面两段代码:
for (int len=2; len<=n; ++len) {
for (int i=1; i+len-1<=n; ++i) {
int j = i + len - 1;
dp[i][j] = 1 << 29;
for (int k=i; k<j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + pre[j] - pre[i - 1]);
}
}
}
for(int len=2;len<=n;++len){
for(int i=1;i+len-1<=n;++i){
int j=i+len-1;
dp[i][j]=1<<29;
for(int k=i;k<j;++k){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+pre[j]-pre[i-1]);
}
}
}
一个好消息是,你不会在本篇中看到第二段没有空格的代码。网上能搜到的很多「野代码」甚至没能做好代码缩进等格式,这些都严重阻碍了代码的阅读。
一般地,我们建议:
- 在双目运算符两侧添加空格(
x = y;) - 在 for、while 等关键字后添加空格,以和函数区别
- 单目运算符与变量间不应添加空格(
++x,!flag) - 在结构比较紧凑的局部,可以不满足 1. (
for (int i=0; i<n; ++i),dp[i-1][j-1] + dp[i][k])
不过,代码格式规范并没有标准答案,你可以根据自己团队的情况调整。重要的是能够便于阅读即可。
7.3.3 为代码添加注释
在赛场上,写上一两句注释就像在草纸上演算,有助于理清思路,避免遗漏步骤。也可以用来标记暂时跳过的步骤。
// 摘自本篇 最短路 Floyd 算法 一节
// init
memset(g, 0x3f, sizeof g);
for (int i=0; i<n; ++i) {
g[i][i] = 0;
}
// Read
for (int i=0; i<m; ++i) {
int x, y, w;
cin >> x >> y >> w;
g[x][y] = min(g[x][y], w); // 防止重边
}
// Floyd
for (int k=0; k<n; ++k) {
for (int i=0; i<n; ++i) {
for (int j=0; j<n; ++j) {
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
}
}
在练习中,我们建议您整理收录做过的题目,代码的注释可以帮助回想题目的解法和实现的细节,是笔记不可或缺的部分。
然而,笔者建议应该避免的是完全把代码当成笔记,在注释里写小作文、做阅读理解。一条比较感性的理由是,这样极度损害了代码本身的美观度。这样做大大改变了代码本身的排版结构,代码长度不能得到直观反映,也不利于对代码二次修改。
事实上,注释不是越多越好。它应当简明扼要,在对应的位置做出必要的解释。大段的文字应当放在笔记的文字区域集中进行论述。另外,要避免复述代码一眼能够看出含义的语句。
// 错误示范:复述代码
int x = 0; // 声明变量 x,初始化为 1
++x; // 令 x 自增 1
// 改正:说明必要的含义
int x = 0; // 当前位置
++x;
7.3.4 大括号与代码压行
阅读下面两段代码:
for (int i=0; i<n; ++i) {
for (int j=0; j<m; ++j) {
cin >> a[i][j];
}
}
for (int i=0; i<n; ++i)
for (int j=0; j<m; ++j)
cin >> a[i][j];
当控制语句只有一条语句紧跟时,可以不写大括号。
前一种方式写出了所有的大括号,这也是笔者采用的风格,它的好处是在循环中可以随时方便地加入新语句。在循环层次较多时,这种方法也能在视觉上争取更多的层次感,对于初学者也比较友好。
后一种方式,你也许经常在教科书上看到,或许会「不明觉厉」、或许会惊叹于它反人类的排版。实际上,他们也许只是为了省下油墨和纸张(这样明显行数更少)。但为何在竞赛中也有这种书写方式呢?你可以看到:
// 选自单调栈模板
while (!s.empty() && s.top() >= x) s.pop();
这种完全把语句压缩到一行的情况。
实际上,这是「代码压行」技术。竞赛中使用它的主要原因是,它几乎不影响阅读,但在一屏(一个屏幕内同时看到的范围)内可以展示更多的代码。本文也时常有使用压行的地方:
// 摘自本篇 二进制枚举
for (int j=0; j<n; ++j) {
if (i >> j & 1) { // 确认选中的情况
if (!head) cout << ' '; // 严谨输出
else head = false;
cout << a[j];
}
}
如果不使用压行:
for (int j=0; j<n; ++j) {
if (i >> j & 1) { // 确认选中的情况
if (!head) {
cout << ' '; // 严谨输出
} else {
head = false;
}
cout << a[j];
}
}
这种结构使得「严谨输出」这一并不是重点的部分太过抢眼(占到 6 行),整体结构变得松散,尤其是放到整篇代码中的时候。在笔者看来,代码的排版同样是一种艺术,这种美的感知需要长时间与代码打交道来培养。也正是这样,艺术不具有标准的答案,我们关于它的论述也不得不诉诸主观。
在很多模板中,往往压行技术应用较多,一部分原因是模板很少需要改动,另一部分原因就是减少不必要的行数,突出重点,减少屏幕滚动距离。在一屏内能浏览更多更重要的代码段,聚焦于解决更重要的问题。
我的意思是,合适的代码压行反而能提高代码的可读性与美感。
不过与此相反,一部分人认为认为「左大括号是否应换到新行」,那样做才能使得程序层次更加清晰易读。不论如何,怎样写代码没有对错,你可以决定你自己编写什么样的代码。
总而言之,对称是一种美;不对称也是一种美。只要你喜欢、那就随你喜欢。
7.3.5 写好 for 循环
这是对于初学者写好清楚代码非常重要的一点。for 循环是高级版的 while 循环;for 循环可以实现 while 循环的所有功能。使用 for 循环的情况,往往要循环特定次数;而使用 while 循环的情况,往往要满足特定条件时循环。
// 例:循环 n 次
for (int i=0; i<n; ++i); // 普通
for (int i=1; i<=n; ++i); // 下标从 1 开始
for (int i=n-1; i>0; --i); // 反向
// 例:栈非空时循环
while (!s.empty()) {
s.pop();
}
竞赛中,习惯上把「回答 t 组询问」解释为第二种情况,即「还有询问时循环」,写作:
while (t--) {
solve();
}
当然,你也可以把它理解为为不需要再用到 t 的值时的简便写法。
