CSP2020 儒略日 题解
题目大意:求公元前 4713 年 1 月 1 日 经过 r 天后的日期,公元 1582 年 10 月 4 日以前适用儒略历,公元 1582 年 10 月 15 日以后适用格里高利历
q 次询问,\(q\leq 10^5\)
这题就我目前所知有三种做法:
做法一
大概就是先把儒略历和格里高利历的分界点判掉,然后两边分别先400年400年跳,100年100年跳,4年4年跳,1年1年跳,最后一个月一个月跳。
这种做法挺难写的。。。考场上并没有去写这种做法。。。
做法二
我考场上的做法。
一样的把分界点判掉,然后两边分开处理。
我先把每一年都看成是366天,这样可以直接往后跳 r / 366 年,然后令 r %= 366 ,然后因为我把一些平年看成了闰年,所以我需要把r加上我跳过的这些年中的平年数量,这个可以简单前缀和相减得到。剩下的小于366天的部分暴力一天一天跳即可。
这样做细节相对少一点,而且处理儒略历和格里高利历的方法类似,可以直接开两个namespace然后复制粘贴过去。
然而蒟蒻考场上思路混乱这题还是写了1个多小时(不应该呀
考场代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1007
#define M 1000007
#define LL long long
int Const=2299160; //r_i-=Const+1;
namespace Julian{
int type[N]={0,1,-1,1,0,1,0,1,1,0,1,0,1};
int base=-4716,year,month,day;
void reset(){
year=-4712,month=1,day=1;
}
int run(int x){
return x/4;
}
int pingnian(int l,int r){
l-=base,r-=base;
int rn=run(r)-run(l-1);
return (r-l+1)-rn;
}
inline void chkday(int x){
if(day>x)day=1,month++;
}
void travel(int times)
{
while(times){
day++; times--;
if(month==2){
if(year%4==0)chkday(29);
else chkday(28);
}
else{
if(type[month]==1)chkday(31);
else chkday(30);
}
if(month==13)year++,month=1;
}
}
void solve(LL times){
reset();
while(times>=366){
int dlt=times/366; times%=366;
int ping=pingnian(year,year+dlt-1);
year+=dlt,times+=ping;
}
travel(times);
if(year>0)printf("%d %d %d\n",day,month,year);
else printf("%d %d %d BC\n",day,month,-(year-1));
}
}
namespace Gregorian
{
int type[N]={0,1,-1,1,0,1,0,1,1,0,1,0,1};
int base=1580,year,month,day;
void reset(){
year=1582,month=10,day=15;
}
int run(int x){
return x/4-x/100+x/400;
}
int pingnian(int l,int r){
//l-=base,r-=base;
int rn=run(r)-run(l-1);
return (r-l+1)-rn;
}
inline void chkday(int x){
if(day>x)day=1,month++;
}
void travel(int times)
{
while(times){
day++; times--;
if(month==2){
if(year%400==0||(year%4==0&&year%100!=0))chkday(29);
else chkday(28);
}
else{
if(type[month]==1)chkday(31);
else chkday(30);
}
if(month==13)year++,month=1;
}
}
void solve(LL times){
reset();
while(times>=366){
int dlt=times/366; times%=366;
int ping=pingnian(year+1,year+dlt);
year+=dlt,times+=ping;
}
travel(times);
printf("%d %d %d\n",day,month,year);
}
}
int main()
{
LL r;
int m;
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%lld",&r);
if(r<=Const){
Julian::solve(r);
}else{
Gregorian::solve(r-(Const+1));
}
}
return 0;
}
做法三
这种做法更显得无脑暴力,因为它将每400年组成的循环节中每一天的日期全部打表打出来了。
首先注意到从公元前 4713 年 1 月 1 日 到公元 1582 年 10 月 4 日中间只有2299161天,我们可以把这2299161天的日期全部打表打出来,这样就可以直接回答所有儒略历的询问了。
然后处理格里高利历,我们建立两个映射,一个是从400年中的某个日期映射到它是这个400年中的第几天,另一个是从400年中的第几天映射到它在这400年中的日期,然后直接把整400年的都跳掉,然后剩下的天数直接通过映射表查询它会对应到那个日期。
代码:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N=3e6+7;
const int M=2e5+7;
struct date{
int y,m,d;
}ju[N],gr[M];
int year,month,day;
int ty[13]={0,1,-1,1,2,1,2,1,1,2,1,2,1};
int tot,len,idx[407][13][33];
inline void chkday(int x){
if(day>x)day=1,month++;
if(month>12)month=1,year++;
}
void init(){
year=-4712,month=1,day=1;
ju[0]={year,month,day};
while(year!=1582||month!=10||day!=4){
day++;
if(ty[month]==1)chkday(31);
else if(ty[month]==2)chkday(30);
else {
if(year%4==0)chkday(29);
else chkday(28);
}
ju[++tot]={year,month,day};
}
}
void init_2(){
year=0,month=1,day=1;
while(year!=400){
idx[year][month][day]=len;
gr[len++]={year,month,day};
day++;
if(ty[month]==1)chkday(31);
else if(ty[month]==2)chkday(30);
else{
if(year%400==0||(year%4==0&&year%100!=0))chkday(29);
else chkday(28);
}
}
}
void print(date v){
if(v.y<=0){
printf("%d %d %d BC\n",v.d,v.m,-(v.y-1));
}
else{
printf("%d %d %d\n",v.d,v.m,v.y);
}
}
void Gregor(LL times){
year=1582,month=10,day=15;
year+=(times/len)*400,times%=len;
int rest=(year%400+400)%400;
year-=rest;
int ind=idx[rest][month][day];
ind+=times;
if(ind>=len)year+=400,ind-=len;
year+=gr[ind].y,month=gr[ind].m,day=gr[ind].d;
print({year,month,day});
}
int main()
{
init(); init_2();
int q;
scanf("%d",&q);
for(int i=1;i<=q;i++){
LL r; scanf("%lld",&r);
if(r<=tot)print(ju[r]);
else Gregor(r-tot-1);
}
return 0;
}