跳转至

前言

约 1248 个字 170 行代码 预计阅读时间 6 分钟

鉴于多次看到学弟学妹各种的奇葩命名和较为繁琐的代码,2023.4.10动笔,2023.4.11完成。

程序设计竞赛的命名规范,与开发略有不同。首先最重要的点,是不要重名,下面抓一个cf橙名选手在ecf犯下的错误

//图片1(待上传)

其次是尽快的编码速度,也就是短命名。

其次是稍稍考虑可读性,至少队友之间能看得懂。

本文还介绍了短路,能有效降低代码阅读难度,最后对于编码风格做了一定的讨论。

命名规范

define

首先是个人观点,除了ifdef这种,以及dls的计算几何板子(它太好用了),能不用define就不用define

当然相当多的选手,甚至顶尖选手,用一大堆define加快编码速度,个人不苟同

define能完成的事,大多可以用其他语法代替,而且能有效避免重名问题

typedef

有些类型名太长,为加快编码速度,因此对类型重命名,使用typedef,例子如下

typedef long long ll;
typedef long double ld;
int main()
{
    ll a=114514;
    ld b=1.919180;
}

const

有些需要多次使用的数字,为加快编码速度,便于阅读,加快程序运行速度(指模数),以及避免打错,将其命名为常量,使用const,例子如下

const int N=2e5+3;
//表示n的最大值多一点点,例如重链剖分的众多数组开同样大的空间,和欧拉筛限定范围
//通常用变量的大写作为常量,表示该变量的数据范围,比如M表示m的最大值多一点
int son[N], siz[N], dep[N], fa[N],top[N], dfn[N];
void seive()
{
    int cnt=0;
    for(int i=2;i<N;++i)
    {
        if(!v[i])
            p[cnt++]=i;
        for(int j=0;j<cnt && i*p[j]<N;++j)
        {
            v[i*p[j]]=1;
            if(i%p[j]==0)
                break;
        }
    }
}
const double PI = acos(-1.0);
//圆周率pi,在计算几何中用到偏多
const int mod1 = 1e9 + 7;//常见模数1
const int mod2 = 998244353;//常见模数2
//modulus的缩写,表示模数,模数定义为常量和变量,二者运行速度差很多,所以必须用常量
const int INF = 0x3f3f3f3f;
//infinite的缩写,表示无穷大,相比于int最大值,满足无穷大加无穷大等于无穷大的需求
const double EPS = 1e-6;
//epsilon的缩写,表示无穷小,用来判断两个浮点数是否相等,因为计算机计算有误差
//
int sign(double x) { return x < -EPS ? -1 : x > EPS; }
//判断数符号,负数返回-1,0返回0,正数返回1
bool floatEqual(double x, double y) { return !sign(x - y); }
//

标识符命名

尽量用3-4个字母,是单词或者短句的缩写,下面给出一些例子

dis[N]//distance的缩写,表示图上点到起点的最短距离
vis[N]//visited的缩写,表示是否访问过该节点
fa[N]//father的缩写,在dfs中为避免走回头路,表示来时的路,或者树上的祖先
p[N]//prime的缩写,表示质数表
siz[N]//size的缩写,表示子树大小,或者其他的集合大小
hson[N]//heavy son的缩写,表示重链剖分的重儿子
dep[N]//depth的缩写,表示树上节点的深度
dfn[N]//dfs number的缩写,表示dfs时到达节点的先后顺序
lz[N]//lazy tag的缩写,表示线段树的懒标记
f[N]//union&find的缩写,表示并查集
C[N][N]//combination的缩写,表示组合数
fac[N]//factorial的缩写,表示阶乘
queue<int> q//queue的缩写,表示队列
priority_queue<int> pq//priority_queue的缩写,表示优先队列
namespace bit{}//binary indexed tree的缩写,表示树状数组或者二进制索引树
vector<int> e[N]//edge的缩写,表示邻接表
int head[N]//表示链式前向星的链头
cnt,tot,res,ans,id,tmp//count,total,result,answer,identify,temporary的缩写
dfs,bfs,kru,dij等各种算法//depth first search,breadth first search,kruskal,dijkstra的缩写
更多推荐使用的变量命名

变量作用域

局部变量

生效在函数的{}内,出花括号就结束生命周期,需要赋初值,否则为随机

