算法竞赛专题解析(16):附A-测试数据的构造与对拍

本系列文章将于2021年整理出版,书名《算法竞赛专题解析》。
前驱教材:《算法竞赛入门到进阶》 清华大学出版社
网购:京东 当当      作者签名书

如有建议,请加QQ 群:567554289,或联系作者QQ:15512356

文章目录

  • A.0 随机数基础
  • A.1 构造负数和实数
  • A.2 构造极大范围内的随机数
  • A.3 去掉重复的随机数
  • A.4 例题
  • A.5 对拍
  • A.6 统计执行时间

  竞赛队员在OJ上做题时,提交到OJ的代码,OJ是如何判断是否正确的?OJ并不看代码,而是使用“黑盒测试”,用测试数据来验证。每一题,OJ都事先设定好很多组输入数据datain和输出数据dataout,参赛队员提交代码后,OJ执行代码,输入datain,产生输出,看是否和dataout完全一致。
  OJ所设定的测试数据,可能非常多而且复杂,队员不可能知道,更不能手工一个个进行验证,这时要自己构造测试数据,并且用两种代码进行对比测试。包括以下两个任务:
  (1)构造数据。包括输入和输出数据。如果输入情况复杂,不能方便手工构造,而要编一个小程序,随机生成。输出数据,则要通过下面的低效率代码来生成。
  (2)两种代码。用两种算法写代码进行对比:低效率代码、高效率代码。被称为“对拍”。
  低效率代码。一般用暴力法实现,代码简单、逻辑正确,它因为效率低无法通过OJ的时间限制,它的用途是做为基准(benchmark)代码,产生正确的输出数据。
  高效率代码,准备提交到OJ。把它产生的输出数据,与低效率代码产生的输出数据进行对比,如果一致,就认为是正确的,可以提交了;如果不一致,可以找到那些不一致的数据,用来修改程序。测试数据非常重要,往往能帮助编程者找到代码的错误所在。

A.0 随机数基础

  构造输入数据,需要用到随机数。
  传统的c++98只能产生一个较小的随机数;如果编译器支持c++11,那么可以产生一个很大的随机数,用起来简单得多。
1、c++98随机数
  与随机数相关的函数和变量有:
  srand(seed):产生以seed为种子的随机数列。对一个seed种子,产生的随机数列是固定的,所以是“伪随机”的。要产生“真”随机数列,就需要一个“随机”的seed,一般用当前时间做seed。
  time(0):返回当前时间,即从1970.1.1日零时到现在的秒数。
  rand():返回[0, RAND_MAX]内的一个随机正整数。
  RAND_MAX:一般有RAND_MAX = 32767。

2、c++11随机数
  用c++11的3个函数,能简单地获得大随机数。
  mt19937:随机数产生器1,返回一个unsigned int的32位无符号随机数。
  mt19937_64:返回一个64位无符号随机整数。
  uniform_int_distribution2:在指定区间内生成一个随机整数。

  下面是演示代码。

#include 
using namespace std;
int main(){   
    mt19937 u32Rnd(time(0));              //32位
    cout << "32: " << u32Rnd() << endl;   //打印一个无符号32位随机整数
    cout << "32: " << u32Rnd() << endl;   //再打印一个
                                          //如果用printf打印,注意输出的格式
    mt19937_64 u64Rnd(time(0));           //64位
    cout << "64: " << u64Rnd() << endl;   //打印一个无符号64位随机整数

    uniform_int_distribution<> distrib(-100, 100);    //设置随机数范围
    cout << "[32]: " << distrib(u32Rnd) << endl;      //打印[-100,100]内的一个随机数
    cout << "[64]: " << distrib(u64Rnd) << endl;      

    return 0;
}

  输出的例子:

32: 1575274362
32: 3712921037
64: 8709987447837980106
[32]: 67
[64]: -20

A.1 构造负数和实数

  c++98环境,需要[-n, n]内的随机数时,先产生一个[0, 2n]的正随机数,然后再减去n。
  c++11环境,直接用uniform_int_distribution设置范围。
  需要实数时,先产生一个大的随机正整数,然后除以10的幂。

A.2 构造极大范围内的随机数

  c++11可以直接产生大随机数。下面是c++98环境的用法。

  1. 生成[0, 1000000]内的随机数
      rand() 只能生成大小在[0, RAND_MAX]内的随机数,RAND_MAX = 32767。
      如何用rand()生成大随机数?有2个方法:
      (1) rand()*rand()
      这方法很简单,但是缺点是生成的几乎全是合数,很少有素数。
      (2)用组合的方法,把3个随机数组合起来,得到一个24位的随机数。
