Python在竞赛中的应用-测试数据的构造与对拍 --算法专题精解(31)

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

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

  很多人认为Python是最受欢迎的编程语言,它编码简洁,有强大的库。
  Python早已应用在算法竞赛中,能节省大量比赛时间。
  大学的某些算法竞赛(ICPC)已经支持用Python语言提交代码。即使比赛不支持用Python提交代码,而Linux系统一般是预装Python的,可以用Python做辅助工作。
  Python的优点有编码简便、处理大数非常简单、构造测试数据比C++更简单等。Python的主要问题是执行时间慢,比C++、java慢得多,竞赛队员可能不会直接用Python交题。不过,Python可以作为工具使用,常用的是构造数据、写对拍代码。一个典型的应用是:如果能用打表法交题,可以先用python打表出数据,然后用c++或直接用Python提交。
  读者会发现,用Python做这些事,比用C++容易得多,这在紧张的比赛中是很有用的。
  若读者想学习用Python写算法竞赛题的代码,建议到力扣网站(leetcode-cn.com)练习,每个题都有人提交Python题解。
  Python的强大,主要是因为它庞大的库。Python有两种版本Python 2、Python 3,两者不兼容。本节基于 Python 3。Python编译环境下载地址:https://www.python.org/downloads/
  下面介绍Python在算法竞赛中的应用。

C.1 计算大数

  下面的题目是计算大数。


阶乘之和 洛谷 P1009https://www.luogu.com.cn/problem/P1009
题目描述:计算S=1!+2!+3!..+n! (n≤50)


  计算结果是一个天文数字,如果用C++编码,需要用高精度,很复杂。而Java和python都能直接处理大数。
  用Java编码很简单:

import java.math.*;
import java.util.*;
public class Main {
	public static void main(String[] args) {
         Scanner in= new Scanner(System.in);
         int n = in.nextInt();                               
         BigInteger s = new BigInteger("1");
         BigInteger ans = new BigInteger("0");
         for (int i = 1; i <= n; i++) {
              s = s.multiply(new BigInteger(String.valueOf(i)));
              ans = ans.add(s);
         }
         System.out.println(ans);
    }
}

  用Python编码更简单:

n = int(input()) 
s = 1 
ans = 0
for i in range(1,n+1,1):    
    s *= i
    ans += s 
print(ans)

C.2 构造随机数和随机字符串

  用Python构造测试数据,比c++简单得多。它能直接产生极大的数字,方便地产生随机字符等。下表列出了一些典型的随机数、随机字符串构造方法1

(1)导入库

import random  

  可以写成:

from random import *

  此时后面的代码能够简单一点,例如把random.randint直接写为randint

(2)在指定范围内生成一个很大的随机整数:

print (random.randint(-9999999999999999,9999999999999999))  

  输出示例:428893995939258

(3)在指定范围内(0到100000)生成一个随机偶数:

print (random.randrange(0, 100001, 2))   

  输出示例:14908
(4)生成一个0到1之间的随机浮点数:

print (random.random())   

  输出示例:0.2856636141181378
(5)在指定范围内(1到20)生成一个随机浮点数:

print (random.uniform(1, 20))   

  输出示例:9.81984258258233
(6)在指定字符中生成一个随机字符:

print (random.choice('abcdefghijklmnopqrst@#$%^&*()')) 

  输出示例:d
(7)在指定字符中生成指定数量的随机字符:

print (random.sample('zyxwvutsrqponmlkjihgfedcba',5))  

  输出示例:[‘z’, ‘u’, ‘x’, ‘w’, ‘j’]
(8)导入库

import string

  若写成from string import *,下面的string.ascii_letters改为ascii_letters
(9)用a-z、A-Z、0-9生成指定数量的随机字符串:

ran_str = ''.join(random.sample(string.ascii_letters + string.digits, 8))
print (ran_str)

  输出示例:iCTm6yxN
(10)从多个字符中选取指定数量的字符组成新字符串:

print (''.join(random.sample(['m','l','k','j','i','h','g','d'], 5)))

  输出示例:mjlhd
(11)打乱数组的顺序:

items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]   
random.shuffle(items)
for i in range(0,len(items),1):      #逐个打印
   print (items[i]," ",end='')

  输出示例:1 0 8 3 5 7 9 4 6 2

C.3 数组去重

1、整数去重
  随机生成的整数,很多是重复的,而一般情况下需要不重复的数据。下面给出两种去重方法。
  第1种方法用set(),速度极快。set()的时间复杂度差不多是O(n)的,和生成随机数的时间差不多。set()去重不能保持数据的原顺序。set()去重的原理是hash,所以有时候它返回的结果中部分数据看起来像排过序,但整体上并不是有序的。虽然set()去重后的数据的顺序不是那么随机,但是可以用shuffle()再次把数据打乱,得到排序随机的数组。
  第2种方法是暴力去重,非常非常慢,不过它的结果保持了原数据的顺序。

def NonRepeatList1(data):   #函数1:set去重,不保持原顺序
    return list(set(data))
def NonRepeatList2(data):   #函数2:暴力去重,保持原顺序
    return [i for n, i in enumerate(data) if i not in data[:n]]
#下面测试上面2个函数
import random
import time
time0 = time.time()
a = []
for i in range(0,100000,1):                #10万个随机数
    a.append(random.randint(-100000000,100000000))    #随机数取值范围
