[NOIP 2016 普及组] 回文日期

GitHub跳转原题关系图返回列表

利用回文日期的结构直接由年份反推月份和日期,再判断是否为真实日期并检查是否落在区间内。

OJ: luogu

题目 ID: P2010

难度:普及-

标签:枚举模拟

日期: 2026-06-19 00:55

题意

给定两个 8 位日期 date1date2,要求统计闭区间 [date1, date2] 中有多少个真实存在的回文日期。

这里的“真实存在”指:

  • 月份必须在 1..12
  • 日期必须在当月的合法范围内
  • 2 月还要区分平年和闰年

思路

先看一个最直接的朴素做法:

date1 开始一天一天往后推到 date2,每遇到一个真实日期,就判断它的 8 位表示是否回文。

这个做法很好理解,也方便拿来对拍:

cpp
// brute.cpp:按天递增枚举区间里的每一个真实日期,直接判断是否回文。
#include <bits/stdc++.h>
using namespace std;

string left_date, right_date;

bool is_leap(int year) {
    if (year % 400 == 0) {
        return true;
    }
    if (year % 100 == 0) {
        return false;
    }
    return year % 4 == 0;
}

int days_in_month(int year, int month) {
    if (month == 1 || month == 3 || month == 5 || month == 7 ||
        month == 8 || month == 10 || month == 12) {
        return 31;
    }
    if (month == 4 || month == 6 || month == 9 || month == 11) {
        return 30;
    }
    if (month == 2) {
        if (is_leap(year)) {
            return 29;
        }
        return 28;
    }
    return 0;
}

void split_date(const string &s, int &year, int &month, int &day) {
    year = stoi(s.substr(0, 4));
    month = stoi(s.substr(4, 2));
    day = stoi(s.substr(6, 2));
}

string format_date(int year, int month, int day) {
    char buf[16];
    snprintf(buf, sizeof(buf), "%04d%02d%02d", year, month, day);
    return string(buf);
}

bool is_palindrome(const string &s) {
    for (int i = 0; i < 4; i++) {
        if (s[i] != s[7 - i]) {
            return false;
        }
    }
    return true;
}

void next_day(int &year, int &month, int &day) {
    day++;
    if (day <= days_in_month(year, month)) {
        return;
    }

    day = 1;
    month++;
    if (month <= 12) {
        return;
    }

    month = 1;
    year++;
}

void solve() {
    int year, month, day;
    int ans = 0;

    split_date(left_date, year, month, day);

    while (true) {
        string cur = format_date(year, month, day);
        if (is_palindrome(cur)) {
            ans++;
        }
        if (cur == right_date) {
            break;
        }
        next_day(year, month, day);
    }

    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> left_date >> right_date;
    solve();

    return 0;
}

但这题还有一个更关键的结构性质:

如果一个日期写成

yyyy mm dd

并且整个 8 位串是回文,那么它一定长成:

abcd dc ba

也就是说:

  • 年份确定后,月份就被唯一确定为 dc
  • 日期也被唯一确定为 ba

所以我们没必要在区间里把每一天都试一遍,只要枚举区间涉及到的每一个年份:

  1. 用年份反推出对应的 mmdd
  2. 判断这个年月日是不是真实日期
  3. 再判断这个回文日期是否落在给定区间内

这样每年最多检查一次,复杂度就很小了。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

string left_date, right_date;

bool is_leap(int year) {
    if (year % 400 == 0) {
        return true;
    }
    if (year % 100 == 0) {
        return false;
    }
    return year % 4 == 0;
}

int days_in_month(int year, int month) {
    if (month == 1 || month == 3 || month == 5 || month == 7 ||
        month == 8 || month == 10 || month == 12) {
        return 31;
    }
    if (month == 4 || month == 6 || month == 9 || month == 11) {
        return 30;
    }
    if (month == 2) {
        if (is_leap(year)) {
            return 29;
        }
        return 28;
    }
    return 0;
}

bool is_valid_date(int year, int month, int day) {
    if (month < 1 || month > 12) {
        return false;
    }
    if (day < 1 || day > days_in_month(year, month)) {
        return false;
    }
    return true;
}

string format_date(int year, int month, int day) {
    char buf[16];
    snprintf(buf, sizeof(buf), "%04d%02d%02d", year, month, day);
    return string(buf);
}

void solve() {
    int start_year = stoi(left_date.substr(0, 4));
    int end_year = stoi(right_date.substr(0, 4));
    int ans = 0;

    for (int year = start_year; year <= end_year; year++) {
        // 回文日期一定形如 abcd dc ba。
        int month = (year % 10) * 10 + (year / 10) % 10;
        int day = (year / 100 % 10) * 10 + year / 1000;

        if (!is_valid_date(year, month, day)) {
            continue;
        }

        string cur = format_date(year, month, day);
        if (cur >= left_date && cur <= right_date) {
            ans++;
        }
    }

    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> left_date >> right_date;
    solve();

    return 0;
}

复杂度

设左右端点的年份分别是 Y1Y2

时间复杂度是 O(Y2Y1+1)O(Y2 - Y1 + 1),空间复杂度是 O(1)O(1)

总结

这题的关键不是日期模拟本身,而是先看出“回文日期由年份唯一决定后四位”这个结构。

先利用结构把搜索空间大幅缩小,再做合法性判断,会比按天枚举更自然。