unsigned long ulrand(){      //生成一个大随机数
    return (
             (((unsigned long)rand()<<24)& 0xFF000000ul)
            |(((unsigned long)rand()<<12)& 0x00FFF000ul)
            |(((unsigned long)rand())    & 0x00000FFFul));
}
  1. 生成 [MIN, MAX]之间的一个随机数
const int MAX = 1000000;    //自己定义MAX和MIN 
const int MIN = 0;
int big_rand;               //大随机数
big_rand = ulrand() % (MAX - MIN + 1) + MIN;

A.3 去掉重复的随机数

  上面生成的随机数,很多是重复的。如何去掉100万个数中的重复数字?而且要求去重后不改变剩下数据的顺序。
  因为数太多,所以不可能用暴力法一个个对比。用unique()函数去重则会改变顺序。
  简单又快的方法是用hash去重:

int myhash[MAX];     //hash表
int mynum[MAX];      //记录去重后的随机数

  下面处理随机数big_rand,把重复的去掉。

if(myhash[big_rand] == 0) {   //第big_rand位置还没有
     myhash[big_rand]=1;      //随机数big_rand ,登记在myhash[]的第big_rand个位置
     mynum[num]= big_rand;    //记录随机数
     num++;                   //记录随机数的数量
}

A.4 例题

  用下面的例题演示如何构造大量的不重复的测试数据。


hdu 1425 sort
给你n个整数,请按从大到小的顺序输出其中前m大的数。
输入:每组测试数据有两行,第一行有两个数n, m(0 < n, m < 1000000),第二行包含n个各不相同,且都处于区间[-500000, 500000]的整数。
输出:对每组测试数据按从大到小的顺序输出前m大的数。


(1)c++98环境
  Hdu 1425的测试数据构造代码:

#include 
using namespace std;
const int MAX  = 1000000;    //100万个数
const int MIN  = 0;
int myhash[MAX]={0};         //用hash查重
int mynum[MAX];              //记录去重后的随机数

unsigned long ulrand(){      //生成一个大随机数
    return (
      (((unsigned long)rand()<<24)& 0xFF000000ul)
     |(((unsigned long)rand()<<12)& 0x00FFF000ul)
     |(((unsigned long)rand())    & 0x00000FFFul));
}
int main(){
    int big_rand;            //大随机数
  //printf("system: RAND_MAX= %d\n", RAND_MAX);  //查看系统随机数的最大值,一般是32767
    srand(time(0));          //用当前时间做随机数种子
//本题需要不同的随机数,用hash的方法来去重
     int num = 0;            //去重后随机数的数量
     for(int i=0; i< MAX; i++){      //hash
        big_rand = ulrand() % (MAX-MIN + 1) + MIN;  //[MIN,MAX]之间的一个随机数
        if(myhash[big_rand] == 0){   //用hash查重
           myhash[big_rand]=1;       //数字big_rand,登记在big_rand这个位置
           mynum[num]= big_rand;     //记录随机数
           num++;                    //最后产生 num-1 个随机数
        }
     }
     big_rand = ulrand() % (MAX-MIN + 1) + MIN;
     printf("%d %d\n",num, big_rand %(num +1));  //case的第一行

     int k= 500000; //题目要求[-500000,500000]内的数字
     for(int i=0; i<= num-1; i++){
        if(i< num-1)      printf("%d ", mynum[i]-k);   //有正有负
        if(i == num-1)    printf("%d\n", mynum[i]-k);  //最后一个,末尾没有空格
     }
	return 0;
}

(2)c++11环境

#include 
using namespace std;
const int MAX  = 1000000;    //100万个数
const int MIN  = 0;
int myhash[MAX]={0};         //用hash查重
int mynum[MAX];              //记录去重后的随机数

