NOIP / ACM 选手必知的编程技巧及常用C++头文件模板

前言

      平时做题或者比赛掌握一些有用的技巧会方便我们调试,使程序可读性更强,另外模板也比较省时,所以我就从网上整理了一下。

重载运算符

重载运算符是通过对运算符的重新定义,使得其支持特定数据类型的运算操作。

有的时候,我们构造了一种新的数据类型(高精度数,矩阵),当然可以将数据类型的运算操作用函数的形式写出来。但采用重载运算符的方式,可以让程序更为自然。

当然,重载运算符在语法上也有一些要求:

重载运算符的一般形式为返回值类型 operator 运算符(参数,…)

在结构体内部重载运算符时,括号内的参数会少一个(事实上,另外一个参数是this指针,即指向当前参数的指针(这个不重要啦,你只需要知道就好)),也就是说,两元运算符只需要1个参数。(在结构体外部定义时,两元运算符还是需要2个参数)

其他要求就和普通函数的要求差不多了。
 

#include <stdio.h>
struct pair_num//一个二元组类型
{
     int x,y;
     pair_num operator +(pair_num a)const //不加const会CE
     {
          pair_num res;
          res.x=x+a.x;//x事实上是this.x
          res.y=y+a.y;
          return res;
     }
     pair_num operator -(pair_num a)const
     {
          pair_num res;
          res.x=x-a.x;
          res.y=y-a.y;
          return res;
     }
     bool operator <(pair_num a)const //sort,set等数据结构需要使用小于号
     {
          return x<a.x||(x==a.x&&y<a.y);
     }
}a,b,res;
pair_num operator *(pair_num a,pair_num b)//在结构体外部定义时,不要加const
{
     pair_num res;
     res.x=a.x*b.x;
     res.y=a.y*b.y;
     return res;
}
int main()
{
     scanf("%d%d",&a.x,&a.y);
     scanf("%d%d",&b.x,&b.y);
     res=a+b;
     printf("%d %d\n",res.x,res.y);
     res=a-b;
     printf("%d %d\n",res.x,res.y);
     res=a*b;
     printf("%d %d\n",res.x,res.y);
     return 0;
}

namespace的用法

在C++程序中,我们往往会加上using namespace std;这一行语句,那么它是干嘛的呢?

事实上,C++引入了一个叫做名字空间(namespace)的机制。引入这个机制的目的,是为了解决程序中变量/函数名重复的问题。

举个栗子:我写了一个solve()函数,另外一个人也写了一个solve()函数,如果把这两个函数放在一个程序里面,就会出现函数重名的问题。这时,我可以把我的solve()函数放在namespace code1中,把另外一个人的solve()放在namespace code2当中,然后,通过code1::solve();语句就可以调用我写的solve()函数,code2::solve()可以调用另外一个人的solve()函数。

写成代码大概是这样的:
 

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
long long n,m,a[100005],p[100005],aw[100005],atk[100005];
namespace one_game
{
     //其实namespace里也可以声明变量
     //这个例子还没有体现出namespace的强大
     //不过我们几乎不写工程代码,所以也无所谓了,这只是个例子
     void solve()
     {
      for(int y=0;;y++)
           if((a[1]+p[1]*y)%atk[1]==0)
           {
                cout<<(a[1]+p[1]*y)/atk[1]<<endl;
                return;
           }
     }
}
namespace p_1
{
     void solve()
     {
          if(atk[1]==1)//solve 1-2
          {
               sort(a+1,a+n+1);
               cout<<a[n]<<endl;
               return;
          }
          else if(m==1)//solve 3-4
          {
               long long k=atk[1],kt=ceil(a[1]*1.0/k);
               for(int i=2;i<=n;i++)
                    k=aw[i-1],kt=max(kt,(long long)ceil(a[i]*1.0/k));
               cout<<k<<endl;
          }
     }
}
int main()
{
     int T;
     cin>>T;
     while(T--)
     {
          memset(a,0,sizeof(a));
          memset(p,0,sizeof(p));
          memset(aw,0,sizeof(aw));
          memset(atk,0,sizeof(atk));
          cin>>n>>m;
          for(int i=1;i<=n;i++)
           cin>>a[i];
          for(int i=1;i<=n;i++)
               cin>>p[i];
          for(int i=1;i<=n;i++)
           cin>>aw[i];
          for(int i=1;i<=m;i++)
           cin>>atk[i];
          if(n==1&&m==1)one_game::solve();//solve 8-13
          else if(p[1]==1)p_1::solve();//solve 1-4 or 14-15
          else cout<<-1<<endl;
     }
     return 0;
}

