201712-3 Crontab
时间限制:10.0s
内存限制:256MB
题目描述:
样例输入:
3 201711170032 201711222352
0 7 * * 1,3-5 get_up
30 23 * * Sat,Sun go_to_bed
15 12,18 * * * have_dinner
样例输出:
201711170700 get_up
201711171215 have_dinner
201711171815 have_dinner
201711181215 have_dinner
201711181815 have_dinner
201711182330 go_to_bed
201711191215 have_dinner
201711191815 have_dinner
201711192330 go_to_bed
201711200700 get_up
201711201215 have_dinner
201711201815 have_dinner
201711211215 have_dinner
201711211815 have_dinner
201711220700 get_up
201711221215 have_dinner
201711221815 have_dinner
分析
此题有以下几个难点,逐一分析
时间的表示
在题目中有两个时间的表示,一个是输入的起止时间,格式为yyyymmddHHMM
,它也是输出时间格式;一个是输入的crontab配置信息,格式为<minutes> <hours> <day of month> <month> <day of week> <command>
,其中可能会有,
、*
、-
符号。
对于时间的处理,我们大致有以下两种方法:
-
将年月日时分全部转换成long long格式的分钟,再进行减,比较等运算
这种方法对于进行一次转换后只需要对分钟进行运算的题目比较方便,是一劳永逸的;但是如果转换后输出仍然需要按照年月日时分,或者中途的比较不仅仅是比较分钟,也要比较年月日信息,就不适合。
-
用struct存储年月日时分,对每个部分进行比较
很显然这样比较会有很多for循环嵌套,需要注意细节问题。对于5层的循环,我们由外到内进行判断能避免浪费时间。例如,最外层year就不满足配置规则,若从min开始匹配,到最外层发现不满足再year++从头开始浪费时间。
for(year) for(month) for(day) for(hour) for(minute)
对于起止时间的表示很显然用第二种方法较好。
匹配的方法
匹配有两种方法,我们可以对照样例来看
-
遍历配置信息
即对于起止时间中的每一分钟,都让其顺序与Crontab中的配置信息进行匹配,匹配不了则换下一个配置信息。
这样可以保证按照题意输出:优先时间顺序,相同的时间能匹配两个配置,则按照配置顺序匹配。
-
遍历起止时间
即对于每一条配置信息,都让其顺序于起止时间中每分钟进行匹配,匹配不了则换下一分钟。
这样能够不预先存储配置信息,减少空间开销
但由于我们前面选择了从外到内匹配,就实现上来说2比较易于实现,因此选择方法2并在事后进行排序。
关于星期
对于配置信息,有一个<day of week>
字段,不像年月日时分是可进位的关系,星期和日是等价的,因此对星期的判断,需要和对日的判断在同一层,用&&
进行判断。
解法
数据结构
-
起止时间
yyyymmddHHMM
需要注意的是,由于年月日时分需要于配置信息进行比较,因此存储为整数比字符串更合适。可以直接用格式化输入存储。
struct Time { int y,m,d,h,min; }start,end;
-
配置信息
<minutes> <hours> <day of month> <month> <day of week> <command>
对于配置信息的前五个部分,可能有
*
、,
、-
字符的乱入,因此我们需要对其读入后进行处理再存储。处理的结果一定是一个个数字段,例如12-15,17-19
,对于其中每个数字段,使用pair< int, int>
存储其头尾;而对于每个部分,都有很多这种pair段,因此每个部分用vector< pair< int,int> >
存储;整条配置信息用数组vector< pair< int,int> > Cron[5]
存储。struct Crontab { vector< pair< int,int> > Cron[5]; string cmd; }input[N];
-
结果存储
由于我们需要对结果进行排序,因此要将其存储起来,存储的内容包括时间、命令和命令的次序,其中时间和次序是两个排序比较项。
struct Ans { long long time; string cmd; int num; bool operator < (const Ans &a) const { return time == a.time ? (num < a.num) : (time < a.time); } }; vector<Ans> ans; Ans tmp_ans;
-
各种映射
在时间的处理中往往需要定义各种映射,例如星期,月份,每月日期
//map可以像这样类似struct的方式初始化 map<string,int> month { {"jan",1},{"feb",2},{"mar",3},{"apr",4},{"may",5},{"jun",6}, {"jul",7},{"aug",8},{"sep",9},{"oct",10},{"nov",11},{"dec",12} }; map<string,int> week { {"sun",0},{"mon",1},{"tue",2},{"wed",3},{"thu",4},{"fri",5},{"sat",6} }; int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
读取配置信息
对于输入的读取我们可以用格式化字符串的方式,它甚至会帮我们自动处理前导0
//会自动处理前导0,若是00会保留一个0
//201711170032
scanf("%4d%2d%2d%2d%2d",&s.y,&s.m,&s.d,&s.h,&s.min);
scanf("%4d%2d%2d%2d%2d",&t.y,&t.m,&t.d,&t.h,&t.min);
对于匹配规则的读取我们需要注意循环之间的次序,循环的含义,不要把自己绕晕了。每条匹配规则的读取策略是:
-
按
' '
读取每一段字符串,其中每一段依次对应<minutes> <hours> <day of month> <month> <day of week> <command>
,' '
的分割我们可以用scanf("%s)
完成,它会自动停止在空格处。 -
对每段字符串,按照
,
进行分割,存储到vector<string> tmp
中。 -
对
tmp
中的每个string(可能是*
、12
、1-wed
等)进行判断。首先判断是否为*
,如果是则存入pair<-1,-1>
;如果不是则对每段进行'-'
分割,如果不能分割则头尾都存该数(例如pair<12,12>
),如果可以分割则存储其头尾字符。注意对于month和week需要将字符串映射成对应的数字,而不是直接把数字字符串转换成数字
-
读取最后一段字符串入cmd中。
代码如下,注意巧妙之处在于先用s1
,s2
存储字符串,再针对v=-1
的值一同转换成数字
for(int i = 0; i < n; i++)
{
read(input[i]);
}
void read(Crontab &input)
{
for(int j = 0; j < 5; j++)
{
char s[100] = {0};
vector<string> tmp;
scanf("%s",s);
strlwr(s);//转换成小写,无需担心标点数字,只会转换大写字母
char *p = strtok(s,",");
while(p)
{
tmp.push_back(p);
p = strtok(NULL, ",");
}
if(tmp[0] == "*")//任意字符的范围是[-1,-1]
input.Cron[j].push_back(make_pair(-1,-1));
else//否则可能是, -组合的数字字符段:1-3,jul-8
{
for(int k = 0; k < tmp.size(); k++)
{
int pos = tmp[k].find("-");
string s1 = "", s2 = "";
int v1 = -1,v2 = -1;
if(pos == string::npos)//单个值
{
s1 = tmp[k];
s2 = tmp[k];
}
else//-链接的头尾值
{
s1 = tmp[k].substr(0,pos);
s2 = tmp[k].substr(pos+1);
}
if(j == 3 || j == 4)//month或者week映射成数字,若本身是数字则返回-1
{
v1 = strmap(j,s1);
v2 = strmap(j,s2);
}
//只有map成功的v才有效,没有map(数字)或map失败(数字)都从s转换
if(v1 == -1)v1 = strtoint(s1);
if(v2 == -1)v2 = strtoint(s2);
input.Cron[j].push_back(make_pair(v1,v2));
}
}
}
//读入command
cin >> input.cmd;
}
匹配配置信息
按照y->m->d->h->min逐层匹配配置信息,如果满足条件,则进入下一层;如果不满足,则该层信息++。
我们需要注意每层的临界值的处理,同时顾及开始时间和截止时间并在每层内找到满足的是很难的(例如:2017-11-17-00:32 2019-6-22-23:23,对于17年月份的范围是11-12,对于18年范围是1-12,对于19年范围是1-6,每次都进行判断再取范围十分麻烦),因此我们仅仅顾及到开始时间,以开始时间的年月日时分初始化,终止时间就到min层再判断是否超过。对于单纯的判断时间是否超过,依次从年月日时分开始判断即可。
代码如下:
//判断是否满足规则
bool in(int a, vector<pair<int,int> >v)
{
if(v[0].first == -1)return true;//遇到*
for(int i = 0; i < v.size(); i++)
{
if(a >= v[i].first && a <= v[i].second)
return true;
}
return false;
}
//判断是否闰年
bool leap(int y)
{
return (!(y % 400) || (!(y % 4) && (y % 100)));
}
//判断当前日期是第几周
int cal_week(int y, int m, int d)
{
//1970-01-01 thu
long long day = 0;
for(int i = 1970; i < y; i++)
{
if(leap(i))
day += 366;
else
day += 365;
}
for(int i = 1; i < m; i++)
{
day += days[i];
}
if(leap(y) && m > 2)
day++;
day += d - 1;
return (day+4)%7;//注意被模数为正
}
//判断是否到达截止时间
bool end(int y, int m, int d, int h, int min, Time t)
{
if(y < t.y) return false;
if(m > t.m) return true;
if(m < t.m) return false;
if(d > t.d) return true;
if(d < t.d) return false;
if(h > t.h) return true;
if(h < t.h) return false;
if(min > t.min) return true;
if(min < t.min) return false;
return true;
}
//判断是否满足第num条匹配规则
void judge(Crontab &input, int num)
{
int y = s.y, m = s.m, d = s.d, h = s.h, min = s.min;
int day = 0;
for(; y <= t.y; y++, m = 1)//第二年开始从1月开始算
{
for(; m <= 12; m++, d = 1)//第二个月从1号开始算
{
if(in(m,input.Cron[3]))
{
day = days[m];
if(m == 2 && leap(y))
day++;
for(; d <= day; d++, h=0)//第二天从0h开始算
{
if(in(d,input.Cron[2]) && in(cal_week(y,m,d), input.Cron[4]))
for(;h < 24; h++, min=0)//第二h从0min开始算
{
if(in(h,input.Cron[1]))
for(; min < 60; min++)
{
if(end(y,m,d,h,min,t))break;
if(in(min,input.Cron[0]))
{
//注意整数溢出的问题
long long a = (long long)y*100000000 + (long long)m*1000000 + (long long)d*10000 + (long long)h*100 + min;
tmp_ans.time = a;
tmp_ans.num = num;
tmp_ans.cmd = input.cmd;
ans.push_back(tmp_ans);
}
}
}
}
}
}
}
}
最后则是排序和输出,前面对Ans的定义中重载了运算符,因此对于vector< Ans> ans
直接用algorithm中的sort排序即可sort(ans.begin(),ans.end())
,注意vector不像list是没有自己的sort方法的。