简介:这是一份面向计算机专业学生与C#初学者的课程设计资源,采用C#语言实现双色球与大乐透的过滤算法及号码预测功能,覆盖历史数据读取、条件过滤、趋势预测等典型流程,适合作为期末大作业、课程设计或入门进阶的参考项目。压缩包共28个文件,以13个cs源码文件为主体,配合csproj与sln工程配置、txt数据文件、README说明及JSON配置,整体约243KB,结构紧凑便于打开即用。已有173人学习浏览。项目按SupperLotto、UnionLotto等模块组织,包含JsonHelper数据解析、Forecast预测、History历史管理等核心类,代码均测试运行通过,可直接在此基础上扩展新玩法或改进预测策略,也可作为C#桌面应用开发的完整范例。
1. 双色球与大乐透的过滤算法与预测:一套 C# 课程大作业能拆出多少东西
“C# 课程大作业”常见的挂科点是做了个界面但没算法、有算法但没数据验证。这套双色球+大乐透过滤算法与预测工程,核心不是神棍式“预测开奖”,而是完整的 C# 数据统计与组合筛选项目:读取历史开奖数据、生成候选号码组、按和值/跨度/奇偶比/区间分布/AC 值/遗漏值逐层过滤,输出缩水后的号码清单和回测结果。它适合三类人:C# 初级学员拿课设框架,想学数组/集合/文件 IO/事件处理的中级读者,以及想理解“彩票过滤为什么是概率游戏而不是预测游戏”的爱好者。先说边界:每个号码独立随机,过滤只是基于历史统计特征做条件筛选,不能提高真实中奖概率;但作为算法与工程练习,这套资源的价值非常扎实。
2. 玩法规则与概率模型:过滤算法为什么有生存空间
2.1 双色球与大乐透的规则、组合数与 C(33,6) 计算
双色球每注由 6 个红球(01-33 中选 6 个,不重复)加 1 个蓝球(01-16 中选 1 个)组成;大乐透每注由 5 个前区号码(01-35 中选 5 个,不重复)加 2 个后区号码(01-12 中选 2 个,不重复)组成。两注的一等奖组合数分别是 C(33,6) × 16 = 17,721,088 与 C(35,5) × C(12,2) = 21,425,712,量级都在两千万上下。这个数字决定了“全包”不现实,也给过滤算法留出了空间:一次课设如果只是 Random 生成几注号码再比较,过滤条件(比如和值范围)可以把候选池从百万级收缩到几百组。
| 彩种 | 前区/红球选号 | 后区/蓝球选号 | 组合总数 | 一等奖概率(约) |
|---|---|---|---|---|
| 双色球 | 33 选 6 | 16 选 1 | 17,721,088 | 1/1772 万 |
| 大乐透 | 35 选 5 | 12 选 2 | 21,425,712 | 1/2142 万 |
用 C# 实现组合数计算,避免直接算阶乘导致中间结果溢出:
static long Combination(int n, int k) { if (k < 0 || k > n) return 0; k = Math.Min(k, n - k); // 利用 C(n,k)=C(n,n-k),减少循环次数 long result = 1; for (int i = 1; i <= k; i++) { result = result * (n - k + i) / i; // 边乘边除,防止中间溢出 } return result; }逻辑说明:代码用递推公式的累积形式,先把 k 缩到 n/2 以内减少循环,再边乘边除让中间结果保持在较小范围。参数 n 是号码总数,k 是选号个数,调用 Combination(33, 6) 返回 1,107,568,再乘 16 就是双色球全组合。这里强调数据类型用 long:33 选 6 在 int 范围内,但大乐透 35 选 5 × 12 选 2 之后接近 2.1 千万,单步中间结果用 long 更稳。很多课设里直接把 33 的阶乘算出来,一旦 k 略大就溢出,改用边乘边除是这份资源里比较值得抄的习惯。
2.2 号码特征统计:和值、跨度、奇偶比、区间分布、AC 值
过滤条件本质上都是一些“号码集合的统计特征”,每个条件给号码组打一个分或做一次布尔判定。不少人觉得这些是玄学,其实它们只是把号码组数字化,让程序能批量比较。下面这几个是这套资源里最常见的:
- 和值:红球/前区所有号码之和。双色球均值是 (1+33)/2 × 6 = 102;大乐透前区均值是 (1+35)/2 × 5 = 90。历史开奖号码的和值会集中在均值附近,所以过滤时把和值限定在均值 ±1 个标准差范围内是常规做法。
- 跨度:最大号减最小号。跨度太小说明号码挤成一团,太散则说明分布过宽,筛掉两端。
- 奇偶比:奇数个数比偶数个数。双色球历史分布里 3:3、2:4、4:2 出现频率最高,极端 0:6 或 6:0 极少见。
- 区间分布:把号码池按号码大小切成 3 或 5 个区间,记录每个区间取几个。比如双色球 33 个红球切 1-11、12-22、23-33 三个区间。
- AC 值:一组号码的“散度”。定义是“这组号码中所有两两差值的不同个数”减去“选号个数减 1”。红球 AC 值一般落在 5-9 之间,AC 值过低表示号码结构太规律。
static int CalcAcValue(int[] nums) { var diffs = new HashSet<int>(); for (int i = 0; i < nums.Length; i++) for (int j = i + 1; j < nums.Length; j++) diffs.Add(Math.Abs(nums[i] - nums[j])); return diffs.Count - (nums.Length - 1); }逻辑说明:双层循环算所有两两差值,放进 HashSet 自动去重,然后用“不同差值个数”减去“选号个数减 1”。对双色球 6 个红球来说,两两组合最多 15 种,AC 值最大是 15 - 5 = 10。注意这里必须先保证 nums 已经按升序排列且内部无重复,否则差值集合计算结果会失真,这是一个容易踩的坑,后面避坑章节会专门复盘。
参数说明:AC 值没有“越大越好”的说法,实际课设里常用 AC >= 6 作为过滤条件,把过于规整或过于均匀的号码组剔除。
2.3 过滤条件的正交性与筛选强度
把过滤条件逐个应用时,相邻条件的相关性直接决定缩水效率。如果同时加“和值 90-114”和“跨度 20-31”,两个条件对号码池的筛选强度并不是简单相乘,因为和值偏高往往伴随跨度偏大,条件高度相关,交集实际只筛掉边缘组合;而“区间分布 2/2/2”和“奇偶比 3:3”相关性低,组合起来效果好。
这一点对课设评分影响很大。很多同学把十个过滤条件全加一遍,结果候选池还是好几万,原因就是条件重复覆盖了同一批号码。反过来,条件设太狠,比如同时限定和值 100-104、跨度 25-28、区间分布必须 2/2/2,候选可能只剩几十注,回测时经常一注都中不了。这套资源里过滤器的默认参数是按“4 个正交条件 + 1 个宽松 AC 条件”组织的,而不是把所有条件全选上。正交性不是教科书概念,它直接决定你的缩水比是 1/1000 还是 1/50。
3. 过滤算法核心实现:C# 里如何把选号器搭起来
3.1 号码数据模型与历史开奖数据读取
历史开奖数据在课设里一般是从网上复制的文本或 Excel 导出,常见格式是“期号|红球1..6|蓝球”或 CSV。先定义数据模型:
public class NumResult { public string DrawCode { get; set; } // 期号 public int[] Reds { get; set; } // 红球/前区,长度 6 或 5 public int Blue { get; set; } // 蓝球/后区第一码 } static List<NumResult> LoadHistory(string path) { var list = new List<NumResult>(); try { using var reader = new StreamReader(path, Encoding.UTF8); string line; while ((line = reader.ReadLine()) != null) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(','); // 逗号分隔 CSV int[] reds = parts[1..^1].Select(int.Parse).ToArray(); int blue = int.Parse(parts[^1]); list.Add(new NumResult { DrawCode = parts[0], Reds = reds, Blue = blue }); } } catch (FileNotFoundException ex) { MessageBox.Show($"历史数据文件不存在:{ex.Message}"); } return list; }逻辑说明:历史数据文件每行一个期号,后面是逗号分隔的若干红球,最后一个蓝球。代码用 parts[1..^1] 取第 2 个到倒数第 2 个元素作为红球数组,最后一个元素作为蓝球,避免出现红蓝倒挂的错位。用 Encoding.UTF8 读取,但如果是老 Excel 导出的 GB2312 文本,这一行会乱码,解决方式在避坑章节里说明。
参数说明:红球数组长度由数据文件本身决定,6 个就是双色球,5 个就是大乐透前区。同一个类可以复用到两个彩种,区别只在于后面过滤参数不同。蓝球字段在大乐透场景下改成后区号码。try-catch 里只捕了 FileNotFoundException,实际还可以补 IOException 和 UnauthorizedAccessException,课设演示时最怕弹未处理异常窗口。
3.2 红球组合生成器与随机采样策略
号码过滤的前提是有一个候选池。候选池有两种来源:一是全组合生成,双色球红球 110 万组可接受;二是随机采样 N 组,适合大乐透 35 选 5 的 32 万组前区组合。
static IEnumerable<int[]> GenerateCombinations(int n, int k) { int[] combo = new int[k]; foreach (var arr in Combine(combo, 0, 1, n, k)) yield return (int[])arr.Clone(); } static IEnumerable<int[]> Combine(int[] combo, int pos, int start, int n, int k) { if (pos == k) { yield return combo; yield break; } for (int v = start; v <= n - (k - pos) + 1; v++) // 剪枝:剩余位置必须填得下 { combo[pos] = v; foreach (var arr in Combine(combo, pos + 1, v + 1, n, k)) yield return arr; } }逻辑说明:递归每层选一个数字,下一层从当前值加 1 开始,保证组合内不重复且有序。循环上限写成 n - (k - pos) + 1 是这道题比较值得抄的剪枝:如果剩余位置太多、后面填不下就直接不进循环。GenerateCombinations(33, 6) 会按字典序产出 1,107,568 组红球组合。注意 yield return 是惰性求值,不会一次性全部占内存,这对课设里“边生成边过滤”非常重要。
参数说明:n 是号码池总数,k 是选号个数。随机采样则不需要生成全组合,直接 new Random() 一组一组生成并用 HashSet 去重,采样量建议 10 万到 50 万之间,太小的话过滤后剩余不足,太大的话后续每层遍历有性能压力。如果想复现结果,Random 一定要传固定种子,否则每次跑出来的候选池不同,回测没法对比。
3.3 常用过滤条件的实现:和值、奇偶比、区间分布、连号
把每个过滤条件封装成对 int[] 的谓词,是所有后续逻辑的地基:
static class FilterHelper { public static bool CheckSum(int[] nums, int min, int max) { int sum = nums.Sum(); return sum >= min && sum <= max; } public static bool CheckParity(int[] nums, int oddMin, int oddMax) { int odd = nums.Count(x => x % 2 == 1); return odd >= oddMin && odd <= oddMax; } public static bool CheckSpan(int[] nums, int min, int max) { int span = nums[^1] - nums[0]; // 前提:nums 已升序 return span >= min && span <= max; } public static bool CheckInterval(int[] nums, int segment, int[] expected) { var counts = new int[segment]; foreach (var v in nums) counts[(v - 1) / (33 / segment)]++; // 按号码值映射到区间 for (int i = 0; i < segment; i++) if (counts[i] != expected[i]) return false; return true; } }逻辑说明:每个方法都是“号码数组 → 布尔值”的纯函数。CheckSum 用 LINQ 求和;CheckParity 统计奇数个数,把奇数个数落在 oddMin 到 oddMax 之间作为条件,相当于同时限制了奇偶比范围;CheckSpan 依赖数组已升序,直接取最后一个减第一个;CheckInterval 把号码池等分为 segment 个区间,expected 数组描述每个区间应该有的号码个数,比如 [2,2,2]。这些方法互相独立,因此在调用处可以自由组合成过滤流水线。
参数说明:分段逻辑用的 33/segment 是双色球口径,如果换到大乐透 35 个前区号码,分段分母要改成 35,否则第 34、35 号会映射到错误的区间下标。这个细节很容易在课设答辩时被问到,建议在代码注释里写明。
3.4 过滤流水线与结果输出
过滤器像流水线一样逐层处理候选池,每层通过才进入下一层:
static List<int[]> RunFilter(IEnumerable<int[]> pool, FilterOptions opt) { var result = new List<int[]>(); foreach (var combo in pool) { if (!FilterHelper.CheckSum(combo, opt.SumMin, opt.SumMax)) continue; if (!FilterHelper.CheckParity(combo, opt.OddMin, opt.OddMax)) continue; if (!FilterHelper.CheckSpan(combo, opt.SpanMin, opt.SpanMax)) continue; if (!FilterHelper.CheckInterval(combo, 3, new int[] { opt.Interval0, opt.Interval1, opt.Interval2 })) continue; result.Add(combo); } return result; }逻辑说明:foreach 里每个条件用 continue 短路,被筛掉的号码不再进入后续判断。这个写法比嵌套 if 干净,也方便以后加新条件。FilterOptions 是封装所有过滤参数的配置类,把和值上下限、奇数个数上下限、跨度、区间分布集中在一处,UI 上改了参数就重新跑一次 RunFilter。
注意这里 RunFilter 的输入可能是全组合也可能是随机采样。全组合场景下,110 万组红球经过 4 层条件,最终通常剩几百到几千组,运行时间在几百毫秒量级。输出到界面时,把每组号码用 string.Join(" ", combo.Select(x => x.ToString("00"))) 格式化,蓝球单独追加在末尾,避免排序污染。
4. 参数与回测:过滤条件到底该怎么设
4.1 参数推荐区间表与口径说明
课设里最常被问“和值范围到底写多少”这类问题。下面这套默认参数按历史开奖频率统计,口径是“均值 ± 1 个标准差”,不是拍脑袋:
| 彩种/区域 | 和值 | 跨度 | 奇数个数 | 区间分布 | AC 值 |
|---|---|---|---|---|---|
| 双色球红球 | 90-114 | 20-31 | 2-4 | 3 区间 2/2/2 或 2/1/3 | >= 6 |
| 大乐透前区 | 75-103 | 21-33 | 2-3 | 5 区间较平均 | >= 5 |
| 大乐透后区(2 码) | 3-19 | 1-11 | 不限制 | 不适用 | 不适用 |
口径说明:和值直接用号码真实值求和;奇数个数是“奇数号码出现的次数”,等于 6 减偶数个数;区间分布按号码值等分。大乐透后区只有两个号码、66 种组合,只建议做跨度与和值过滤,别做区间分布,否则很容易滤空。
参数怎么改:如果不想抄这一组固定值,可以直接在代码里按“均值 ± 1 个标准差”动态计算。均值公式是 (最小号 + 最大号) / 2 × 选号个数,标准差用历史数据的实际分布拟合。动态计算的代码可读性稍差,但课设答辩时更有说服力,因为它证明参数不是随手填的。
4.2 用历史数据做回测:小样本验证过滤效果
课设加分项是自己跑一轮历史回测:拿最近 N 期开奖数据,对每一期统计“过滤后的池子里有没有这期的开奖号码、池子缩水到什么规模”。回测代码:
static void RunBacktest(List<NumResult> history, FilterOptions opt, int startIndex) { int hit = 0, total = 0; for (int i = startIndex; i < history.Count; i++) { var target = history[i].Reds; var pool = RunFilter(GenerateCombinations(33, 6), opt); total++; if (pool.Any(x => x.SequenceEqual(target))) hit++; Console.WriteLine($"第{i}期:剩余{pool.Count}组,命中{(pool.Any(x => x.SequenceEqual(target)) ? "是" : "否")},缩水比{pool.Count / 17721088.0:P2}"); } Console.WriteLine($"回测命中率:{hit * 100.0 / total:F2}%"); }逻辑说明:回测循环里每一期都用相同过滤参数生成候选池,然后用 SequenceEqual 判断该期实际开奖号码是否在池内。这里有两个细节:一是 GenerateCombinations(33, 6) 每期都重跑,运行较慢,优化做法是提前生成一次全组合缓存成 List,后续每期只做过滤;二是 SequenceEqual 要求两边序列顺序一致,所以 target 和 combo 都必须先 OrderBy 升序。实际命中率通常不会比随机乱选高太多,写进文档时千万不要把它包装成“预测成功率”,那既不诚实也容易被答辩老师追问到翻车。
参数说明:startIndex 表示从第几期开始回测,比如历史共 200 期,从 100 期开始留 100 期做验证集。缩水比用当前剩余组数除以全组合 1772 万,这个指标比单纯看剩余组数更能直观体现过滤强度。
4.3 大乐透前后区分开过滤,其实是两个独立系统
大乐透前区和后区的组合完全独立,在算法层面必须拆成两个管道。前区 35 选 5,全组合 324,632 组,过滤后剩几百组;后区 12 选 2,全组合只有 66 组,基本不需要缩水,只需要按最近开奖热冷号做一个排序。把两个管道合并成一个大乐透全组合,也就是约 2142 万注再过滤,内存和时间直接翻车,前面章节说的组合数计算在这里就是预警。
实操时一般把大乐透过滤写成两个方法:FilterFrontRegion 和 FilterBackRegion,前者返回 List<int[]>,后者返回排序后的分数列表,最后用双层循环把前区和后区拼接成完整一注。这样设计的好处是,后区的 66 种组合可以直接在内存里全量枚举,回测命中判定也更简单。课设报告里把这两个方法分开写,流程图和代码都会清晰很多。
5. 避坑指南:C# 实现过滤算法的四个常见翻车点
5.1 蓝球混进红球数组,排序后号码直接无效
现象:程序跑出来的结果里,有些注的红球列表出现 07、12、25、33、16、08,其中 16 明显是蓝球被当成红球输出,有些组合甚至红蓝重号,但肉眼看一遍不一定能发现。
原因:数据模型里把红球和蓝球合并成一个大数组统一排序,过滤逻辑没有区分区域,导致蓝球参与跨度、区间分布、去重等计算。
解决:维护数据模型时红蓝必须分开存储。Reds 数组只存放红球,Blue 字段单列;输出前分别排序,先生成红球升序字符串,再拼接蓝球。这个改动很小,但能避免一整轮“号码无效”的返工。大乐透同理,前区 5 个号、后区 2 个号各占一个数组。
5.2 遗漏统计“持续未出期数 vs 间隔期数”口径打架
现象:同一份历史数据,两个人写出的遗漏值差别很大,一个统计“这个号码连续多少期没出”,另一个统计“上次开出到现在的间隔期数”,回测结果对不上,答辩时各执一词。
原因:遗漏在数据分析里有两个口径:连续未出期数和间隔期数。两者在号码刚出现的那一期就相差 1,累积期数越多误差越大。
解决:在工程里统一定义遗漏值等于当前连续未出现期数,即遍历历史数据时数最近一次出现之后还有多少期没出。统计函数单独收敛在一个 OmittedValueAnalyzer 类里,UI 上只显示一个口径,不要两个口径并存。如果文档里想提到间隔,也只写一种定义并标明口径,否则老师一问“你这里遗漏为什么和别人不一样”就解释不清。
5.3 全组合直接进 List,运行内存暴涨卡死
现象:按网络教程把 1700 万组合全部 Add 进 List,程序启动后内存飙到 1GB 以上,UI 卡死,点击过滤按钮十几秒没反应。
原因:错误地把全组合枚举和全组合存储混为一谈。双色球全组合 1772 万注,如果每注存 6 个 int,内存轻松上 GB;正确做法是边生成边过滤,只保留通过条件的少量结果。
解决:生成器使用 yield return 惰性枚举,过滤流水线直接吃 IEnumerable<int[]>,只在末尾 Add 到 List。大乐透场景下把前区和后区分开生成,不生成二者的笛卡尔全组合。如果机器配置差,还可以把全组合改成随机采样 50 万组,效果相近但内存占用稳定。
5.4 历史数据 CSV 读取乱码,过滤全废
现象:从网页复制开奖历史保存成 .csv,程序读取后弹出一堆“������”,期号和号码全是乱码,过滤自然无法进行。
原因:网页源码通常是 UTF-8,而老版本 Excel 保存 CSV 默认是 GB2312/GBK,StreamReader 用 Encoding.UTF8 读 GBK 文件就乱;反过来用 Default 读严格 UTF-8 文件也会乱。
解决:读取方法先读文件头三个字节判断 BOM,有 UTF-8 BOM 就用 UTF-8,否则尝试 GBK。更省事的做法是把数据文件统一转存为带 BOM 的 UTF-8,然后在代码里强制用 Encoding.UTF8 读取。我一般还会在 LoadHistory 里 catch UnauthorizedAccessException 和 IOException,避免课设现场演示时弹异常窗口。这个坑看起来小,但它在数据准备阶段就拦住你,属于最恶心的那种。
6. 进阶技巧:给过滤结果加一个热冷号加权模块
基础过滤已经能缩水,但课设想拿高分,通常要加一个热冷号分析模块。它的作用不是继续过滤,而是给候选池里的号码排序:统计每个号码在历史最近 N 期的出现频率,按频率把号码分成热号、冷号、温号三类,再为每个候选号组打分,包含热号的组合加分、包含冷号的组合减分,最后按分数从高到低输出。
static Dictionary<int, int> CountFrequency(List<NumResult> history, int recentN) { var freq = new Dictionary<int, int>(); var last = history.Skip(Math.Max(0, history.Count - recentN)); foreach (var rec in last) foreach (var v in rec.Reds) freq[v] = freq.GetValueOrDefault(v) + 1; return freq; }代码说明:CountFrequency 统计最近 recentN 期里每个红球出现次数,返回字典。这里的红球频率是一个 33 维的小直方图,直接排序就能得到热号列表。参数 recentN 常见取 50 或 100,太小会抖动,太大则热冷号特征不明显。
打分代码更简单:
static int ScoreCombo(int[] combo, Dictionary<int, int> freq, int hotThreshold, int coldThreshold) { int score = 0; foreach (var v in combo) { int count = freq.GetValueOrDefault(v, 0); if (count >= hotThreshold) score += 1; else if (count <= coldThreshold) score -= 1; } return score; }逻辑说明:score 初始为 0,热门号码加 1,冷门号码减 1,最终得分越高的组合在统计意义上越接近历史开奖结构,按分数倒序输出。这个模块不新增过滤条件,因此不会把候选池越缩越小,也不会因为条件过严滤空。热冷号阈值怎么定:按历史 N 期的平均频率加减 1 个标准差划分,平均频率等于每期号码数 × N / 号码池大小,频率不低于平均值加标准差就是热号,不高于平均值减标准差就是冷号,其余温号。这两段代码放一起,加上前面第 4 章的参数表,就能串成一个完整的“统计→生成→过滤→排序→输出”闭环。
说个我自己的教训:第一次提交前为了展示效果,我把过滤条件加到最严,结果候选池只剩 20 组,回测命中率为零,演示当场翻车。从那以后我每次改参数都强制走一遍固定随机种子的回测,先在后台看缩水比和命中情况,再决定要不要上 UI。这套工程里的完整源码和参数文档,就是按这个流程整理的,如果你课设正卡在过滤参数或回测逻辑上,可以直接拿去做对照改。希望这些内容能帮你少走几步弯路,把过滤逻辑写清楚、参数口径写明白,这门课设基本就立住了。
本文还有配套的精品资源,点击获取