事实上,C++标准库把所有的函数都放在了namespace std当中,所以我们调用这些函数的时候,应该采用std::xxx的形式来调用标准库中的函数。当然,在加入using namespace std;一行后,就可以省略std::了,我们就可以偷懒了。(其实using namespace std;在工程上不推荐使用,但各位OIer们平时使用也没有什么问题)

当然,我们在比赛的时候,可以通过使用namespace来使我们的程序结构更加规整。比较常见的用法就是针对每个子任务(诸如上面的屠龙勇士),各写一个namespace,在其中定义我们解决该子任务所需要的变量与函数,这样各个子任务间互不干扰,方便了我们这些蒟蒻们骗分,也降低了正解爆炸的分数损失。


善用标识符进行调试

我们在本地测试的时候,往往要加入一些调试语句。要提交到OJ的时候,就要把他们全部删除,有些麻烦。

我们可以通过定义标识符的方式来进行本地调试。

大致的程序框架是这样的:
 

#define DEBUG
#ifdef DEBUG
 //do something
#endif
// or
#ifndef DEBUG
 //do something
#endif

#ifdef会检查程序中是否有通过#define定义的对应标识符,如果有定义,就会执行下面的内容,#ifndef恰恰相反,会在没有定义相应标识符的情况下执行后面的语句。

我们提交程序的时候,只需要将#define DEBUG一行注释掉即可。

当然,我们也可以不在程序中定义标识符,而是通过-DDEBUG的编译选项在编译的时候定义DEBUG标识符。这样就可以在提交的时候不用修改程序了。


对拍

有的时候我们写了一份代码,但是不知道它是不是正确的。这时候就可以用对拍的方法来进行检验或调试。

什么是对拍呢?具体而言,就是通过对比两个程序的输出来检验程序的正确性。你可以将自己程序的输出与其他程序(打的暴力或者其他dalao的标程)的输出进行对比,从而判断自己的程序是否正确。

当然,我们不能自己比对两段程序的输出,所以我们需要通过批处理的方法来实现对拍的自动化。

具体而言,我们需要一个数据生成器,两个要进行对拍的程序。

每次运行一次数据生成器,将生成的数据写入输入文件,通过重定向的方法使两个程序读入数据,并将输出写入指定文件,利用Windows下的fc命令比对文件(Linux下为diff命令),从而检验程序的正确性。

如果发现程序出错,可以直接利用刚刚生成的数据进行调试啦。

对拍程序的大致框架如下:
 

#include <stdio.h>
#include <stdlib.h>
int main()
{
 //For Windows
 //对拍时不开文件输入输出
 //当然,这段程序也可以改写成批处理的形式
 while(1)
 {
  system("gen > test.in");//数据生成器将生成数据写入输入文件
  system("test1.exe < test.in > a.out")//获取程序1输出
  system("test2.exe < test.in > b.out")//获取程序2输出
  if(system("fc a.out b.out"))
  {
   //该行语句比对输入输出
   //fc返回0时表示输出一致,否则表示有不同处
   system("pause")//方便查看不同处
   return 0;
   //该输入数据已经存放在test.in文件中,可以直接利用进行调试
  }
 }
}



写数据生成器的几个小提示



在使用rand()前,别忘了调用srand(time(NULL))来重置随机数种子。(不重置的话,每次调用rand()只会得到一套随机数)

