第十二次CCF-CSP(Crontab、行车路线)

第十二次CCF-CSP

第三题 Crontab

原题链接: 3254. Crontab - AcWing题库

思路分析

这道模拟题让我找不到思路…太菜了呜呜呜…

看了y总的讲解之后 --> 还能这样?原来如此!妙啊!妙极了!

下面我就从头开始分析一下y总的思路

首先肯定是输入数据,一个整型 n、两个字符串

接着输入n行要处理的数据:

每行分别是分钟minute、小时hour、月份中的天数day_of_month、月份month、星期几day_of_week、以及命令的名字name

输入完之后,需要对数据进行处理。也就是把字符串中的有效信息抽取出来!

详细看代码:

void work(string s, bool st[], int x)	//处理输入的数据,更新它对应的bool[] 出现过就变为 1
{
	if (s == "*")	//如果是 *,也就是所有的范围都是允许出现的,st[] = 1
	{
		for (int i = 0; i < x; i++)
			st[i] = 1;
	}
	else
	{
		//对于其他的情况(数字和字母、以及','和'-'),我们要把其中所表示的范围提取出来
		for (int i = 0; i < s.size(); i++)	//利用双指针算法
		{
			int j = i + 1;
			while (j < s.size() && s[j] != ',')	//找到第一个逗号(如果存在的话)
				j++;
			string str = s.substr(i, j - i);
			i = j;
			int k = str.find('-');	//判断逗号之间是否有'-'。
			if (k != -1)
			{
				int l = get_num(str.substr(0, k));
				int r = get_num(str.substr(k + 1));
				for (int u = l; u <= r; u++)
					st[u] = 1;
			}
			else
				st[get_num(str)] = 1;	//如果不存在'-',那么也就是str只有一个元素,直接把st[]=1
		}
	}
}

这里为了更方便的判断某一时刻(分钟)是否需要执行命令,这里y总利用bool数组,把上面的五个时间的约束条件都用bool[]表示出来,如果其中某一天(分钟/小时/…)满足条件,那么就把它赋值为1。这样一来,判断某一时刻是否需要执行命令,只需要看它们是否都为1! 方便起见,我们构建了一个结构体实现这个功能:

struct Task		//处理任务
{
	string name;
	bool minutes[60], hours[24], day_of_months[32], months[13], day_of_week[7];
	bool check(Timer &t)
	{
		return minutes[t.minute] && hours[t.hour] &&
			day_of_months[t.day] && months[t.month] &&
			day_of_week[t.week];
	}

};

目前为止,我们已经实现了对输入的时间的提取、以及是否执行命令的判断的函数。

那么我们要怎么找到所有满足要求的时刻呢?很简单 遍历啊!

但是这里的时间的表示方法很抽象,所以我们需要构建一个结构体来具体的表示一下这里的时间。具体的操作就看代码吧~

struct Timer	//处理时间
{
	int year, month, week, minute, day,hour;
	Timer(string str)
	{
		//sscanf 把字符串 str,格式化处理,分别存放到year,month,day,hour,minute
		sscanf(str.c_str(), "%04d%02d%02d%02d%02d", &year, &month, &day, &hour, &minute);
	}

	bool operator < (const Timer& t) const
	{
		if (year != t.year)
			return year < t.year;	//意思是,按照年份从小到大排序
		if (month != t.month)
			return month < t.month;
		if (day != t.day)
			return day < t.day;
		if (hour != t.hour)
			return hour < t.hour;
		return minute < t.minute;
	}

	string to_string()
	{
		char str[20];
		sprintf(str, "%04d%02d%02d%02d%02d", year, month, day, hour, minute);
		return str;
	}
	int is_leap()
	{
		if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
		{
			return 1;
		}
		return 0;
	}
	int get_day()
	{
		if (month == 2)
			return months[month] + is_leap();
		return months[month];
	}
	void next()	//要对日期进行一定的变换 以得到后一天的日期
	{
		if (++minute == 60)
		{
			minute = 0;
			if (++hour == 24)
			{
				hour = 0;
				week = (week + 1) % 7;
				if (++day > get_day())
				{
					day = 1;
					if (++month > 12)
					{
						month = 1;
						year++;
					}
				}
			}
		}
	}

};

好了,我们距离成功只差一点了! 模拟题会有很多细节,比如这道题规定月份和周几可以使用数字和英文缩写,且不区分大小写。所以我们想,不妨用哈希表的思想,建立一个字符串到数字的映射关系。