#print (a)                                 #可以打印数组看看
print ("random time =",time.time()-time0)  #统计随机数的生成时间

time0 = time.time()
b = NonRepeatList1(a)                      #去重,不保持原顺序
#print (b)                                 #打印看看顺序
random.shuffle(b)                          #再次打乱顺序
#print (b)                                 #打印看看是否乱序
print ("set time =",time.time()-time0)     #统计set()去重的时间

time0 = time.time()
c = NonRepeatList2(a)                      #去重,保持原顺序
#print (c)                                 #打印看看是否保持原序
print ("enum time =",time.time()-time0)    #统计暴力去重的时间

  代码中统计了两种方法的执行时间,在作者的电脑上运行,10万个数的时间是:

random time = 0.10671424865722656
set time = 0.06288003921508789
enum time = 99.96579337120056

  可见set()是极快的,是暴力去重的1600倍。
  再试试1000万个数,生成随机数和去重的时间是:

random time = 10.069396495819092
set time = 10.71152925491333

2、小数去重
  如果要生成不同的小数,简单的办法是先用上面的代码生成去重整数数组,然后把每个整数除以10的幂次即可。例如生成2位小数,把每个整数除以100。

d = []
for i in range(0,len(b),1):         #b是去重后的整数数组
    d.append(b[i] / 100)

C.4 构造测试数据和对拍

1、构造测试数据
  仍然以Hdu 1425为例。


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


  首先构造测试数据。下面的代码生成60多万个不同的无序的随机数。首先产生100万个随机数,然后用set去重,最后用shuffle打乱即可。

#设本代码的文件名是aa.py
import random
a= []
b= []
for i in range(0,1000000,1):      #100万个随机数
    a.append(random.randint(-500000,500000))
b=list(set(a))               #去重

#print("lena=",len(a))      #验证a的个数是不是100万个
#print("lenb=",len(b))      #b的个数有60多万个

random.shuffle(b)          #打乱b
print(len(b),random.randint(1,len(b)))   #打印n、m
for i in range(0,len(b),1):              #逐个打印
    print ( b[i],end=' ')  

#下面的做法是存到一个文件里面,其实不需要
''' 
f = open("d:\data.in", "w")    #输出到文件里
print(len(b),random.randint(1,len(b)),file=f)
for i in range(0,len(b),1):      #逐个打印
    print ( b[i],end=' ',file=f)    
f.close()
'''

2、对拍
  下面以Windows环境为例说明构造测试数据和对拍的过程,linux环境类似。
  把上面的代码存为文件aa.py,执行下面的命令,输出测试数据到文件data.in

D:\>C:\Users\hp\AppData\Local\Programs\Python\Python39\python aa.py >data.in

  作者的python安装在目录C:\Users\hp\AppData\Local\Programs\Python\Python39\,读者可以按自己的目录操作,或者设置环境变量,就不用输这个目录了。
  下面给出Hdu 1425的对拍代码,功能是:先输入n和m,然后输入n个数,排序后,打印出前m大的数。与C++对拍代码相比,Python代码更简单。

#设本代码的文件名是bb.py
n,m = map(int,input().split())  #输入n、m
a=[int(n) for n in input().split()]  #输入n个数
a.sort()  #排序
for i in range(n-1,n-m,-1):  #打印出前m-1大的数
    print ( a[i],end=' ')
print ( a[n-m])              #打印出第m大的数

  把代码存为文件bb.py。Windows环境下,执行以下命令,读输入数据data.in,输出数据到py.out

D:\>C:\Users\hp\AppData\Local\Programs\Python\Python39\python bb.py <data.in >py.out

  前一节“测试数据的构造与对拍https://blog.csdn.net/weixin_43914593/article/details/106863166”的c++代码是:

#include<bits/stdc++.h>
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个代码的输出是否一样,执行以下命令:

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

  若文件一样,输出(上面一行中的hash.out,在下面显示HASH.OUT这不是笔误):

正在比较文件 py.out 和 HASH.OUT
FC: 找不到差异	

3、把所有过程写成批处理文件2

(1)windows环境
  把上述过程写成windows环境的bat批处理。它是一个死循环,在每个循环生成数据并对拍,直到发现错误,出错误的那组输入和输出都留在文件中,可以查看。下面代码中的路径是作者电脑的路径,请读者改为自己电脑的路径。

@echo off
set path=C:\MinGW\bin
g++ -o hash.exe hash.cpp
:loop
set path=C:\Users\hp\AppData\Local\Programs\Python\Python39
python aa.py >data.in
hash.exe <data.in >hash.out
python bb.py <data.in >py.out
set path=C:\Windows\System32
fc py.out hash.out
if errorlevel == 1 pause
goto loop

(2)linux环境

  linux的文件比较命令是diff。

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

  本文没有在linux环境中试Python,这里给出一个c++的参考,功能类似。

#!bin/bash
while true; do
    gcc hash.cpp -o hash.exe
    gcc sort.cpp -o sort.exe
    gcc makedata.cpp -o makedata.exe
    ./makedata.exe >data.in
    ./hash.exe <data.in >hash.out
    ./sort.exe <data.in >sort.out
    if diff hash.out sort.out; then
        echo OK
    else 
        echo wrong
        break
    fi
done

  1. 参考https://www.cnblogs.com/zqifa/p/python-random-1.html ↩︎

  2. 感谢华东理工大学17级队员李震 ↩︎

  • 27
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗勇军

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值