Skip to content

第 37 课:日期模拟 ​

为了使自己的身体更健康,蛙蛙对自己的生活习惯做了全方位的改善,其中就包含了一个全年的健康饮食计划,他计划从 1 月 1 日开始,一直持续到第 d 天。

蛙蛙想清楚地知道在计划执行到第 d 天的时候是几月几日,以便提前准备相应的食材,合理安排饮食。他想到了通过程序进行计算,但是关于日期方面的知识蛙蛙了解甚少,于是他又去请教了氪町博士。

氪町博士告诉他,蛙蛙的问题被称作日期模拟算法,这种算法经常会涉及以下问题:

  1. 到某年某月的天数;

  2. 判断给定日期的合法性;

  3. 给定年份,求这一年第 n 天的日期;

  4. 给定年、月、日,求经过 n 天后的日期;

  5. 查找两个日期之间有多少个回文日期。

37.1 基础模板 ​

得到某年某月的天数 ​

得到某年某月的天数问题经常会作为其他问题的模板来使用。首先我们需要存储一年中所有月份相对应的天数,操作如下:

cpp
int day[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

其中 day[0] 用不到,这里置为 0 天,day[1] 表示一月份共有 31 天,2 月份先赋值为 28 天,具体天数还需要检测年份是不是闰年,如果是闰年还需要加 1 天。

闰年判断 ​

闰年分为普通闰年和世纪闰年。

  • 普通闰年:年份是 4 的倍数,但不是 100 的倍数,例如 2004、2020 年等。
  • 世纪闰年:是 400 的倍数,例如 1900 不是世纪闰年,2000 是世纪闰年。

假设 year 表示年份,操作如下:

cpp
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
    cout << "是闰年";
else
    cout << "不是闰年";

其中 year % 4 == 0 && year % 100 != 0 判断当年是否为普通闰年,若满足则为普通闰年,year % 400 == 0 判断当年是否为世纪闰年,若满足则为世纪闰年。两者满足其一均为闰年。

二月特判 ​

当月份不是 2 月时,就返回当前月份的初始天数。

当月份是 2 月时,才需要判断年份是不是闰年。当年份为闰年时,2 月份天数需要加 1 天,不成立时则不需要加。

cpp
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int check(int y, int m) // 判断是不是 2 月,y 表示年份,m 表示月份
{
    if (m != 2)
        return days[m]; // 不是 2 月,返回当前月份的初始天数
    else                // 是 2 月,则需要判断当前年份是不是闰年
    {
        int leap = (y % 4 == 0 && y % 100 != 0 || y % 400 == 0);
        return days[m] + leap; // 是闰年 leap 为 1,不是闰年 leap 为 0
    }
}

判断日期是否合法 ​

一般这样的问题会先给你一个格式,看是否符合这个格式,比如要满足 20251201 这样的格式才是合法的。

首先你需要依次分离出年份、月份、日,然后分别判断年、月、日是否符合题意。

  1. 判断月份:若月份不在 1 月和 12 月之间(包含 1 月和 12 月),则月份不符合。具体语句如下:
cpp
if (m < 1 || m > 12)
    cout << "月份不符合";
  1. 判断日:若当月只有 30 天,但日期达到了 31 日,则日不符合。具体语句如下:
cpp
if (d < 1 || d > (指定月份的总天数))
    cout << "日不符合";

注:年份一般不需要检查,但也要视题意而定。

37.2 实例讲解 ​

例 1:第 n 天的日期 ​

给定一个年份 y 和一个整数 d,问:这一年的第 d 天是几月几日?注意闰年的 2 月有 29 天。满足下面条件之一的是闰年:

  1. 年份是 4 的整数倍,而且不是 100 的整数倍;

  2. 年份是 400 的整数倍。

【输入格式】输入两行。

第 1 行包含一个整数 y,表示年份,年份在 1900 和 2025 之间(包含 1900 和 2025)。

第 2 行包含一个整数 d,d 在 1 和 365 之间。

【输出格式】输出两行,每行一个整数,分别表示答案的月份和日期。

【输入样例】

2015
80

【输出样例】

3
21

解析

题目要求根据输入的年份 y 和该年份中的第 d 天,计算并输出对应的月份和日期。求解过程中需要先判断输入的年份是否为闰年,因为闰年的 2 月有 29 天,平年的 2 月有 28 天,这会影响日期的计算。然后,通过逐月减去每个月的天数,直到剩余天数小于 0 或者等于 0,从而确定具体的月份和日期。

样例分析

输入样例:2015 80。

2015 年不是闰年,2 月有 28 天。

1 月有 31 天,减去 31 天后,n 变为 49。

2 月有 28 天,再减去 28 天后,n 变为 21。

3 月时,n 减去 31 后小于 0,此时输出月份为 3,日期为 31 + (-10) = 21。

所以输出为 3 21。

  1. 先定义一个长度为 13 的整型数组 day,用于存储每个月的天数,数组的下标从 1 开始,对应 1 月到 12 月。初始时,2 月的天数设为 0,后续根据年份是否为闰年进行赋值,代码如下:
cpp
int day[13] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  1. 定义 check 函数:使用该函数接收一个整数 n 作为参数,表示年份。

a. 闰年的判断规则:年份是 400 的整数倍,或者年份是 4 的整数倍但不是 100 的整数倍,则该年份为闰年。

b. 如果满足闰年条件,函数返回 true,否则返回 false。

代码如下:

cpp
bool check(int n) // 判断闰年
{
    if (n % 400 == 0 || (n % 4 == 0 && n % 100 != 0))
        return true;
    return false;
}
  1. 主函数内求目标日期。

a. 先使用 for 循环从 1 月开始逐月减去每个月的天数。

b. 当 n 减去当前月份的天数后小于 0 时,说明目标日期在当前月份。此时,输出当前月份 j,并通过 day[j] + n 计算出目标日期。

c. 当 n 减去当前月份的天数等于 0 时,说明目标日期是当前月份的最后一天。此时,输出当前月份 j 和该月的天数 day[j]。

d. 无论哪种情况,一旦找到目标日期,就使用 break 语句跳出循环。

参考代码

cpp
#include <iostream>
using namespace std;
int day[13] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool check(int n) // 判断闰年
{
    if (n % 400 == 0 || (n % 4 == 0 && n % 100 != 0))
        return true;
    return false;
}
int main()
{
    int m, n;
    cin >> m >> n;
    if (check(m))
        day[2] = 29;
    else
        day[2] = 28;
    for (int j = 1; j <= 12; j++)
    {
        n -= day[j];
        if (n < 0)
        {
            cout << j << endl << day[j] + n;
            break;
        }
        if (n == 0)
        {
            cout << j << endl << day[j];
            break;
        }
    }
    return 0;
}

例 2:日期距离 ​

输入一个日期,输出它和 2025 年 5 月 17 日相差多少天。注意闰年的 2 月有 29 天。满足下面条件之一的是闰年:

  1. 年份是 4 的整数倍,而且不是 100 的整数倍;

  2. 年份是 400 的整数倍。

【输入格式】输入三行。

第 1 行包含一个整数 y,表示年份,年份在 1 和 2025 之间(包含 1 和 2025)。

第 2 行包含一个整数 m,表示月份,m 在 1 和 12 之间(包含 1 和 12)。

第 3 行包含一个正整数 d,表示日期,d 在 1 和 31 之间(包含 1 和 31)。

【输出格式】输出一行,为两个日期之间相差的天数。

【输入样例】

1988
7
3

【输出样例】

13467

解析

样例分析

输入样例:1988 7 3。

转化成问题是计算输入的日期(1988 年 7 月 3 日)与 2025 年 5 月 17 日之间相差的天数。

  1. 先计算 1988 年之后到 2025 年之前完整年份的天数:从 1989 年开始到 2024 年,这期间每一年要么是 365 天(平年),要么是 366 天(闰年)。

a. 1989 年不是闰年,有 365 天;

b. 1990 年不是闰年,有 365 天;

c. 1991 年不是闰年,有 365 天;

d. 1992 年是闰年,有 366 天;

e. ……

以此类推,按照闰年和平年的规则逐年计算天数并累加,得到这期间所有完整年份的总天数为 13149 天。

  1. 计算 2025 年 1 月 1 日到 5 月 17 日的天数:

a. 1 月有 31 天;

b. 2025 年不是闰年,2 月有 28 天;

c. 3 月有 31 天;

d. 4 月有 30 天;

e. 5 月到 17 日有 17 天。

将这些天数相加,得到 2025 年 1 月 1 日到 5 月 17 日的总天数为 31 + 28 + 31 + 30 + 17 = 137 天。

  1. 计算 1988 年 7 月 3 日之后当年剩余的天数:

a. 7 月总共有 31 天,已经过了 3 天,所以 7 月还剩 31 - 3 = 28 天;

b. 8 月有 31 天;

c. 9 月有 30 天;

d. 10 月有 31 天;

e. 11 月有 30 天;

f. 12 月有 31 天。

将 7 月剩余天数以及 8 月到 12 月的天数相加,得到 1988 年 7 月 3 日之后当年剩余的天数为 181 天。

  1. 计算总天数差:1989 年到 2024 年整年份共 13149 天,2025 年 1 月 1 日到 5 月 17 日共 137 天,1988 年 7 月 3 日之后当年剩余的天数为 181 天,总共 13467 天。

参考代码

cpp
#include <iostream>
using namespace std;
int day[13] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool check(int k) // 判断闰年
{
    if (k % 400 == 0 || (k % 4 == 0 && k % 100 != 0))
        return true;
    return false;
}
int sum; // 计算总天数
int main()
{
    int y, m, d;
    cin >> y >> m >> d;
    if (check(y))
        day[2] = 29;
    else
        day[2] = 28;
    for (int i = y + 1; i < 2025; i++) // 计算完整年份的天数
    {
        if (check(i))
            sum += 366;
        else
            sum += 365;
    }
    if (y == 2025)
    {
        for (int i = m + 1; i < 5; i++) // 计算完整月份的天数
            sum += day[i];
        if (m == 5)
            sum += 17 - d;
        else
            sum += day[m] - d + 17;
    }
    else
    {
        sum += 137;                    // 2025 年 1 月 1 日到 2025 年 5 月 17 日的总天数
        for (int i = m + 1; i <= 12; i++) // 计算完整月份的天数
            sum += day[i];
        sum += day[m] - d;
    }
    cout << sum << endl;
    return 0;
}

例 3:每日天数 ​

计算从 1900 年 1 月 1 日开始,到 1900 + n − 1 年 12 月 31 日,每个月的 13 号是星期六、星期日、星期一、星期二、星期三、星期四以及星期五的次数分别是多少。

注意

  1. 1900 年 1 月 1 日是星期一;

  2. 闰年 2 月有 29 天,平年 2 月有 28 天,闰年是指年份是 4 的倍数但不是 100 的倍数,或者是 400 的倍数的年;

  3. 输出从星期六开始。

【输入格式】输入一行,一个正整数 n,1 ≤ n ≤ 10000。

【输出格式】输出一行,表示每个月的 13 号是星期六、星期日、星期一、星期二、星期三、星期四以及星期五的次数分别是多少。

【输入样例】

20

【输出样例】

36 33 34 33 35 35 34

解析

  1. 年份遍历:通过 for 循环遍历从 1900 年到 1900 + n − 1 年的每一年,对于每一年,调用 check 函数判断是否为闰年,若为闰年,则将 day[2] 设为 29,否则设为 28。

  2. 月份遍历

a. 内层 for 循环遍历每一年的 12 个月。

b. 通过 sum += day[j - 1]; 将前一个月的总天数累加到 sum 中。

c. 通过 1 + (sum + 12) % 7 计算当前月 13 号是星期几。(sum + 12) % 7 得出的结果范围是 0~6,其中 0 代表星期日,1~6 分别代表星期一到星期六。先取模再加 1 是为了避免出现星期 0,将结果映射到 1~7。然后将对应星期几的计数器加 1,即 ans[1 + (sum + 12) % 7]++;。

  1. 12 月处理:通过 sum += day[12]; 在处理完一年的 12 个月后,将 12 月的天数累加到 sum 中。

  2. 输出结果:按照题目要求,从星期六开始输出每个星期几对应的 13 号出现的次数,即 ans[6](星期六)、ans[7](星期日)、ans[1](星期一)、ans[2](星期二)、ans[3](星期三)、ans[4](星期四)、ans[5](星期五)。

参考代码

cpp
#include <iostream>
using namespace std;
int day[13] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int ans[8]; // 求结果
int sum;    // 求总天数
bool check(int n) // 判断闰年
{
    if (n % 400 == 0 || (n % 4 == 0 && n % 100 != 0))
        return true;
    return false;
}
int main()
{
    int n;
    cin >> n;
    for (int i = 1900; i <= 1900 + n - 1; i++)
    {
        if (check(i))
            day[2] = 29;
        else
            day[2] = 28;
        for (int j = 1; j <= 12; j++)
        {
            sum += day[j - 1];         // 加上前一个月全部的天数
            ans[1 + (sum + 12) % 7]++; // 先求余后加 1 是为了防止星期 0 出现
        }
        sum += day[12]; // 12 月加上
    }
    cout << ans[6] << " " << ans[7] << " " << ans[1] << " " << ans[2] << " " << ans[3]
         << " " << ans[4] << " " << ans[5];
    return 0;
}

基于 MIT 协议发布