本篇代码严格遵守一致的代码风格。我们也建议您在算法练习中,养成良好的书写习惯。风格清楚的代码就像写得一手好字,使人赏心悦目,大大提高了易读性。尽管在编译器看来别无两样,代码同时承担着程序员间沟通的使命,就像本篇不遗余力地使用大量代码一样,它们能最客观、最清晰地表达程序。

笔者代码风格参考 Google 风格,根据个人习惯和竞赛实际需要有一定调整。

Google 风格参考: C++ 风格指南 - 内容目录

7.3.1 命名规范

竞赛中,程序编写速度固然重要;但真正需要时间的是思考。优秀的变量声明简短,又具有一定的含义。

我们对于常见算法中的变量,有习惯上的命名,能够充分体现含义,详见本篇各段代码。如无特殊情况,应遵守这些约定俗成的命名。(如,常数 N 表示数据最大范围)

对于题目特定的变量,可直接用题目给定的命名,或是自己习惯的命名。可以在声明时,在一旁注释上变量的含义,以免混淆。

  1. // 摘自本篇 最短路 SPFA 一节
  2. struct Edge {
  3. int y, w; // 终点,边权
  4. Edge() {}
  5. Edge(int a, int b): y(a), w(b) {}
  6. } 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]);
        }
    }
}

一个好消息是,你不会在本篇中看到第二段没有空格的代码。网上能搜到的很多「野代码」甚至没能做好代码缩进等格式,这些都严重阻碍了代码的阅读。

一般地,我们建议:

  1. 在双目运算符两侧添加空格(x = y;
  2. 在 for、while 等关键字后添加空格,以和函数区别
  3. 单目运算符与变量间不应添加空格(++x!flag
  4. 在结构比较紧凑的局部,可以不满足 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 的值时的简便写法。