//映射的初始化
void init()
{
	string keys[] = {
		"jan","feb","mar","apr","may","jun",	//1--12月
		"jul","aug","sep","oct","nov","dec",
		"sun","mon","tue","wed","thu","fri","sat"	//周一到周日
	};
	int values[] = {
		1,2,3,4,5,6,
		7,8,9,10,11,12,
		0,1,2,3,4,5,6
	};
	for (int i = 0; i < 19; i++)
		nums[keys[i]] = values[i];
}
//处理数字和大小写
int get_num(string s)
{
	if (s[0] >= '0' && s[0] <= '9')	//如果s是数字
		return stoi(s);		//直接把字符串类型转换为int型
	else  //如果s是字符串
 	{
		string str;
		for (auto c : s)
		{
			str += tolower(c);	//统一转换为小写字母
		}
		return nums[str];	//返回哈希表的映射(映射为数字)
	}

}

最后就是遍历啦~

Timer time("197001010000"), Start(start), End(end);	//定义了三个时间节点:初始化 开始时间 截止时间
time.week = 4;	//1970年1月1日是周四

//下面按照时间 '遍历' 如果在规定时间之内的所有时刻(分钟),出现了满足输入数据所要求的情况,那么就把该时刻打印输出。
while (time < End)	//需要重载 < 
{
    if (!(time < Start))
    {
        for (int i = 1; i <= n; i++)
        {
            if (task[i].check(time))
            {
                //需要手写 to_string() 
                cout << time.to_string() << " " << task[i].name << endl;
            }
        }
    }
    time.next();	//手写 next() 遍历时间
}

代码展示

#include<bits/stdc++.h>
#include<unordered_map>

using namespace std;

int n;
int months[13] = { //12 个月的天数
	0,31,28,31,30,31,30,31,31,30,31,30,31
};
unordered_map<string, int> nums;	//做一个从字符串到整形的映射


struct Timer	//处理时间
{
	int year, month, week, minute, day,hour;
	Timer(string str)
	{
		//sscanf 把字符串 str,格式化处理,分别存放到year,month,day,hour,minute
		sscanf(str.c_str(), "%04d%02d%02d%02d%02d", &year, &month, &day, &hour, &minute);
	}

	bool operator < (const Timer& t) const
	{
		if (year != t.year)
			return year < t.year;	//意思是,按照年份从小到大排序
		if (month != t.month)
			return month < t.month;
		if (day != t.day)
			return day < t.day;
		if (hour != t.hour)
			return hour < t.hour;
		return minute < t.minute;
	}

	string to_string()
	{
		char str[20];
		sprintf(str, "%04d%02d%02d%02d%02d", year, month, day, hour, minute);
		return str;
	}
	int is_leap()
	{
		if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
		{
			return 1;
		}
		return 0;
	}
	int get_day()
	{
		if (month == 2)
			return months[month] + is_leap();
		return months[month];
	}
	void next()	//要对日期进行一定的变换 以得到后一天的日期
	{
		if (++minute == 60)
		{
			minute = 0;
			if (++hour == 24)
			{
				hour = 0;
				week = (week + 1) % 7;
				if (++day > get_day())
				{
					day = 1;
					if (++month > 12)
					{
						month = 1;
						year++;
					}
				}
			}
		}
	}

};

struct Task		//处理任务
{
	string name;
	bool minutes[60], hours[24], day_of_months[32], months[13], day_of_week[7];
	bool check(Timer &t)
	{
		return minutes[t.minute] && hours[t.hour] &&
			day_of_months[t.day] && months[t.month] &&
			day_of_week[t.week];
	}

};

void init()
{
	string keys[] = {
		"jan","feb","mar","apr","may","jun",	//1--12月
		"jul","aug","sep","oct","nov","dec",
		"sun","mon","tue","wed","thu","fri","sat"	//周一到周日
	};
	int values[] = {
		1,2,3,4,5,6,
		7,8,9,10,11,12,
		0,1,2,3,4,5,6
	};
	for (int i = 0; i < 19; i++)
		nums[keys[i]] = values[i];
}

