时间日期问题

日期问题

日期问题通常涉及到暴力、枚举、模拟等算法,但是由于日期问题存在很强的规律性,同时日期问题通常会涉及到枚举、模拟两种算法,很难完全的去划分题型,所以专门创建一个日期问题的题型

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天。

判断年份是否为闰年的一般方法是根据以下规则:

  1. 如果年份能够被 4 4 4整除但不能被 100 100 100整除,则是闰年。
  2. 如果年份能够被 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) (1i8)从左向右数的第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<N89991231,保证 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;
}
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值