占据的空间是在栈(stack)中一段连续的空间,栈的空间默认大小为2M或1M,较小

为避免重名,小变量能用局部变量的尽量用局部变量,函数如果用到局部变量用传参解决(能引用不传值)

当然,代码特别短的时候,小变量可以作为全局变量

全局变量

生效在所有地方,int类型初值为0,char类型初值为'\0',其他类型都有默认初值

占据的空间是在全局区(静态区)(static),全局区的空间为2G

所以大数组或者STL 容器一般作为全局变量

手动申请内存的变量

malloc和new出的空间,开在堆(heap)的一段不连续的空间,理论上是硬盘大小

不过在程序设计竞赛内,我只见过用指针写法的一些数据结构或者长链剖分,一般情况下不用指针

短路

概念:当已经知道整体表达式的值,不必再计算后面的表达式

在与运算 && 中,若顺序靠前的逻辑表达式为false,则不执行后面逻辑表达式

类似的,在或运算 || 中,若顺序靠前的逻辑表达式为true,则不执行后面逻辑表达式

类似的,在ifelse中,若顺序靠前的逻辑表达式为true,则不判断后面逻辑表达式(给定分数输出等级是一个例子)

if(score<60)
    //
else if(score>=60 && score<70)
    //
else if(score>=70 && score<90)
    //
else
    //
上述代码可以改写成下面这样
if(score<60)
    //
else if(score<70)
    //
else if(score<90)
    //
else
    //

还有一种短路是尽量使用continue,break,return这些

例如跳出多层循环,把循环放到函数中用return会更方便,下面是对比

int flag=0;
for(int i=0;i<n && !flag;++i)
{
    for(int j=0;j<n && !flag;++j)
    {
        for(int k=0;k<n && !flag;++k)
        {
            if(XXX)
                flag=1;
        }
    }
}
上述代码可以改写成下面这样
void calculateDP(int n)
{
    for(int i=0;i<n;++i)
    {
        for(int j=0;j<n;++j)
        {
            for(int k=0;k<n;++k)
            {
                if(XXX)
                    return;
            }
        }
    }
}
此外,对于分类讨论的问题,使用return也会更方便

if(XXX)
{
    cout<<"NO\n";
    continue;
}
/*
*/
cout<<"YES\n";
上述代码可以改写成下面这样
bool solve()
{
    if(XXX)
        return false;
    /*
    */
   return true;
}
cout<<(solve()?"YES":"NO")<<"\n";
如果要输出方案可以改成这样
if(solve())
{
    cout<<"YES\n";
    //
}
else
    cout<<"NO\n";

if嵌套,使用continue或者break会更简洁

for()
{
    if()
    {
        if()
        {
            if()
            {}
        }
    }
}
上述代码可以改写成下面这样
for()
{
    if()
        continue;
    if()
        continue;
    /*
    */
}

编码风格

本文不讨论换行缩进空格这些格式上的问题。

首先需要了解一下未定义行为(undefined behavior,简称ub),c/cpp语言标准未做规定的行为。编译器可能不会报错,但是这些行为编译器会自行处理,所以不同的编译器会出现不同的结果。

最著名的ub应该是下面这条

int a=1;
a=++a+a++;

一行代码只写一条语句

形如下面的代码极有可能是ub,所以不建议这么写,而是每条语句单独成行

a++,b+=a,c+=b;

命名空间

以jly为代表的选手,不使用using namespace std;

不用的话,可以用max,min这些原来会和std库重名的标识符了

不过个人还是喜欢用using namespace std;

标识符命名

直观,望文生义,最好用英文单词缩写,最好不用拼音

对于某些字体下难以区分的字符,要么换字体,要么不用这些字符命名,比如lL1|(分别是小写l,大写L,数字1,或运算|)和oO0(分别是小写o,大写O,数字0)

lambda

如果是一次性函数,可以用,如果是多次用的函数,建议不用

程序功能块划分

当一个函数行数太多时,一般建议拆分成多个函数,按照功能来拆分,使得每一块更好debug

个人习惯是,多测和输入输出都放在main函数,比如下面这样

int n,a[N];
int solve()
{

}
int main()
{
    int T;
    cin>>T;
    for(int kase=1;kase<=T;++kase)
    {
        cin>>n;
        for(int i=0;i<n;++i)
            cin>>a[i];
        cout<<solve()<<"\n";
    }
}