int get_num(string s)
{
	if (s[0] >= '0' && s[0] <= '9')	//如果s是数字
		return stoi(s);		//直接把字符串类型转换为int型
	else  //如果s是字符串
 	{
		string str;
		for (auto c : s)
		{
			str += tolower(c);	//统一转换为小写字母
		}
		return nums[str];	//返回哈希表的映射(映射为数字)
	}

}
void work(string s, bool st[], int x)	//处理输入的数据,更新它对应的bool[] 出现过就变为 1
{
	if (s == "*")	//如果是 *,也就是所有的范围都是允许出现的,st[] = 1
	{
		for (int i = 0; i < x; i++)
			st[i] = 1;
	}
	else
	{
		//对于其他的情况(数字和字母、以及','和'-'),我们要把其中所表示的范围提取出来
		for (int i = 0; i < s.size(); i++)	//利用双指针算法
		{
			int j = i + 1;
			while (j < s.size() && s[j] != ',')	//找到第一个逗号(如果存在的话)
				j++;
			string str = s.substr(i, j - i);
			i = j;
			int k = str.find('-');	//判断逗号之间是否有'-'。
			if (k != -1)
			{
				int l = get_num(str.substr(0, k));
				int r = get_num(str.substr(k + 1));
				for (int u = l; u <= r; u++)
					st[u] = 1;
			}
			else
				st[get_num(str)] = 1;	//如果不存在'-',那么也就是str只有一个元素,直接把st[]=1
		}
	}
}

Task task[100];

int main()
{
	init();	//初始化,用来生成哈希表
	string start, end;
	cin >> n;
	cin >> start >> end;

	for (int i = 1; i <= n; i++)
	{
		string minute, hour, day_of_month, month, day_of_week,name;
		cin >> minute >> hour >> day_of_month >> month >> day_of_week>>name;

		work(minute, task[i].minutes, 60);
		work(hour, task[i].hours, 24);
		work(day_of_month, task[i].day_of_months, 32);
		work(month, task[i].months, 13);
		work(day_of_week, task[i].day_of_week, 7);
		task[i].name = name;
	}
	
	Timer time("197001010000"), Start(start), End(end);	//定义了三个时间节点:初始化 开始时间 截止时间
	time.week = 4;	//1970年1月1日是周四

	//下面按照时间 '遍历' 如果在规定时间之内的所有时刻(分钟),出现了满足输入数据所要求的情况,那么就把该时刻打印输出。
	while (time < End)	//需要重载 < 
	{
		if (!(time < Start))
		{
			for (int i = 1; i <= n; i++)
			{
				if (task[i].check(time))
				{
					//需要手写 to_string() 
					cout << time.to_string() << " " << task[i].name << endl;
				}
			}
		}
		time.next();	//手写 next() 遍历时间
	}
	return 0;
}

总结反思

通过这道大模拟题,我学到了很多巧妙的方法。

  • stoi(s) 可以把字符串 s 直接转换为整数!

  • tolower(c) 可以把字符 c 转为小写

  • toupper(c) 把字符 c 转为大写

  • 结构体构建的巧妙!!!

  • bool数组的巧妙利用!!!

  • 哈希表的映射!!!

y总nb!!!

第四题 行车路线

原题链接:3255. 行车路线 - AcWing题库

思路分析

这道题的意思是说,有 n 个节点, m 条边,每条边的权重(长度)为 x,有小路和大路两种边。其中大路边权给多少就是多少,但是小路的边权是连续小路的长度之和的平方! 好像这样说也并没有很清楚… 总之意思就是这个意思,在题目中是这样描述的:大道比较好走,每走 1 公里小明会增加 1 的疲劳度。小道不好走,如果连续走小道,小明的疲劳值会快速增加,连续走 s 公里小明会增加 s^2 的疲劳度。

所以不难发现,难点就在于小路的处理。

不过鉴于本人水平有限,且发现题目中说有部分的评测用例,不存在小道;故写出正常的dijkstra()算法,得了60分hhh

言归正传,正确的思路是什么呢?

观察数据范围不难发现,节点的个数很少,且保证答案不超过1e6! 这说明什么?说明连续的小路的长度不会超过1000公里! 所以我们将使用拆点的思想:

也就是说,我们要把原来的dist[i],从节点 1 到节点 i 的距离,变为dist[i][j] ,第一维还是表示从节点 1 到节点 i 的距离。 第二维 j 表示,最后一段路(通往i的那段)的小路的长度——如果是大路,则长度 j=0,如果是小路则长度j = 1,2,…,1000(因为小路的长度最大就是1000)