rand()的生成随机数范围在Windows下为 [0,32767] ,在Linux下为 [0,2^{31}-1] ,所以如果数据过大,最好手写一个随机数生成器。

小技巧:

打的代码最好不要删除,用//注释掉是一个更好的主意,说不定以后要用。

输出64位整数时请记得使用%lld!(当然也可以使用cin/cout来解决这个问题,别忘了加上ios::sync_with_stdio(false);防止IO耗时过久)

别忘了算内存,有可能你不小心多输了一个0就MLE了。(一般情况下,256M内存的话,int数组最多可以开到 5 * 10^7 )

模板

#include<bits/stdc++.h>
#define lson l,m,rt<<1  
#define rson m+1,r,rt<<1 | 1  
#define ffr(i,x,y) for(int i=(x),_en=(y);i<=_en;i++)  
#define rff(i,x,y) for(int i=(x),_en=(y);i>=_en;i--)  
#define clr(f,z) memset(f,z,sizeof(f)) 
using namespace std;
typedef double db;
typedef long long ll;
typedef long double ld;
typedef vector<int> vi;
typedef pair<int,int> pa;
typedef unsigned int uint;
typedef unsigned long long ull;
const int maxn=1005,inf=1<<29,mod=1000000007;
const double pi=acos(-1.0);
bool sf(int &x){return scanf("%d",&x)==1;}
bool sf(ll &x){return scanf("%I64d",&x)==1;}
bool sf(double &x){return scanf("%lf",&x)==1;}
bool sf(char &x){return scanf("%c",&x)==1;}
bool sf(char *x){return scanf("%s",x)==1;}
bool sf(string &x){return cin>>x;}
template<class T>inline void sf2(T&num){
    num=0;T f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    num*=f;
}
void pf(int x,int op){op==0?printf("%d",x):printf("%d%c",x,op==1?' ':'\n');}
void pf(ll x,int op){op==0?printf("%I64d",x):printf("%I64d%c",x,op==1?' ':'\n');}
void pf(char x,int op){op==0?printf("%c",x):printf("%c%c",x,op==1?' ':'\n');}
void pf(char *x,int op){op==0?printf("%s",x):printf("%s%c",x,op==1?' ':'\n');}
void pf(string x,int op){op==0?cout<<x:cout<<x<<(op==1?' ':'\n');}
inline int gcd(int x,int y){if(!x)return y;return gcd(y%x,x);}
inline int power(int x,int k,int p){int res=1;for(;k;k>>=1,x=(ll)x*x%p)if(k&1)res=(ll)res*x%p;return res;}
int n,m,k;
int dp[maxn][maxn],a[maxn][maxn];
int dir[][2]={{0,1},{-1,0},{0,-1},{1,0},{-1,1},{-1,-1},{1,-1},{1,1}};
int main()
{
	
	return 0;
}

解释
对于代码段:

#include<bits/stdc++.h>


bits/stdc++.h这个头文件,号称是包含所有C++头文件的头文件,只能说666666,不过有些OJ可能不支持哦,正规比赛最好老老实实地一个个头文件敲吧。

对于代码段:

#define lson l,m,rt<<1  
#define rson m+1,r,rt<<1 | 1 


这两个宏定义是为了计算树里面左右儿子节点。在线段树里用的比较多,比如当前节点维护的是[l,r],令m=(l+r)/2,那么左儿子维护的是[l,m],右儿子维护[m+1,r]。

对于代码段:

#define ffr(i,x,y) for(int i=(x),_en=(y);i<=_en;i++)  
#define rff(i,x,y) for(int i=(x),_en=(y);i>=_en;i--)  
#define clr(f,z) memset(f,z,sizeof(f)) 


有木有觉得写for循环很累,从现在起,比如你要写for(int i=0;i<=10;i++) 只需要写成ffr(i,0,10)就可以了,是不是省了不少力?

rff刚好是个倒序循环。

