3 条题解

  • 0
    @ 2026-6-5 9:37:55

    太棒了!自己动手编写数据生成器是算法进阶的必经之路。

    对于这道题,需要特别向你说明一点:因为所有的正常解法时间复杂度都是 O(1)O(1),所以我们无法通过扩大数据范围来“卡”时间复杂度。这道题的测试数据设计的核心完全在于【逻辑分支的全面覆盖】与【边界陷阱(Corner Cases)】。

    初学者做这道题最容易犯的错误就是把闰年规则写错(比如只判断了 % 4,漏掉了 % 100 的特判)。因此,我们的数据必须狠狠地揪出这种逻辑漏洞!

    下面为你精心设计的 10 个测试点覆盖策略以及完整的 C++ 数据生成器代码。

    核心设计思路(10 个测试点分布)

    1. 测试点 1 (2022 1):样例1。测试基础的大月(31天)。
    2. 测试点 2 (2020 2):样例2。普通的闰年 2 月(能被4整除,不能被100整除),输出 29。
    3. 测试点 3 (2023 4):测试基础的小月(30天)。
    4. 测试点 4 (2100 2)【核心杀手数据】“百年不闰”陷阱!能被4整除但也能被100整除(且不能被400整除),这是平年,输出 28。用来卡掉只写了 A % 4 == 0 的错解。
    5. 测试点 5 (2000 2)【核心杀手数据】“四百年再闰”边界!能被400整除,这是闰年,输出 29。用来卡掉写了 A % 100 == 0 就全部当成平年的错解。
    6. 测试点 6 (2021 2):最普通的平年 2 月,输出 28。
    7. 测试点 7 (2024 8):测试 8 月(容易出错的地方,因为 7 月和 8 月连续两个大月),输出 31。
    8. 测试点 8 (3000 2)最大年份边界。3000 是百年不闰的平年,输出 28。
    9. 测试点 9 (2996 2):接近最大边界的普通闰年,输出 29。
    10. 测试点 10 (2500 12):普通大月测试,且包含 12 月边界。

    数据生成器完整 C++ 代码 (generator.cpp)

    请将以下代码保存为 generator.cpp,并在本地编译运行。它会在同级目录下瞬间生成 1.in ~ 10.in1.out ~ 10.out 共 20 个文件。

    #include <iostream>
    #include <fstream>
    #include <string>
    #include <vector>
    
    using namespace std;
    
    // 标程函数:使用最优的“查表法”生成标准答案
    int solve(int A, int B) {
        const int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        // 闰年特判
        if (B == 2 && ((A % 4 == 0 && A % 100 != 0) || (A % 400 == 0))) {
            return 29;
        }
        // 非闰年2月或其他月份直接查表
        return days[B];
    }
    
    int main() {
        // 优化系统 IO 速度
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        // 存储精选的 10 个测试用例,格式为 {年份A, 月份B}
        // 使用 struct 或者 pair 都可以,这里用简单的 struct 方便阅读
        struct TestCase {
            int year;
            int month;
        };
    
        vector<TestCase> test_cases = {
            {2022, 1},  // Test 1: 样例 1,普通大月
            {2020, 2},  // Test 2: 样例 2,普通闰年 2 月
            {2023, 4},  // Test 3: 普通小月
            {2100, 2},  // Test 4: 杀手数据!百年不闰(平年,28天)
            {2000, 2},  // Test 5: 杀手数据!四百年再闰(闰年,29天),年份下限边界
            {2021, 2},  // Test 6: 普通平年 2 月
            {2024, 8},  // Test 7: 连续大月测试(8月是31天)
            {3000, 2},  // Test 8: 年份上限边界,百年不闰(平年,28天)
            {2996, 2},  // Test 9: 接近上限的普通闰年
            {2500, 12}  // Test 10: 年终月份边界(12月)
        };
    
        // 循环生成 10 组测试数据
        for (int i = 0; i < test_cases.size(); ++i) {
            int A = test_cases[i].year;
            int B = test_cases[i].month;
            
            // --- 1. 生成 .in 文件 ---
            string in_filename = to_string(i + 1) + ".in";
            ofstream fout_in(in_filename);
            if (!fout_in.is_open()) {
                cerr << "无法创建文件: " << in_filename << "\n";
                continue;
            }
            // 注意格式,用空格隔开,末尾换行
            fout_in << A << " " << B << "\n";
            fout_in.close();
    
            // --- 2. 调用标程计算正确答案 ---
            int ans = solve(A, B);
    
            // --- 3. 生成 .out 文件 ---
            string out_filename = to_string(i + 1) + ".out";
            ofstream fout_out(out_filename);
            if (!fout_out.is_open()) {
                cerr << "无法创建文件: " << out_filename << "\n";
                continue;
            }
            fout_out << ans << "\n";
            fout_out.close();
    
            // 终端输出生成日志,方便核对
            cout << "成功生成 Test " << (i + 1) << "\t"
                 << "| 输入: " << A << " " << B << "\t"
                 << "| 答案: " << ans << "\n";
        }
    
        cout << "\n所有测试数据生成完毕!文件总大小不足 100 Bytes,OJ系统可极速读取。" << endl;
        return 0;
    }
    

    OJ 部署建议与总结

    1. 防爆栈/防除零/防超时:对于这道题目而言,因为无任何递归调用、没有除法(仅有取模 %)、时间复杂度绝对恒定,所以原生的测试数据生成是绝对安全的。
    2. 考察区分度:把这组数据放到 OJ 后,你会发现:
      • 那些没有判断 % 100 的同学,会在 Test 4Test 8 拿到 Wrong Answer (WA)。
      • 那些把整除条件写混淆,没有正确组合 &&|| 优先级的同学,会在 Test 5 拿到 WA。
      • 靠背诵“135781012”口诀但是把 8 月遗漏的同学,会在 Test 7 栽跟头。
    3. 文件大小:单组数据输入输出极其精简(一行几个字符),所有的 .in.out 文件加在一起通常不到 100 字节,远小于 2MB,符合最高标准的轻量化测试需求。
    • 0
      @ 2026-6-5 9:36:48

      你好!作为你的教练,我很高兴看到你准备好将刚才的思路转化为真正的代码了。

      这道题非常特殊,因为它不存在循环,所以无论你怎么写,时间复杂度都是 O(1)O(1) 级别的(只需要运行几步就结束了)。因此,我们这道题的演进过程,不是“算法时间维度的优化”,而是**“代码编写逻辑的优化”(从臃肿容易出错,演进到极简优雅)**。

      下面为你展示三个版本的完整可运行代码,严格遵循 NOIP/CSP 的 C++14 标准。


      版本一:新手直觉版 —— 穷举所有的 if-else

      刚刚接触编程的同学,最容易想到的就是把 1 到 12 月挨个判断一遍。这种写法逻辑最直白,但也最长。

      #include <iostream>
      
      using namespace std;
      
      int main() {
          // 优化输入输出速度
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int A, B;
          // 易错点1:题目说先输入A(年),再输入B(月)。千万别写反了!
          cin >> A >> B;
      
          // 枚举所有的月份
          if (B == 1) {
              cout << 31 << "\n";
          } else if (B == 2) {
              // 关键点:2月份需要判断是否为闰年
              // 闰年判断口诀:四年一闰且百年不闰,或四百年再闰
              // 易错点2:注意逻辑运算符 && (且) 和 || (或) 的组合,最好用括号括起来明确优先级
              if ((A % 4 == 0 && A % 100 != 0) || (A % 400 == 0)) {
                  cout << 29 << "\n";
              } else {
                  cout << 28 << "\n";
              }
          } else if (B == 3) {
              cout << 31 << "\n";
          } else if (B == 4) {
              cout << 30 << "\n";
          } else if (B == 5) {
              cout << 31 << "\n";
          } else if (B == 6) {
              cout << 30 << "\n";
          } else if (B == 7) {
              cout << 31 << "\n";
          } else if (B == 8) {
              cout << 31 << "\n";
          } else if (B == 9) {
              cout << 30 << "\n";
          } else if (B == 10) {
              cout << 31 << "\n";
          } else if (B == 11) {
              cout << 30 << "\n";
          } else if (B == 12) {
              cout << 31 << "\n";
          }
      
          return 0;
      }
      

      【复杂度与思考】

      • 时间复杂度O(1)O(1)。虽然代码很长,但程序只会选择其中一条分支执行,最多做十来次判断,属于常数时间。
      • 空间复杂度O(1)O(1)。只用了 AB 两个变量。
      • 代码优化建议:在考场上,手敲这么长的代码不仅浪费时间,而且极其容易因为复制粘贴(Ctrl+C/V)忘记修改数字,导致莫名其妙的低级丢分(比如把 11月错敲成 31天)。我们需要合并同类项。

      版本二:逻辑合并版 —— 分组判断法

      把有相同特征的月份用逻辑或(||)连接起来,大大缩减了代码的行数。

      #include <iostream>
      
      using namespace std;
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int A, B;
          cin >> A >> B;
      
          // 先把闰年的判断单独抽出来,让逻辑更清晰
          // is_leap 为 true 代表是闰年,false 代表是平年
          bool is_leap = ((A % 4 == 0 && A % 100 != 0) || (A % 400 == 0));
      
          // 组别1:所有的“大月”(31天)
          if (B == 1 || B == 3 || B == 5 || B == 7 || B == 8 || B == 10 || B == 12) {
              cout << 31 << "\n";
          } 
          // 组别2:所有的“小月”(30天)
          else if (B == 4 || B == 6 || B == 9 || B == 11) {
              cout << 30 << "\n";
          } 
          // 组别3:特殊的2月
          else if (B == 2) {
              if (is_leap) {
                  cout << 29 << "\n";
              } else {
                  cout << 28 << "\n";
              }
          }
      
          return 0;
      }
      

      【复杂度与思考】

      • 时间复杂度O(1)O(1)
      • 空间复杂度O(1)O(1)。多用了一个布尔变量 is_leap,依然是常数级别。
      • 代码优化建议:这段代码已经非常清爽了,也是大部分同学能写出的标准答案。但在真正的竞赛高手眼里,写一长串 B == x || B == y 依然不够优雅。如果有一种方法可以“直接去抽屉里拿答案”就好了。

      版本三:竞赛标答版 —— “打表法”(数组查表)极简最优

      在算法竞赛中,遇到这种**“输入一个小范围的整数,输出一个固定的对应值”的题目,最强大的武器就是数组打表**(Look-up Table)。我们把月份的天数预先存在数组里,把复杂的条件分支转化为直接的数组访问。

      #include <iostream>
      
      using namespace std;
      
      // 关键点1:将月份的天数预埋在数组中。
      // 数组大小开 13,是为了让下标 1~12 刚好对应 1月~12月,下标 0 的位置废弃不用(填0即可)。
      // 这种技巧叫 "1-based indexing"(1-based 索引),能避免脑子绕弯计算下标。
      const int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int A, B;
          cin >> A >> B;
      
          // 关键点2:优雅地判断闰年。
          // 如果是闰年且月份是2月,说明需要特判输出 29
          if (B == 2 && ((A % 4 == 0 && A % 100 != 0) || (A % 400 == 0))) {
              cout << 29 << "\n";
          } else {
              // 其他所有情况(包括平年的2月,以及所有的1、3~12月),直接从数组中取答案!
              cout << days[B] << "\n";
          }
      
          return 0;
      }
      

      【复杂度与思考】

      • 时间复杂度:绝对的 O(1)O(1)。没有任何冗长的 if-else 跳转逻辑,直接读取内存地址,这是 CPU 执行最快的方式。
      • 空间复杂度O(1)O(1)。使用了一个长度为 13 的整型数组,占用 13×4=5213 \times 4 = 52 字节内存,微乎其微。
      • 教练点评:版本三是你在赛场上追求的终极写法!它不仅代码量最少,出错概率也极低(你只需要核对一遍数组里的数字对不对即可)。**打表思维(空间换时间 / 空间换代码量)**是贯穿整个 OI 学习生涯的核心思想之一。以后的复杂搜索题、动态规划题的边界初始化,都会大量运用到这种思维。

      赶紧在你的电脑上把第三个版本敲一遍,体会一下“查表”带来的清爽感吧!有什么不懂的随时问我。

      • 0
        @ 2026-6-5 9:35:19

        你好!很高兴再次以教练的身份指导你。这道题是一道非常经典的条件分支和逻辑判断入门题,也是以后做复杂“日期模拟类”题目的基石。我们一步步来拆解。


        1. 教练的思路提示(不提供完整代码)

        • 提示一:分类讨论。一年有12个月,大部分月份的天数是固定的。你可以先试着把它们分成几类?比如“总是31天的”、“总是30天的”,还有“特殊的”。
        • 提示二:揪出“特殊月份”。几月份的天数是会变化的?没错,就是2月。2月的天数完全取决于当前年份是平年还是闰年。
        • 提示三:闰年的判定法则。还记得小学数学里怎么判断闰年吗?口诀是:“四年一闰,百年不闰,四百年再闰”。在编程里,这句话要如何用“求余数(%)”和“逻辑与(&&)、逻辑或(||)”翻译出来?

        2. 预备知识点总结

        要完美拿下这道题,你需要掌握以下几个知识点:

        1. 常识储备:每个月对应的天数,以及闰年的精确判断规则。
        2. 分支控制语句:熟练使用 if - else if - else 或者 switch - case 语句进行多条件分支判断。
        3. 逻辑运算符:熟练掌握 &&(逻辑与,表示“并且”)、||(逻辑或,表示“或者”)、!=(不等于)。
        4. 取模运算:使用 % 判断一个数能否被另一个数整除(比如 A % 4 == 0)。

        3. 启发式与图形式的草稿纸推导(教学模拟)

        “来,拿出草稿纸。我们不要急着写代码,先把关于月份的逻辑画个清晰的图表。”

        第一步:画图给月份分组(寻找规律)

        教练启发:“如果让你写很多个 if 去判断1到12月,代码会又长又容易错。我们把相同的月份合并起来吧。”

        [草稿纸推导] 月份分组
        
          大月 (31天)  : 1, 3, 5, 7, 8, 10, 12  (口诀: 一三五七八十腊,三十一天永不差)
          小月 (30天)  : 4, 6, 9, 11
          特殊月(??天) : 2
        

        第二步:攻克2月(画判断流程图)

        教练启发:“只有在输入的月份 B=2B = 2 的时候,我们才需要关心年份 AA。我们用流程图来表示闰年规则:”

        [草稿纸推导] 闰年判断逻辑树 (判断年份 A)
        
                     [A 能被 400 整除吗?]
                        /           \
                    是(Yes)         否(No)
                     /                \
                【闰年! 29天】     [A 能被 100 整除吗?]
                                      /           \
                                  是(Yes)         否(No)
                                   /                \
                             【平年! 28天】     [A 能被 4 整除吗?]
                                                    /        \
                                                是(Yes)      否(No)
                                                 /             \
                                           【闰年! 29天】  【平年! 28天】
        

        教练提问:“你能把这棵树合并成一行逻辑表达式吗?” 学生思考后写出:“是闰年的条件:(A 能被 4 整除 并且 A 不能被 100 整除) 或者 (A 能被 400 整除)。”

        第三步:复杂度的思考与“代码优雅度”优化

        • 空间复杂度
          • 我们只需要两个变量 AABB 来接收输入。不论数据多少,变量数量恒定。空间复杂度为 O(1)O(1)
        • 时间复杂度
          • 程序只需要进行两三步 if-else 判断或简单的算术运算。没有循环结构(没有 forwhile)。时间复杂度为 O(1)O(1),这也是计算机执行最快的时间复杂度。
        • 代码层面的“优化”建议(进阶技巧)
          • 既然时间已经是最优的 O(1)O(1) 了,那怎么优化呢?优化代码的简洁度
          • 初学者可能会写一堆 if (B == 1 || B == 3 || B == 5 ...)
          • 教练支招(打表法):在遇到固定的映射关系时,可以用一个数组来存天数! 比如定义:int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 这样一来,只要判断是闰年且 B=2B=2,我们就输出 days[2] + 1,否则直接输出 days[B]。这叫“空间换时间/空间换代码量”,是极其优秀的竞赛思维!

        4. 读题时的“题眼”(关键词)提取技巧

        处理此类题目,抓住以下几个关键词就能瞬间破题:

        1. “平年和闰年” —— 题眼:这是题目在明示你核心考察点!暗示你必须动用 % 4% 100% 400 这三个关键除数来做判断。
        2. 2000A30002000 \le A \le 30001B121 \le B \le 12 —— 题眼
          • 年份 AA 的范围只有 1000 年,且在公元后,说明不仅不会遇到负数年份,也不会超出普通整型 int 的范围。
          • A2000A \ge 2000 意味着测试数据里肯定包含 A=2000A=2000(能被400整除)和 A=2100A=2100(能被4整除但不能被100整除的平年陷阱)这种关键边界数据。
        3. “一个日期的年、月” —— 题眼:注意输入的顺序!先输入的是 AA(年),后输入的是 BB(月)。在写 cin >> A >> B; 时千万别写反了,这是考场上因为粗心最容易丢分的地方。

        现在,顺着流程图和打表法的思路,去草稿纸上试着把核心的几行代码写出来吧!理清了再上机敲,会有事半功倍的效果。

        • 1

        信息

        ID
        13918
        时间
        1000ms
        内存
        128MiB
        难度
        1
        标签
        递交数
        7
        已通过
        4
        上传者