int main(){
    int big_rand;            //大随机数
//本题需要不同的随机数,用hash的方法来去重
     int num = 0;            //去重后随机数的数量
     mt19937 u32Rnd(time(0)); 
     uniform_int_distribution<> distrib(MIN, MAX);

     for(int i=0; i< MAX; i++){      //hash
        big_rand = distrib(u32Rnd);    //[MIN,MAX]之间的一个随机数
        if(myhash[big_rand] == 0){   //用hash查重
           myhash[big_rand]=1;       //数字big_rand,登记在big_rand这个位置
           mynum[num]= big_rand;     //记录随机数
           num++;                    //最后产生 num-1 个随机数
        }
     }
     big_rand = distrib(u32Rnd);
     printf("%d %d\n",num, big_rand %(num +1));  //case的第一行

     int k= 500000; //题目要求[-500000,500000]内的数字
     for(int i=0; i<= num-1; i++){
        if(i< num-1)      printf("%d ", mynum[i]-k);   //有正有负
        if(i == num-1)    printf("%d\n", mynum[i]-k);  //最后一个,末尾没有空格
     }
	return 0;
}

(3)运行
  上面的两个代码在一次运行后,从生成的100万个随机数中,得到约63万个不重复的数。
  设代码的文件名是hdu1425,执行代码,输出到文件data.in:

D:\>hdu4585 >data.in

A.5 对拍

  把程序提交到OJ之前,要先检验程序的正确性。前面已经获得了输入测试数据,那么输出数据如何产生?此时需要尽快写一个代码,它的逻辑和代码很简单,但是保证结果是正确的。
1、对拍的多个代码
  下面以Hdu 1425为例,给出3个实现:暴力的冒泡代码、sort排序、hash算法。
  (1)冒泡排序
  冒泡算法很容易写,但是它的时间复杂度是O( n 2 n^2 n2),如果排序100万个数,时间长达数小时,不能作为benchmark。
  不过,由于冒泡排序的代码非常简单,在赛场能快速编码,那么可以用小规模的输入测试数据,例如对10万个数排序,运行时间就能接受了。
  (2)sort排序
  下面代码也能通过OJ测试,不过这里把它看成benchmark,用来验证后面hash算法的正确性。

#include
using namespace std;
int a[1000001];    		               //记录数字
#define swap(a, b) {int temp = a; a = b; b = temp;} //交换
int n, m;
int main(){
    while(~scanf("%d%d", &n, &m)){
        for(int i=1; i<=n; i++)  scanf("%d", &a[i]);//scanf比cin快
        sort(a + 1, a + n + 1);
        for(int i = n; i >= n-m+1; i--){   //打印前m大的数,反序打印
            if(i == n-m+1)  printf("%d\n", a[i]);
            else            printf("%d ",  a[i]);
        }
    }
    return 0;
}

  设代码的文件名是sort,执行代码,读取输入data.in,输出到文件sort.out

D:\>sort <data.in >sort.out

(3)hash算法

#include
using namespace std;
const int MAXN = 1000001;
int a[MAXN];
int main(){
    int n,m;
    while(~scanf("%d%d", &n, &m)){
        memset(a, 0, sizeof(a));
        for(int i=0; i<n; i++){
            int t;
            scanf("%d", &t); //此题数据多,如果用很慢的cin输入,肯定TLE
            a[500000+t]=1;    //数字t,登记在500000+t这个位置
        }
        for(int i=MAXN; m>0; i--)
            if(a[i]){
                if(m>1)  printf("%d ", i-500000);
                else      printf("%d\n", i-500000);
                m--;
            }
    }
    return 0;
}

  设代码的文件名是hash,执行代码,读取输入data.in,输出到文件hash.out

D:\>hash <data.in >hash.out

2、比较2个代码的输出是否一样
  Windows:fc命令

D:\>fc sort.out hash.out /n

  Linux:diff命令

[root]#diff -c hash.out sort.out

A.6 统计执行时间

  代码运行的时间是多少,能通过OJ的时间限制吗?一般情况下,通过分析算法的复杂度就足够估算运行时间了。如果确实想知道运行时间,可以用下面的代码计时(windows环境)。不过,个人电脑和OJ的服务器速度并不同,而且还有输入输出的时间,所以下面的代码只能作为参考。

#include
using namespace std;
int main(){ 
   	clock_t start, end;
  	start = clock();                                  //开始时间
    system("d:\\hash d:\\data.out");    //运行hash代码,注意目录
   	end = clock();                                    //结束时间
  	cout << (double)(end - start) / CLOCKS_PER_SEC << endl;      //输出时间
}

  1. https://en.cppreference.com/w/cpp/numeric/random/mersenne_twister_engine ↩︎

  2. https://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution ↩︎

你可能感兴趣的:(算法竞赛专题解析(16):附A-测试数据的构造与对拍)