对于ACM,经常要多组输入,那很多时候,变量啊,数组啊就要清空初始化,那就要用memset,代码贼长,现在,有了这个宏定义,比如要把数组a(不管几维都可以)清空,那就可以clr(a,0)或者clr(a,-1),贼方便,同时要注意,z的值只能是0或者-1哦,这是因为memset这个函数只能把数组每个元素搞成0或者-1,因为0的二进制补码表示是全0,-1的二进制补码表示是全1,重置起来方便嘛。那如果你要重置成某个特定的别的值,就要么就自己循环重置,要么就用C++里的fill函数。

代码段:

typedef double db;
typedef long long ll;
typedef long double ld;
typedef vector<int> vi;
typedef pair<int,int> pa;
typedef unsigned int uint;
typedef unsigned long long ull;


就是定义各种类型的缩写,有些类型太长了,写起来都不太愿意,而且DEV又没有什么代码自动补全。

比较常用的就是long long,比如之前定义这个类型要long long a=1;现在只要ll a=1;就行了,贼方便。接下来看代码段:

const int maxn=1005,inf=1<<29,mod=1000000007;
const double pi=acos(-1.0);


定义了一些常用的常量,比如用来表示数组的最大值的maxn,无穷大inf,还有表示模的mod,以及π的精确表示pi。

接下来的代码段:

bool sf(int &x){return scanf("%d",&x)==1;}
bool sf(ll &x){return scanf("%I64d",&x)==1;}
bool sf(double &x){return scanf("%lf",&x)==1;}
bool sf(char &x){return scanf("%c",&x)==1;}
bool sf(char *x){return scanf("%s",x)==1;}
bool sf(string &x){return cin>>x;}


利用函数重载把常见的一些数据类型进行了读取。函数传的是引用,返回值代表有没有数据读入,这在多组输入中有用。

然后接下来的代码段:

template<class T>inline void sf2(T&num){
    num=0;T f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar();
    num*=f;
}


这是个读入函数,优点是比前面的读入快,特别是输入数据量比较大的时候就能体现出它的价值了,可以叫它“读入外挂”。利用模板template来接受任何类型的参数,当然函数内的实现决定了,这只对整数有用。

接下来的代码段:

void pf(int x,int op){op==0?printf("%d",x):printf("%d%c",x,op==1?' ':'\n');}
void pf(ll x,int op){op==0?printf("%I64d",x):printf("%I64d%c",x,op==1?' ':'\n');}
void pf(char x,int op){op==0?printf("%c",x):printf("%c%c",x,op==1?' ':'\n');}
void pf(char *x,int op){op==0?printf("%s",x):printf("%s%c",x,op==1?' ':'\n');}
void pf(string x,int op){op==0?cout<<x:cout<<x<<(op==1?' ':'\n');}


利用函数重载实现各种各样的常用输出。

op是控制输出变量后面跟的是什么,0代表什么都不跟,1代表跟空格,2代表跟换行。

接下来的代码段:

inline int gcd(int x,int y){if(!x)return y;return gcd(y%x,x);}
inline int power(int x,int k,int p){int res=1;for(;k;k>>=1,x=(ll)x*x%p)if(k&1)res=(ll)res*x%p;return res;}


gcd函数用来求两个数的最大公约数,power用来求快速幂x^k%p。这两个函数在数论中经常用到。

接下来的代码段:

int n,m,k;
int dp[maxn][maxn],a[maxn][maxn];
int dir[][2]={{0,1},{-1,0},{0,-1},{1,0},{-1,1},{-1,-1},{1,-1},{1,1}};


n,m,k是常用的变量,dp,a是常用数组,dir是常用方向数组。在搜索里面用得比较多,用来向相邻的八个方向扩散。
 

来源

洛谷日报#86

你迎哥哥 https://blog.csdn.net/u013615904/article/details/80310016 

猜你喜欢

转载自blog.csdn.net/qq_41785863/article/details/84232502