第 37 课:日期模拟
为了使自己的身体更健康,蛙蛙对自己的生活习惯做了全方位的改善,其中就包含了一个全年的健康饮食计划,他计划从 1 月 1 日开始,一直持续到第 d 天。
蛙蛙想清楚地知道在计划执行到第 d 天的时候是几月几日,以便提前准备相应的食材,合理安排饮食。他想到了通过程序进行计算,但是关于日期方面的知识蛙蛙了解甚少,于是他又去请教了氪町博士。
氪町博士告诉他,蛙蛙的问题被称作日期模拟算法,这种算法经常会涉及以下问题:
到某年某月的天数;
判断给定日期的合法性;
给定年份,求这一年第 n 天的日期;
给定年、月、日,求经过 n 天后的日期;
查找两个日期之间有多少个回文日期。
37.1 基础模板
得到某年某月的天数
得到某年某月的天数问题经常会作为其他问题的模板来使用。首先我们需要存储一年中所有月份相对应的天数,操作如下:
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 表示年份,操作如下:
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 天,不成立时则不需要加。
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 月和 12 月之间(包含 1 月和 12 月),则月份不符合。具体语句如下:
if (m < 1 || m > 12)
cout << "月份不符合";- 判断日:若当月只有 30 天,但日期达到了 31 日,则日不符合。具体语句如下:
if (d < 1 || d > (指定月份的总天数))
cout << "日不符合";注:年份一般不需要检查,但也要视题意而定。
37.2 实例讲解
例 1:第 n 天的日期
给定一个年份 y 和一个整数 d,问:这一年的第 d 天是几月几日?注意闰年的 2 月有 29 天。满足下面条件之一的是闰年:
年份是 4 的整数倍,而且不是 100 的整数倍;
年份是 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。
- 先定义一个长度为 13 的整型数组 day,用于存储每个月的天数,数组的下标从 1 开始,对应 1 月到 12 月。初始时,2 月的天数设为 0,后续根据年份是否为闰年进行赋值,代码如下:
int day[13] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};- 定义 check 函数:使用该函数接收一个整数 n 作为参数,表示年份。
a. 闰年的判断规则:年份是 400 的整数倍,或者年份是 4 的整数倍但不是 100 的整数倍,则该年份为闰年。
b. 如果满足闰年条件,函数返回 true,否则返回 false。
代码如下:
bool check(int n) // 判断闰年
{
if (n % 400 == 0 || (n % 4 == 0 && n % 100 != 0))
return true;
return false;
}- 主函数内求目标日期。
a. 先使用 for 循环从 1 月开始逐月减去每个月的天数。
b. 当 n 减去当前月份的天数后小于 0 时,说明目标日期在当前月份。此时,输出当前月份 j,并通过 day[j] + n 计算出目标日期。
c. 当 n 减去当前月份的天数等于 0 时,说明目标日期是当前月份的最后一天。此时,输出当前月份 j 和该月的天数 day[j]。
d. 无论哪种情况,一旦找到目标日期,就使用 break 语句跳出循环。
参考代码
#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 天。满足下面条件之一的是闰年:
年份是 4 的整数倍,而且不是 100 的整数倍;
年份是 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 日之间相差的天数。
- 先计算 1988 年之后到 2025 年之前完整年份的天数:从 1989 年开始到 2024 年,这期间每一年要么是 365 天(平年),要么是 366 天(闰年)。
a. 1989 年不是闰年,有 365 天;
b. 1990 年不是闰年,有 365 天;
c. 1991 年不是闰年,有 365 天;
d. 1992 年是闰年,有 366 天;
e. ……
以此类推,按照闰年和平年的规则逐年计算天数并累加,得到这期间所有完整年份的总天数为 13149 天。
- 计算 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 天。
- 计算 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 天。
- 计算总天数差:1989 年到 2024 年整年份共 13149 天,2025 年 1 月 1 日到 5 月 17 日共 137 天,1988 年 7 月 3 日之后当年剩余的天数为 181 天,总共 13467 天。
参考代码
#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 号是星期六、星期日、星期一、星期二、星期三、星期四以及星期五的次数分别是多少。
注意
1900 年 1 月 1 日是星期一;
闰年 2 月有 29 天,平年 2 月有 28 天,闰年是指年份是 4 的倍数但不是 100 的倍数,或者是 400 的倍数的年;
输出从星期六开始。
【输入格式】输入一行,一个正整数 n,1 ≤ n ≤ 10000。
【输出格式】输出一行,表示每个月的 13 号是星期六、星期日、星期一、星期二、星期三、星期四以及星期五的次数分别是多少。
【输入样例】
20【输出样例】
36 33 34 33 35 35 34解析
年份遍历:通过 for 循环遍历从 1900 年到 1900 + n − 1 年的每一年,对于每一年,调用 check 函数判断是否为闰年,若为闰年,则将 day[2] 设为 29,否则设为 28。
月份遍历
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]++;。
12 月处理:通过
sum += day[12];在处理完一年的 12 个月后,将 12 月的天数累加到 sum 中。输出结果:按照题目要求,从星期六开始输出每个星期几对应的 13 号出现的次数,即 ans[6](星期六)、ans[7](星期日)、ans[1](星期一)、ans[2](星期二)、ans[3](星期三)、ans[4](星期四)、ans[5](星期五)。
参考代码
#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;
}