那么在更新dist[][] 时,就要这样:

//从点i更新临点k,当这段路是大路时:
dist[k][0] = min(dist[k][0],dist[i][0] + w[i]);

//当是小路时:y表示这段小路的长度(连续长度)
y += w[i];	//更新长度
dist[k][y] = min(dist[k][y],dist[i][j]-j^j + y^2);	//原来的权重 - 上一个节点的连续的小路的权重,再加上更新的节点的连续小路的权重。

完整代码

#include<bits/stdc++.h>

using namespace std;
const int N = 1010, M = 2e5 + 5,INF=1e6;
int h[N], e[M], ne[M], t[M], w[M], idx;
int n, m;
int dist[N][1010];
bool st[N][1010];

struct node		//重载  
{
	int x, y, v;	//点的编号,最后一段小路的长度,1到x点的最短距离
	bool operator < (const node& t) const   //优先队列默认是大根堆,所以这样重载就是小根堆!
	{
		return v > t.v;
	}
};

void add(int a, int b, int c, int d)
{
	w[idx] = c;
	t[idx] = d;
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

void dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
	priority_queue<node> q;
	
	dist[1][0] = 0;
	q.push({ 1,0,0 });
	while (!q.empty())
	{
		auto tmp = q.top();
		q.pop();
		if (st[tmp.x][tmp.y])
			continue;
		st[tmp.x][tmp.y] = true;
		for (int i = h[tmp.x]; ~i; i = ne[i])
		{
			int j = e[i];
			if (t[i] == 1)	//小路
			{
				int y = tmp.y, v = tmp.v;
				y += w[i];
				if(y<=1000)	//注意这个条件!!! 可以剪枝!!!
			    	if (dist[j][y] > v - tmp.y * tmp.y + y * y)
			    	{
			    		dist[j][y] = v - tmp.y * tmp.y + y * y;
    				// 	if(dist[j][y]<=INF)
    			    	    	q.push({ j,y,dist[j][y] });
		    		}
			}
			else  //大路
			{
				if (dist[j][0] > tmp.v + w[i])
				{
					dist[j][0] = tmp.v + w[i];
				// 	if(dist[j][0]<=INF)
			    		q.push({ j,0,dist[j][0] });
				}
			}

		}
	}


}
int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c, d;
		cin >> d >> a >> b >> c;
		add(a, b, c, d);
		add(b, a, c, d);	//无向边!!!
	}
	
	dijkstra();
	int ans = INF;
	for (int i = 0; i <= 1010; i++)
	{
		ans = min(dist[n][i], ans);
	}
	cout << ans;
	return 0;
}

反思总结:

对于优先队列,不能只会用它默认的形式,还有会把它和结构体结合起来。

下面代码的意思是,构造一个优先队列 q ,其中按照 z 值的大小,从小到大排序!!!

因为优先队列默认是大根堆!所以重载 < 时,return > 才会是从小到大排序!(这点和sort相反)

struct node
{
	int x,y,z;
   	bool operator< (const node &t)const
    {
        return z > t.z;
    }
}
priority_queue<node> q;

60分代码:

#include<bits/stdc++.h>

using namespace std;
const int N = 1010, M = 2e5 + 5;
int n, m;
int h[N], e[M], ne[M], w[M], t[M],idx;
queue<int> q;
int dist[N];

void add(int a, int b, int c, int d)
{
	w[idx] = c;
	t[idx] = d;
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

void bfs(int x)
{
	memset(dist, 0x3f, sizeof dist);

	q.push(x);
	dist[x] = 0;
	while (!q.empty())
	{
		int tmp = q.front();
		q.pop();

		for (int i = h[tmp]; ~i; i = ne[i])
		{
			int j = e[i];
			int v = 0;
			if (t[i] == 1)
				v = w[i] * w[i];
			else
				v = w[i];
			if (dist[j] > dist[tmp] + v)
			{
				dist[j] = dist[tmp] + v;
				q.push(j);
			}
		}
	}
}

int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	for (int i = 1; i <= m; i++)
	{
		int a, b, c, d;
		cin >> d >> a >> b >> c;
		add(a, b, c, d);
		add(b, a, c, d);
	}
	bfs(1);
// 	for (int i = 1; i <= n; i++)
// 	{
// 		cout << dist[i] << " ";
// 	}
    cout<<dist[n];
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值