日期问题
日期问题通常涉及到暴力、枚举、模拟等算法,但是由于日期问题存在很强的规律性,同时日期问题通常会涉及到枚举、模拟两种算法,很难完全的去划分题型,所以专门创建一个日期问题的题型。
1.日期枚举
1.1 日期的枚举(不经过判断)
日期枚举就是根据给定的两个日期去枚举之间合法的日期。如下所示:
问题:请问从1900年1月1日至9999年12月31日,总共有多少天,年份的数位数字之和等于月的数位数字之和加日的数位数字之和。
针对上面这个问题,我们可以枚举出在此区间所有的日期,然后判断此日期是否满足此条件,满足就记录一次。这样的问题,就设计到了日期的枚举。
对于日期的枚举,存在两种情况。
- 完整日期枚举:年份不固定,给定的时间从1月1日开始,到12月31日。或者是,年份固定。
- 例如:1900年1月1日至9999年12月31日(年份不固定,给定的时间从1月1日开始,到12月31日)
- 例如:2023年5月1日至2023年8月9日(年份固定)
- 不完整日期枚举:年份不固定,给定的时间非1月1日开始。
- 例如:1900年5月16日到2355年9月16日
为什么会存在两种不同的情况呢?举一个例子。当枚举1900年1月1日至9999年12月31日,我们会发现每一年都会从1月1日到12月31日,所以写的循环变量的边界是一致的。当枚举1900年5月16日到2355年9月16日,需要进行特殊判断,因为1900年和2355年不是从1月1日到12月31日的。
下面给出具体的代码:
完整日期的枚举:
代码
int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int getDaysInMonth(int year, int month) {
if(month == 2 && isLeapYear(year)) {
return 29;
} else {
return daysInMonth[month];
}
}
int main() {
int ans = 0;
for(int year = 1900; year <= 9999; year ++) {
for(int month = 1; month <= 12; month ++) {
for(int day = 1; day <= getDaysInMonth(year, month); day ++) {
}
}
}
return 0;
}
不完整日期的枚举:
// 获取年份
int getYear(int num) {
return num / 10000;
}
// 获取月份
int getMonth(int num) {
return (num / 100) % 100;
}
// 获取日期
int getDay(int num) {
return num % 100;
}
// 判断某年是否为闰年
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 获取某月的天数
int getDaysInMonth(int year, int month) {
if (month == 2 && isLeapYear(year)) {
return 29;
} else {
int daysInMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
return daysInMonth[month];
}
}
int main() {
// 给定的两个8位日期数字
int num1 = 0;
int num2 = 0;
cin >> num1 >> num2;
// 存储结果
ll ans = 0;
// 获取第一个日期的年、月、日
int year1 = getYear(num1);
int month1 = getMonth(num1);
int day1 = getDay(num1);
// 获取第二个日期的年、月、日
int year2 = getYear(num2);
int month2 = getMonth(num2);
int day2 = getDay(num2);
// 从第一个日期遍历到第二个日期
for (int year = year1; year <= year2; year++) {
int startMonth = (year == year1) ? month1 : 1; // 如果是第一年,从第一个日期的月份开始遍历,否则从1月开始
int endMonth = (year == year2) ? month2 : 12; // 如果是最后一年,遍历到第二个日期的月份,否则遍历到12月
for (int month = startMonth; month <= endMonth; month++) {
int startDay = (year == year1 && month == month1) ? day1 : 1; // 如果是第一年第一个月,从第一个日期的日期开始遍历,否则从1号开始
int endDay = (year == year2 && month == month2) ? day2 : getDaysInMonth(year, month); // 如果是最后一年最后一个月,遍历到第二个日期的日期,否则遍历到当月最后一天
for (int day = startDay; day <= endDay; day++) {
}
}
}
return 0;
}
1.2 位数枚举日期(需要判断)
我们可以知道,日期是由8位来组成的。例如20220315,位数为8位。我们可以去枚举每一位,一共枚举出八位,这样就确定出了一个日期。但是这个日期不一定合法,我们可以经过判断函数,来判断当前日期是否合法。
在最坏的情况下,每一位我们均可以0~9枚举,一共有8个数,运行次数为 1 0 8 10^8 108。但由于,题目中经常给出一些条件,例如回文日期。这样,我们枚举四位即可。当我们利用好条件时,时间复杂度就会降低。
2.相关性质
2.1 判断当前年份是否为闰年
平年的2月份是28天,闰年的2月份是29天。
判断年份是否为闰年的一般方法是根据以下规则:
- 如果年份能够被 4 4 4整除但不能被 100 100 100整除,则是闰年。
- 如果年份能够被 400 400 400整除,则也是闰年。
基于这个规则,可以编写一个函数来判断年份是否为闰年,示例如下:
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
2.2 判断当前日期是否合法
给定8位整数日期,判断日期是否合法。这种通常用在我们用循环变量去枚举日期
bool isValidDate(int date) {
int year = date / 10000;
int month = (date / 100) % 100;
int day = date % 100;
// 判断年份是否合法
if (year < 0 || year > 9999) return false;
// 判断月份是否合法
if (month < 1 || month > 12) return false;
// 判断天数是否合法
if (day < 1 || day > 31) return false;
// 对于不同的月份进行天数判断
if ((month == 4 || month == 6 || month == 9 || month == 11) && day > 30)
return false;
else if (month == 2) {
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
if (day > 29) return false;
} else {
if (day > 28) return false;
}
}
return true;
}
3.例题
3.1 回文日期
题目描述
在日常生活中,通过年、月、日这三个要素可以表示出一个唯—确定的日期。
牛牛习惯用8位数字表示一个日期,其中,前4位代表年份,接下来2位代表月份,最后2位代表日期。显然:一个日期只有一种表示方法,而两个不同的日期的表示方法不会相同。
牛牛认为,一个日期是回文的,当且仅当表示这个日期的8位数字是回文的。现在,牛牛想知道:在他指定的两个日期之间包含这两个日期本身),有多少个真实存在的日期是回文的。
提示:
一个8位数字是回文的,当且仅当对于所有的i
(
1
≤
i
≤
8
)
(1≤i≤8)
(1≤i≤8)从左向右数的第i个数字和第9-i个数字(即从右向左数的第i个数字)是相同的。
例如:
1.对于2016年11月19日,用8位数字20161119表示,它不是回文的。
2.对于2010年1月2日,用8位数字20100102表示,它是回文的。
3.对于2010年10月2日,用8位数字20101002表示,它不是回文的。
输入描述
输入两行,每行包括一个8位数字。
第一行表示牛牛指定的起始日期。第二行表示牛牛指定的终止日期。
保证date和都是真实存在的日期,且年份部分—定为4位数字,且首位数字不为0。
保证date1一定不晚于date2。
输出描述
输出一个整数,表示在date和date2之间,有多少个日期是回文的。
输入输出样例
示例1
输入
20110101
20111231
输出
1
示例2
输入
20000101
20101231
输出
2
思路
第一种想法:由于题目中给定了起始的日期和终止的日期,经过判断属于不完整的日期,我们可以利用模板代码枚举出每个日期,然后对日期进行判断。此方法容易理解,但是时间复杂度高。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
bool ishw(string s) {
int left = 0, right = s.length() - 1;
while(left < right) {
if(s[left]!= s[right])
return false;
left ++, right --;
}
return true;
}
// 获取年份
int getYear(int num) {
return num / 10000;
}
// 获取月份
int getMonth(int num) {
return (num / 100) % 100;
}
// 获取日期
int getDay(int num) {
return num % 100;
}
// 判断某年是否为闰年
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 获取某月的天数
int getDaysInMonth(int year, int month) {
if (month == 2 && isLeapYear(year)) {
return 29;
} else {
int daysInMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
return daysInMonth[month];
}
}
int main() {
// 给定的两个日期数字
int num1 = 0;
int num2 = 0;
cin >> num1 >> num2;
// 存储结果
ll ans = 0;
// 获取第一个日期的年、月、日
int year1 = getYear(num1);
int month1 = getMonth(num1);
int day1 = getDay(num1);
// 获取第二个日期的年、月、日
int year2 = getYear(num2);
int month2 = getMonth(num2);
int day2 = getDay(num2);
// 从第一个日期遍历到第二个日期
for (int year = year1; year <= year2; year++) {
int startMonth = (year == year1) ? month1 : 1; // 如果是第一年,从第一个日期的月份开始遍历,否则从1月开始
int endMonth = (year == year2) ? month2 : 12; // 如果是最后一年,遍历到第二个日期的月份,否则遍历到12月
for (int month = startMonth; month <= endMonth; month++) {
int startDay = (year == year1 && month == month1) ? day1 : 1; // 如果是第一年第一个月,从第一个日期的日期开始遍历,否则从1号开始
int endDay = (year == year2 && month == month2) ? day2 : getDaysInMonth(year, month); // 如果是最后一年最后一个月,遍历到第二个日期的日期,否则遍历到当月最后一天
for (int day = startDay; day <= endDay; day++) {
string s = to_string(year) + (month < 10 ? "0" : "") + to_string(month) + (day < 10 ? "0" : "") + to_string(day);
if(ishw(s)) ans ++;
}
}
}
cout << ans << endl;
return 0;
}
第二种方法:由于是回文日期,所以当我们确定了月日,也就一定确定了年份,因为之间满足回文的关系。例如:0931为月日的日期,如果是回文数字的话,年份肯定是1309。所以,我们只需要去枚举月日即可。当枚举出了月日,根据回文性质得到年份,最终判断此年份是否满足给定的区间即可。
在枚举月日时,第2月为02,那么年份就为**20。如果一个年份为**20这种格式,那么此年份一定是闰年。所以,我们枚举出来的月份不用判断是否为闰年,直接按闰年算就可以。这是因为我们枚举出的月份,并且根据回文性质得到年份,如果存在2月,它一定是闰年。
代码
#include <bits/stdc++.h>
using namespace std;
int month[] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int n, m, ans;
int main() {
cin >> n >> m;
for(int i = 1; i <= 12; i ++) {
for(int j = 1; j <= month[i]; j ++) {
// 这种构造回文日期的方法是乱序的,只能用于统计回文日期的数量
// 构造年份
int year = j % 10 * 1000 + j / 10 * 100 + i % 10 * 10 + i / 10;
// 年份和月日拼接
int sum = year * 10000 + i * 100 + j;
if(sum > m || sum < n) continue;
else ans ++;
}
}
cout << ans << endl;
return 0;
}
3.2 日期统计
3.3 日期统计
问题描述
小蓝现在有一个长度为100的数组,数组中的每个元素的值都在0到9的范围之内。数组中的元素从左至右如下所示:
5 6 8 6 9 1 6 1 2 4 9 1 9 8 2 3 6 4 7 7 5 9 5 0 3 8 7 5 8 1 5 8 6 1 8 3 0 3 7 9 2
7 0 5 8 8 5 7 0 9 9 1 9 4 4 6 8 6 3 3 8 5 1 6 3 4 6 7 0 7 8 2 7 6 8 9 5 6 5 6 1 4 0 10 0 9 4 8 0 9 1 2 8 5 0 2 5 3 3
现在他想要从这个数组中寻找—些满足以下条件的子序列:
1.子序列的长度为8;
2.这个子序列可以按照下标顺序组成一个yyyymmdd格式的日期,并且要求这个日期是2023年中的某一天的日期,例如20230902,20231223。yyyy表示年份,mm表示月份,dd表示天数,当月份或者天数的长度只有一位时需要一个前导零补充。
请你帮小蓝计算下按上述条件—共能找到多少个不同的2023年的日期。对于相同的日期你只需要统计一次即可。
答案提交
这是—道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
思路
对于这个题目而言,这个子序列可以按照下标的顺序组成一个yyyymmdd的格式。这说明,组成日期的格式并不是下标并不是连续的。寻找到合法的日期,需要我们进行比对。我们可以枚举8位日期,若当前的日期与子序列按照下标顺序8位一致,便使结果数增1。
代码
#include<bits/stdc++.h>
int main() {
int a[100] = {
5, 6, 8, 6, 9, 1, 6, 1, 2, 4, 9, 1, 9, 8, 2, 3, 6, 4, 7, 7,
5, 9, 5, 0, 3, 8, 7, 5, 8, 1, 5, 8, 6, 1, 8, 3, 0, 3, 7, 9,
2, 7, 0, 5, 8, 8, 5, 7, 0, 9, 9, 1, 9, 4, 4, 6, 8, 6, 3, 3,
8, 5, 1, 6, 3, 4, 6, 7, 0, 7, 8, 2, 7, 6, 8, 9, 5, 6, 5, 6,
1, 4, 0, 1, 0, 0, 9, 4, 8, 0, 9, 1, 2, 8, 5, 0, 2, 5, 3, 3
};
int daysInMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int ans = 0;
for (int month = 1; month <= 12; ++month) {
for (int day = 1; day <= daysInMonth[month]; ++day) {
int dateSeq[8] = {2, 0, 2, 3, month / 10, month % 10, day / 10, day % 10};
int k = 0;
for (int i = 0; i < 100; ++i) {
if (a[i] == dateSeq[k]) {
++k;
if (k == 8) {
ans++;
break;
}
}
}
}
}
printf("%d\n", ans);
return 0;
}
3.4 顺子日期
问题描述
本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。
小明特别喜欢顺子。顺子指的就是连续的三个数字:123、456等。顺子日期指的就是在日期的yyyymmdd表示法中,存在任意连续的三位数是一个顺子的日期。例如20220123就是一个顺子日期,因为它出现了一个顺子:123;而20221023则不是一个顺子日期,它一个顺子也没有。小明想知道在整个2022年份中,一共有多少个顺子日期?
注意:这里的顺子日期必须是升序的0~9。
闰年的满足条件:
- 能被4整除,同时不能被100整除
- 能被400整除
月份的天数:
int month[] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; //非闰年
int month[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; //闰年
思路
2022为年份,并且年份与月份一起并不满足顺子日期,所以不用判断年份。
枚举月份和日期,用i,j两个变量来表示月份的两位,用k,l两个变量来表示日的两位。
存在两种情况,一种是ijk满足顺子日期,另一种是jkl满足顺子日期,在这两种情况,k总满足k-j==1,所以不用枚举k,k用j+1表示即可。
代码
#include <bits/stdc++.h>
using namespace std;
// 使用数组记录每个月份的天数
int month[] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int main() {
int ans = 0;
// 循环用来模拟月份
for(int i = 0; i < 2; i ++) {
for(int j = 0; j < 10; j ++) {
int k = j + 1;
for(int l = 0; l < 10; l ++) {
int m = i * 10 + j ; // 月
int d = k * 10 + l ; // 日
if(m < 13 && d <= month[m] && (i == j-1 || k == l-1)) ans++;;
}
}
}
printf("%d", ans);
return 0;
}
3.5 回文日期
题目描述
2020
2020
2020年春节期间,有一个特殊的日期引起了大家的注意:
2020
2020
2020年
2
2
2月
2
2
2日。因为如果将这个日期按
y
y
y
y
m
m
d
d
yyyymmdd
yyyymmdd的格式写成一个
8
8
8位数是
20200202
20200202
20200202,恰好是一个回文数。我们称这样的日期是回文日期。有人表示
20200202
20200202
20200202是“千年—遇”的特殊日子。对此小明很不认同,因为不到
2
2
2年之后就是下一个回文日期:
20211202
20211202
20211202即
2021
2021
2021年
12
12
12月
2
2
2日。
也有人表示
20200202
20200202
20200202并不仅仅是一个回文日期,还是一个
A
B
A
B
B
A
B
A
ABABBABA
ABABBABA型的回文日期。对此小明也不认同,因为大约
100
100
100年后就能遇到下一个
A
B
A
B
B
A
B
A
ABABBABA
ABABBABA型的回文日期:
21211212
21211212
21211212即
2121
2121
2121年
12
12
12月
12
12
12日。算不上“千年一遇”,顶多算“千年两遇”。
给定一个
8
8
8位数的日期,请你计算该日期之后下一个回文日期和下一个
A
B
A
B
B
A
B
A
ABABBABA
ABABBABA型的回文日期各是哪—天。
输入描述
输入包含一个八位整数
N
N
N,表示日期。
对于所有评测用例,
10000101
<
N
≤
89991231
10000101<N≤89991231
10000101<N≤89991231,保证
N
N
N是一个合法日期的
8
8
8位数表示。
输出描述
输出两行,每行 1 1 1个八位数。第一行表示下一个回文日期,第二行表示下一个 A B A B B A B A ABABBABA ABABBABA型的回文日期。
输入输出示例
输入
20200202
输出
20211202
21211212
代码
#include <bits/stdc++.h>
using namespace std;
int n;//接收给定的日期
int temp1, temp2; // temp1表示输出的第一行结果, temp2表示输出的第二行结果
// 判断是否为合法日期, 传入的整数为8位整数
bool is_date(int num) {
if(n < num && num < 99999999) {
int year = num / 10000;
int month = num / 100 % 100;
int day = num % 100;
if(year < 1 || year > 9999) return false;
if(month< 1 || month > 12) return false;
if(day < 1|| day > 31) return false;
// 对于不同的月份进行天数判断
if ((month == 4 || month == 6 || month == 9 || month == 11) && day > 30)
return false;
else if (month == 2) {
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
if (day > 29)
return false;
} else {
if (day > 28)
return false;
}
}
return true;
}
return false;
}
int main() {
//接收给定的日期
scanf("%d", &n);
// 枚举第一个合法日期
// 这样构造回文日期的算法是有序的
for(int i = 1; i <= 9; i ++) {
for(int j = 0; j <= 9; j ++) {
for(int k = 0; k <= 9; k ++) {
for(int l = 0; l <= 9; l ++) {
temp1 = i * 1e7 + j * 1e6 + k * 1e5 + l * 1e4 + l * 1e3 + k * 1e2 + j * 1e1 + i;
if(is_date(temp1)) { // 判断是否为合法日期并且是否大于给定的日期
printf("%d\n", temp1);
l = 10;// 输出一次即可,将这些变量置为10,停止循环
k = 10;
j = 10;
i = 10;
}
}
}
}
}
// 枚举第二个合法日期
for(int i = 1; i <= 9; i ++) {
for(int j = 0; j <= 9; j ++) {
temp2 = i*1e7 + j*1e6 + i*1e5 + j*1e4 + j*1e3 + i*1e2 + j*1e1 + i;
if(is_date(temp2)&& temp2 >= temp1) {
printf("%d", temp2);
j = 10;
i = 10;
}
}
}
return 0;
}
3.6 跑步锻炼
题目描述
小蓝每天都锻炼身体。
正常情况下,小蓝每天跑 1千米。如果某天是周一或者月初(1 日),为了激励自己,小蓝要跑 2千米。如果同时是周一或月初,小蓝也是跑 2 千米。
小蓝跑步已经坚持了很长时间,从 2000年 1 月 1日周六(含)到 2020年 10 月 1 日周四(含)。请问这段时间小蓝总共跑步多少千米?
思路
本质上就是就是对日期进行遍历,这里用了另一种遍历方式。同时,可以用ans标志当前的星期数,但由于
#include <iostream>
using namespace std;
int main(){
// 从下标1开始, 标志着每个月的天数
int months[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
// 年 月 日, 代表日期
int year,month,day;
//ans代表星期几, 初始为星期六
int ans=6;
// 跑步公里数
int cnt=0;
for(year=2000;year<=2020;year++){
// 判断是否为闰年
if(year%4==0&&year%100!=0||year%400==0){
months[2]=29;
}else{
months[2]=28;
}
//遍历月份
for(month=1;month<=12;month++){
//遍历天数
for(day=1;day<=months[month];day++){
cnt++;//每天一千米
if(ans==8){
ans=1;//ans自增到 8 时归回 1
}
// 周一或月初多跑一公里, 同时满足也多跑一个公里
if(ans==1||day==1){
cnt++;
}
ans++;//进入第二天
if(year==2020&&month==10&&day==1){//到2020.10.1结束循环
printf("%d",cnt);
}
}
}
}
return 0;
}