ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言实战:用数组与随机数实现“试试手气”彩票模拟程序

C语言实战:用数组与随机数实现“试试手气”彩票模拟程序 PTA题单里L1-085这个编号常刷C语言基础题的同学应该不陌生。题目叫“试试手气”名字挺轻松说白了就是让你写一个抽奖开奖的模拟程序用户输入一组号码程序随机生成一组开奖号码比对之后输出中奖情况。听起来简单但实际写起来牵扯到数组、随机数生成、去重、条件判断、格式化输出这些基础知识点还有不少新手容易踩的坑。这篇就围绕这道题把从设计思路到完整代码、从常见报错到扩展玩法一次说透适合刚开始学C语言的人照着练也适合已经刷过一遍的人回来补细节。1. 项目概述与核心需求解析1.1 从标题和编号理解题目背景PTAProgramming Teaching Assistant的题目编号有一定的规律L1表示基础级别通常面向大一上学期或刚入门编程的初学者。L1-085意味着这是一道典型的基础题难度不高但综合性并不低。题目名“试试手气”很容易让人联想到彩票事实上这道题的核心场景就是模拟一次“手气测试”你选一注号码程序帮你开出当期的“中奖号码”然后根据命中情况判定你中了什么奖。很多第一次接触这道题的同学会误以为题目涉及复杂的概率计算或者算法设计其实完全不是。它就是一道“流程题”考察你能不能把一套明确的规则转换成代码用户输入、随机开奖、逐一比对、输出结果。把思路理清代码其实不难难的是把边界条件和输出格式处理干净。1.2 这道题究竟在考什么我把这道题的知识点拆开大致是这几块数组的声明和初始化用来存放用户号码和开奖号码。随机数的生成通常用rand()配套srand()设置种子。循环和条件判断用于去重、比对命中数量。格式化输出PTA对输出格式的要求非常死多一个空格都算错。逻辑边界处理例如号码范围是否合法、命中数为0时怎么输出。这里有个容易产生困惑的点不同版本的题目具体的号码范围和奖项规则可能不同。比如有的版本是6个红球从1到33选有的可能简化为从1到10选5个。我用的是最常见的“1到33选6个红球”这个设定来讲解因为它的代表性最强。如果遇到其他范围你只需要改宏定义里的数值即可代码逻辑完全通用。1.3 这道题适合谁来练、练完能收获什么作为一道基础综合题“试试手气”的定位很适合下面几类人刚学完指针和数组、想找题目巩固语法的C语言初学者。临近考试或者准备PTA刷题冲分的同学这道题可以作为基础题的收尾练习因为它比单考一个知识点更接近真实编程。想理解“随机数为什么每次运行都不一样”这类概念的人这道题就是最好的实践入口。练完之后你至少能熟练掌握数组遍历、随机数去重和格式控制这几个技能而且对“把中文描述的需求翻译成代码流程”这件事会有更直观的感受。2. 整体设计与思路拆解2.1 动笔之前先想清楚程序的主流程拿到题目先别急着打开编辑器。不管是彩票模拟还是别的什么代码最怕的是边写边想流程结果写到一半发现逻辑对不上。我的习惯是先在纸上或者脑子里把流程捋一遍。这道题的核心流程其实只有四步获取用户输入的号码组合。随机生成一组开奖号码并保证号码不重复。统计两组号码中有多少个相同。根据命中数量判定结果并输出。如果你还希望程序更“拟真”一点可以把开奖号码也打印出来让用户看到自己到底差在哪几个号。这样输出信息更完整代码也更好调试。这一套流程走下来你会发现它和很多业务逻辑是相通的先收集数据再经过一个“处理过程”最后给出结果。只不过在这里“处理过程”是随机开奖和命中比对。2.2 数据结构选型为什么用数组很多人看到“一组号码”第一反应是定义6个单独的变量int a1, a2, a3, a4, a5, a6;——这种写法非常不建议一来代码冗余二来后面比对的循环很难写总不能一个变量一个变量地去判断。数组在这里就是最自然的选择因为号码是一个同类型元素的集合我们需要通过下标来遍历它。核心的数据结构通常有两个数组userNums[6]存放用户输入的6个号码。lotteryNums[6]存放随机生成的开奖号码。有的同学还会加一个“命中标记数组”matchFlags[6]用来记录每个用户号码是否在开奖号码中出现过。但对于只有6个元素的场景完全可以用一个双重循环直接统计命中数不需要额外数组代码反而更简洁。这里必须说一句做题不是写大工程在数据规模很小的时候优先考虑逻辑清晰而不是过度设计。2.3 随机数生成的正确用法与常见误解这是很多新手绕不过去的一道坎。rand()生成的是一个0到RAND_MAX之间的整数如果你直接用它每次得到的其实都是一个确定序列里的数而这个序列的起点是固定的。所以如果你不设置随机种子每次运行程序开出的号码都是同一组那就谈不上“试试手气”了。解决方法是调用srand()为随机数生成器提供一个种子。常规写法是srand((unsigned int)time(NULL));这里time(NULL)返回当前时间的秒数作为种子。因为程序每次运行的时刻不同所以产生的随机序列也不同。必须注意srand()在整个程序里只需要调用一次放在main函数开头即可。有些同学在循环里反复调用srand()结果是每一轮都重新设置种子而如果同一秒内执行多次反而会得到完全相同的号码这就弄巧成拙了。至于“如何生成1到33之间的随机数”通用的映射公式是int num rand() % 33 1;先取余数得到0到32再加1挪到1到33。这个写法在小题目里完全够用虽然严格来说它不是完美的均匀分布但对于这道题的规模不会产生任何可见的问题。如果以后遇到需要更高质量随机数的场景再去考虑别的算法现阶段先把原理搞明白。3. 核心代码实现与逐段讲解3.1 接收用户输入基础但不简单的第一步用户输入这一步看似只是几个scanf但里面藏着一个体验问题用户输错了怎么办比如手滑输了一个36超出了1到33的范围。如果题目没有明确要求校验你可以不管但如果想让自己的代码更完善我是建议加一层简单的范围检查。一种做法是如果某个号码不在合法范围内提示用户重新输入这一个号码。写成代码是这样的#define TOTAL_NUMS 6 #define MIN_NUM 1 #define MAX_NUM 33 int userNums[TOTAL_NUMS]; for (int i 0; i TOTAL_NUMS; i) { do { printf(请输入第%d个号码1-%d, i 1, MAX_NUM); scanf(%d, userNums[i]); if (userNums[i] MIN_NUM || userNums[i] MAX_NUM) { printf(输入无效请重新输入。\n); } } while (userNums[i] MIN_NUM || userNums[i] MAX_NUM); }do...while在这里很好用因为它至少要执行一次天然适合“先输入再判断”的场景。如果你用的是普通的while反而不太顺手。不过这里我多提醒一句如果你最终是要提交到PTA这类在线评测系统输入是固定格式的通常不需要写这样的交互提示写得太“花哨”反而可能因为多余输出导致格式错误。上面这种完整交互写法更推荐用于自己在本地模拟、练习或者后续做小项目的时候。3.2 生成开奖号码随机加去重是核心难点生成开奖号码的难点不在“随机”而在“不重复”。如果直接用6次随机很可能开出两个相同的号码这就不符合彩票的规则了。实现不重复的思路有很多最简单直接的是“每次生成一个候选号然后和已经生成的所有号比较一遍有重复就重新生成”。int lotteryNums[TOTAL_NUMS]; int count 0; while (count TOTAL_NUMS) { int candidate rand() % MAX_NUM 1; int duplicate 0; for (int i 0; i count; i) { if (lotteryNums[i] candidate) { duplicate 1; break; } } if (!duplicate) { lotteryNums[count] candidate; count; } }解释一下这段逻辑count表示已经成功存入数组的号码数量。生成候选号后遍历当前已有的号码看有没有重复。没有重复就存入数组同时count加1如果有重复就跳过重新生成一个候选号。因为每次只取没被使用过的号最终一定能填满6个且互不相同。这里还有一点为什么用while (count TOTAL_NUMS)而不是for (int i 0; i TOTAL_NUMS; i)因为for的循环变量在每次迭代后自动加1但生成重复号时不希望计数增加还必须再试一次。用while手动控制count的增减思路更清晰也避免了在for循环体里对循环变量做额外操作的别扭写法。3.3 命中统计与奖项判定双重循环解决拿到用户号码和开奖号码后要统计用户中了几个号。最直观的方法是双重循环拿用户数组里的每一个号去开奖数组里找一遍找到就命中数加1。int hit 0; for (int i 0; i TOTAL_NUMS; i) { for (int j 0; j TOTAL_NUMS; j) { if (userNums[i] lotteryNums[j]) { hit; break; } } }这段代码的时间复杂度是O(n²)在n6的情况下毫无压力。另外因为开奖数组已经做过去重用户号码如果也没有重复那么每个命中号只会被数一次统计结果准确无误。如果用户的输入允许重复理论上同一个号匹配多次会重复计数所以更严谨的做法是在用户输入阶段也做一次去重这和生成开奖号码的逻辑一模一样不少同学容易漏掉。命中数算出来之后判定规则就简单了。假设规则是命中6个为一等奖5个为二等奖4个为三等奖其余视为未中奖。写if...else if或者switch都可以if (hit TOTAL_NUMS) { printf(恭喜一等奖\n); } else if (hit TOTAL_NUMS - 1) { printf(二等奖\n); } else if (hit TOTAL_NUMS - 2) { printf(三等奖\n); } else { printf(未中奖下次再试试手气吧。\n); }这样写的好处是如果以后把号码数量从6改成5只要TOTAL_NUMS变了奖级判断也跟着自动调整不会出现改一处漏一处的尴尬。3.4 输出格式最容易白丢分的地方在线评测系统对输出格式的敏感程度说一句“锱铢必较”都不夸张。最常见的问题有几个行末多了一个空格、输出后缺少换行、打印了多余的解释文字。比如你要输出开奖号码正确做法是每个数后面跟一个空格但最后一个数后面不要再跟空格然后整体换行。这个用printf的循环写法可以这样控制for (int i 0; i TOTAL_NUMS; i) { if (i 0) { printf( ); } printf(%d, lotteryNums[i]); } printf(\n);先判断是不是第一个元素不是就在前面补一个空格这样无论是元素之间还是末尾都干净利落。很多同学喜欢写成“每个数后面加空格最后再退格”这种做法虽然在某些本地环境能跑出正确结果但在评测系统里是不可靠的一定要避免。如果是在本地练习输出“开奖号码”“你的号码”“命中数”“中奖结果”这些信息时可以用中文描述分隔看得更明白。但提交到OJ之前必须仔细阅读题目要求的输出格式严格照做。4. 完整代码示例与测试过程4.1 一份可直接编译运行的参考代码把上面的思路汇总起来我给出一个完整版本。这个版本做了简单的输入校验也加入了开奖号码展示适合本地练习和二次开发#include stdio.h #include stdlib.h #include time.h #define TOTAL_NUMS 6 #define MIN_NUM 1 #define MAX_NUM 33 int main() { srand((unsigned int)time(NULL)); int userNums[TOTAL_NUMS]; int lotteryNums[TOTAL_NUMS]; printf(请输入你的%d个号码1-%d\n, TOTAL_NUMS, MAX_NUM); for (int i 0; i TOTAL_NUMS; i) { do { printf(第%d个号码, i 1); scanf(%d, userNums[i]); if (userNums[i] MIN_NUM || userNums[i] MAX_NUM) { printf(无效输入请重新输入。\n); } } while (userNums[i] MIN_NUM || userNums[i] MAX_NUM); } int count 0; while (count TOTAL_NUMS) { int candidate rand() % MAX_NUM 1; int duplicate 0; for (int i 0; i count; i) { if (lotteryNums[i] candidate) { duplicate 1; break; } } if (!duplicate) { lotteryNums[count] candidate; count; } } printf(\n本次开奖号码); for (int i 0; i TOTAL_NUMS; i) { if (i 0) printf( ); printf(%d, lotteryNums[i]); } printf(\n你的号码); for (int i 0; i TOTAL_NUMS; i) { if (i 0) printf( ); printf(%d, userNums[i]); } printf(\n); int hit 0; for (int i 0; i TOTAL_NUMS; i) { for (int j 0; j TOTAL_NUMS; j) { if (userNums[i] lotteryNums[j]) { hit; break; } } } printf(命中个数%d\n, hit); if (hit TOTAL_NUMS) { printf(恭喜一等奖\n); } else if (hit TOTAL_NUMS - 1) { printf(二等奖\n); } else if (hit TOTAL_NUMS - 2) { printf(三等奖\n); } else { printf(未中奖下次再试试手气吧。\n); } return 0; }这段代码在本地编译运行没有任何问题但要注意PTA原题通常省略所有中文提示只要求输出指定格式。刷题时请务必对照题目描述做减法。4.2 如何用固定种子做可复现测试随机数是复现问题的一个麻烦点。你想验证程序逻辑是否正确结果每次跑出来的开奖号码都不一样靠肉眼很难核对。我在调试这类题目的时候有个习惯先临时把srand的种子固定住比如srand(42);这样只要代码逻辑不变每次运行生成的序列完全一致方便逐行跟踪、核对输出。等你确认逻辑没问题了再改回srand((unsigned int)time(NULL));提交或者正常使用。这个习惯特别适合所有涉及随机数的题目能省下大量反复运行、对比的功夫。4.3 几组值得多跑几次的测试样例我建议你至少测这几类情况用户输入的号码在边界值附近比如正好输入1和33确认校验和随机生成逻辑没有越界问题。连续运行多次确认开奖号码确实会变化并且每组号码内部没有重复。故意输入一个0或34确认程序能提示无效并要求重新输入而不是带着错误数据继续跑。还有一种情况容易被忽略如果用户在输入时敲了类似1 2 3 4 5 6这样一行多个数字而程序是循环6次scanf那么第二次循环会直接读取缓冲区里剩下的数字不会真的停下来等第二次输入。这在本地命令行里表现得像是“跳过了输入”其实是因为缓冲区里有数据。想彻底弄懂这个机制可以去了解一下scanf的缓冲读取行为这里我先把现象指出来避免你在本地测试时一头雾水。5. 常见问题与排查技巧实录5.1 高频问题速查表我把初学者在这道题上最容易犯的错整理成一个表方便你对照自查现象根本原因解决办法每次运行开奖号码一模一样没有调用srand()随机数序列固定程序开头加一次srand((unsigned int)time(NULL))开奖号码里有重复随机生成后没有去重直接存入了数组用“候选号与已有号逐一比较”的逻辑过滤输出后OJ提示“格式错误”多打了空格、少了换行或多了提示文字按题目要求严格控制输出内容末尾不留多余空格记得换行明明中了5个号却只统计到3个用户输入的号码本身有重复导致重复计数在用户输入阶段也做去重程序在输入阶段表现异常、好像“跳过”了某次输入scanf从输入缓冲区读取了上一次残留的数据理解缓冲区机制必要时用while (getchar() ! \n);清空缓冲区结果正确但代码结构混乱不好改所有逻辑堆在main里没有拆成函数将随机生成、命中统计封装成独立的函数5.2 两个容易被忽略的隐蔽问题第一个是srand放在循环里的问题。我刚入行时在这个坑上栽过跟头为了“更随机”我在每次生成号码前都调用一次srand(time(NULL))结果程序运行特别快同一秒内产生的多次随机数完全一样导致每一注号码都相同。如果你要做批量模拟这点尤其致命。正确的做法是srand只调用一次之后的每一次rand()自然会在序列中往前走。第二个是去重循环的效率陷阱。虽然n6时O(n²)一点问题都没有但如果有人把这道题改造成“从1到80选20个号”之类的大规模场景双重循环就会开始吃紧。遇到那种情况可以用布尔数组做标记bool used[MAX_NUM 1] {false};每次生成候选号后判断if (!used[candidate])然后把它标记为已用。这样去重操作就从O(n)降到了O(1)代码也更清晰。这是一个很好的思维提升点建议今天就把这个写法也试一遍。5.3 调试这类题目的小技巧调试随机数相关的程序切忌“跑一遍看输出猜逻辑”。我的习惯是这样固定随机种子保证每次运行结果可复现。在关键位置加临时的打印语句比如把每次生成的候选号和去重结果打印出来确认循环没跑偏。确认无误后删掉临时打印恢复正常输出。最后再换回时间种子多跑几遍看随机性和稳定性。这种方法不只在“试试手气”这道题上有效任何涉及随机、排序、递归的题目都可以用。把过程可视化出来错误往往一眼就能发现。6. 扩展思路与进阶玩法6.1 从一次手气到十万次模拟这道题刷完之后我最推荐做的扩展是把“单次开奖”升级成“批量模拟”。比如写一个循环模拟买100000注彩票统计命中的分布情况。这个过程能让你直观感受到概率的威力。你可以用一个长度为7的数组stat[7]来记录命中0到6个号的次数。每模拟一注就统计命中数对应计数加1。跑完10万次之后打印分布结果。按照概率常识命中0个和1个的情况会占绝大多数命中6个的次数大概率是0。这种量级的体验比看教科书上的概率公式要深刻得多也会让你对“试试手气”这四个字有新的认识——玩票可以沉迷不值得概率摆在那里。6.2 玩法变体换规则、加选号、做图形化如果觉得常规玩法不够过瘾可以试试这几个改造方向把号码范围从“1到33选6”改成“1到35选5加1到12选2”模拟大乐透规则核心逻辑基本不变只是多了一组数组和一次比对。增加一个“往期开奖记录”功能把每次开奖结果存到文件里下次运行可以读取统计这就能顺带练到文件操作。用图形界面或者网页版实现同一个逻辑把printf换成界面控件本质上也是在练前端和交互设计。这些变体每一种都不白做。因为核心的数组、循环、随机、判定逻辑已经被你掌握了剩下的只是在这个骨架上套不同的壳。6.3 我写这类题目时的一些习惯最后分享几个我个人的编码习惯虽然不直接决定题目对错但对代码质量和调试体验影响很大。第一宏定义代替魔法数字。比如TOTAL_NUMS、MAX_NUM一旦要改规则只改顶部几行就行了不会出现满屏的6和33不知道哪个是哪个的情况。第二把独立逻辑拆成函数。随机生成开奖号码、统计命中数、判断奖级各归各的块。这样代码读起来像文章分段落而不是一大坨流水账。第三提交前做一次“收缩手术”。本地练习版本里那些友好的中文提示如果OJ用不上就果断删掉。被格式错误打击过的同学一定懂我在说什么。这道题名字叫“试试手气”其实手气好不好全靠程序说了算。但你能控制的是代码的严谨程度、逻辑的清晰程度还有对细节的把握。把这些练扎实了遇到任何基础题都不会慌。
返回列表