Codeforces #284 (Div.1 A~E & Div.2 A~E)

本文深入解析了Codeforces平台上的比赛题解,涵盖了观看电影、双语教学、小镇路径规划等多个场景,涉及贪心算法、动态规划等核心算法思想,通过实例详细阐述了解题思路和优化策略。

比赛地址

Codeforces Round #284 (Div. 1)

Codeforces Round #284 (Div. 2)


499A. Watching a movie

题意:

你准备去看一场电影,在预告中你确定了其中n个精彩片段是不容错过的,如果说电影被分成很多个正整数时刻,那么第n个精彩片段是在[l_i, r_i]的时刻。但是这场电影实在是又臭又长,你准备快进省掉一些片段,你现在有一个每次能快进x个时刻的遥控器,请问在观看了所有精彩片段的情况下至少需要看多少个时刻的内容。

n <= 50, x <= 10 ^ 5, 1 <= l_i <= r_i <= 10  ^ 5, r_i < l_{i + 1}。

题解:

模拟,对于两段精彩内容之间的时刻能快进就快进。有了r_i < l_{i + 1}的条件,直接顺着扫一遍就行了,不需要排序。

时间复杂度O(n)。

代码:

#include <cstdio>
int n, x, lastr = 1, l, r, ans;
int main()
{
	scanf("%d%d", &n, &x);
	while(n--)
	{
		scanf("%d%d", &l, &r);
		ans += (l - lastr) % x + r - l + 1;
		lastr = r + 1;
	}
	printf("%d\n", ans);
	return 0;
}

499B. Lecture

题意:

有一个教授在用双语教学,你在记笔记。已知双语中对应的m对单词,教授的一篇演讲包含n个单词,你希望能记得快一点,所以对于一个单词,你会选择它在两种语言里长度较短的一个记下来,如果长度相等则是记第一个单词。求你记的笔记。

n, m <= 3000, 单词为不超过10个小写字母组成的字符串。

题解:

模拟,建立一个映射表就好了。

代码:

#include <map>
#include <string>
#include <iostream>
using namespace std;
map<string, string> Hash;
int n, m;
string a, b;
int main()
{
	ios::sync_with_stdio(false);
	cin >> n >> m;
	while(m--)
	{
		cin >> a >> b;
		if(a.length() > b.length())
			swap(a, b);
		Hash[a] = Hash[b] = a;
	}
	while(n--)
	{
		cin >> a;
		cout << Hash[a] << ' ';
	}
	cout << endl;
	return 0;
}

498A. Crazy Town

题意:

你住在一个奇怪的小镇上,小镇有n条道路,道路将小镇分成了很多个区域,已知每条道路在平面上的直线方程ax + by + c = 0,你现在想从(x1, y1)走到(x2, y2),问最少需要再走几个区域。

n <= 300, 坐标绝对值不超过10 ^ 6。

题解:

可以考虑贪心。区域之间互相由道路分隔开,从一个区域到另一个区域一定会穿过一条道路,最优解一定不会穿过同一条道路两次,否则存在更优解。

对于一条道路,如果起点与终点都在这条路的一侧,最优解一定不会穿过这条道路,否则存在更优解。

经过两次限定后发现,只需要统计起点与终点被多少条道路分隔开即可。

时间复杂度O(n)。注意数据范围,计算过程中不能超过long long。

代码:

#include <map>
#include <set>
#include <list>
#include <queue>
#include <stack>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
int n, ans;
LL x1, y1, x2, y2;
LL val(LL a, LL b, LL c, LL x, LL y)
{
	return a * x + b * y + c;
}
int main()
{
	scanf("%I64d%I64d%I64d%I64d", &x1, &y1, &x2, &y2);
	scanf("%d", &n);
	for(int i = 0; i < n; ++i)
	{
		LL a, b, c;
		scanf("%I64d%I64d%I64d", &a, &b, &c);
		LL ret1 = val(a, b, c, x1, y1), ret2 = val(a, b, c, x2, y2);
		if(ret1 < 0 && ret2 > 0 || ret1 > 0 && ret2 < 0)
			++ans;
	}
	printf("%d\n", ans);
	return 0;
}

498B. Name That Tune

题意:

现在有一个猜歌活动,在T秒内会依次演奏n首歌曲,只有在某一秒猜对了当前的歌曲才会在下一秒播放下一首歌曲。这些歌曲你都曾经听过,第i首歌曲听够t_i秒的瞬间你会想起这首歌,在此之前每秒你都有p_i%的概率猜对这首歌。问你期望能猜对多少歌。注意可以提前完成猜歌活动。游戏从第1秒开始,第T秒出答案后结束。

n, T <= 5000, 1 < t_i <= T, 0 <= p_i <= 100。

题解:

可以考虑dp[i][j]表示在第j秒恰好猜出第i个歌曲的概率,则在播放t_i秒之前的每秒猜对的概率分别为p_i%, (1 - p_i%) * p_i%, (1 - p_i%) ^ 2 * p_i%, ..., (1 - p_i%) ^ (t_i - 2) * p_i%,在播放t_i秒时猜对的概率为1。期望即i * dp[i][j]的求和,其中1 <= i <= n, 0 <= j <= min{T, 前i首歌的t_i求和}。状态O(nT),转移O(T),时间复杂度O(nT^2)。

假设不限定播放t_i秒猜对的概率为1,只考虑(1 - p_i%) ^ k * p_i%,发现可以对dp[i][]求前缀和方便下一次转移,这样可能难写一点(我场上没调出来),不妨重新定义dp[i][j]。

dp[i][j]表示在前j秒内最后猜出第i个歌曲的概率,则可以O(1)由dp[i][j]转移到dp[i][j + 1]与dp[i + 1][j + 1]。再考虑播放t_i秒猜对的概率为1的情况,只需要将之前算重复的地方减去,算少的地方加上即可。状态O(nT),转移O(1),时间复杂度O(nT)。

代码:

#include <cstdio>
#include <cstring>
int n, tm, t[5010];
long double p[5010], f[2][5010], g[5010], ans;
long double pow(long double x, int n)
{
	long double ret = 1.0;
	while(n)
	{
		if(n & 1)
			ret *= x;
		x *= x;
		n >>= 1;
	}
	return ret;
}
int main()
{
	scanf("%d%d", &n, &tm);
	for(int i = 0, x; i < n; ++i)
	{
		scanf("%d%d", &x, &t[i]);
		p[i] = x / 100.0;
	}
	p[n] = 0, t[n] = 5000;
	f[0][0] = 1;
	for(int i = 0; i <= n; ++i)
	{
		int o = i & 1;
		long double pp = pow(1 - p[i], t[i]);
		memset(f[o ^ 1], 0, sizeof f[0]);
		for(int j = 0; j <= tm; ++j)
			g[j] = f[o][j];
		for(int j = 0; j <= tm; ++j)
		{
			f[o][j + 1] += f[o][j] * (1 - p[i]);
			f[o ^ 1][j + 1] += f[o][j] * p[i];
			if(j + t[i] <= tm)
			{
				f[o][j + t[i]] -= g[j] * pp;
				f[o ^ 1][j + t[i]] += g[j] * pp;
			}
		}
		ans += i * f[o][tm];
	}
	printf("%.9f\n", (double)ans);
	return 0;
}

498C. Array and Operations

题意:

现在有一个长度为n的序列a[],有m对关系(i_k, j_k),有一种操作,会从m对关系中选择a[i_k]和a[j_k]有大于1的公因子的一对关系,将a[i_k]和a[j_k]均除以某个公因子,问最多能做多少次操作。

2 <= n <= 100, 1 <= m <= 100,1 <= a[i] <= 10 ^ 9, 1 <= i_k < j_k <= n, i_k + j_k是奇数。

题解:

操作一定是选择公共质因子才能做的次数最多,可以考虑将数字分解成质因数。

注意到i_k + j_k是奇数,即i_k和j_k一奇一偶,那么操作是在两个点集之间的,点集内不会互相有操作,这形成一个二分图。

问题转化为二分图最大权匹配,由于一个数字的质因数个数是O(logA)级别的,所以每个数字最多被拆成9个质因数。

这个题可以用KM做,也可以考虑直接建网络流模型,设源点为s,汇点为t,则s向a[奇数]拆的点分别连一条流量为质因数个数的边,a[偶数]向t同理,对于一条关系(i_k, j_k),假设i_k是奇数,从a[i_k]对应的质因子向a[j_k]中相等的质因子连一条流量为正无穷的边,那么答案即为流量最大值。

设N = nlogA,M = mlogA,则时间复杂度为O(N^2M)。

代码:

#include <queue>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = 60;
class MinCost_MaxFlow
{
private:
	struct Edge { int to, cap, flow, nxt; } edges[23333];
	int n, m, s, t, flow, G[10010], level[10010];
	void AddEdge(int from, int to, int cap)
	{
		edges[m] = (Edge){to, cap, 0, G[from]};
		G[from] = m++;
		edges[m] = (Edge){from, 0, 0, G[to]};
		G[to] = m++;
	}
	int Q[10010];
	bool bfs()
	{
		int head = 0, tail = -1;
		memset(level, 0, sizeof(level));
		level[s] = 1;
		Q[++tail] = s;
		while(head <= tail)
		{
			int u = Q[head++];
			for(int i = G[u]; i != -1; i = edges[i].nxt)
			{
				Edge &e = edges[i];
				if(e.cap > e.flow && !level[e.to])
				{
					level[e.to] = level[u] + 1;
					Q[++tail] = e.to;
				}
			}
		}
		return level[t];
	}
	int dfs(int u, int alpha)
	{
		if(u == t || !alpha) return alpha;
		int w = 0, k;
		for(int i = G[u]; i != -1; i = edges[i].nxt)
		{
			Edge &e = edges[i];
			if(e.cap > e.flow && level[e.to] == level[u] + 1 && (k = dfs(e.to, min(e.cap - e.flow, alpha - w))))
			{
				e.flow += k;
				edges[i ^ 1].flow -= k;
				w += k;
			}
		}
		return w;
	}
	void MaxFlow()
	{
		int Flow;
		while(bfs())
			while((Flow = dfs(s, INF)))
				flow += Flow;
	}
public:
	int a[110], c[110], d[110][10][2];
	int solve()
	{
		memset(G, -1, sizeof(G));
		m = flow = 0;
		int N, M, i, j, k;
		scanf("%d%d", &N, &M);
		s = N * 9, t = N * 9 + 1;
		for(i = 0; i < N; ++i)
		{
			scanf("%d", &a[i]);
			for(j = 2, k = a[i]; (long long)j * j <= k; ++j)
				if(k % j == 0)
				{
					d[i][c[i]][0] = j;
					while(k % j == 0)
					{
						++d[i][c[i]][1];
						k /= j;
					}
					if(i & 1)
						AddEdge(s, i * 9 + c[i], d[i][c[i]][1]);
					else
						AddEdge(i * 9 + c[i], t, d[i][c[i]][1]);
					++c[i];
				}
			if(k > 1)
			{
				d[i][c[i]][0] = k;
				++d[i][c[i]][1];
				if(i & 1)
					AddEdge(s, i * 9 + c[i], d[i][c[i]][1]);
				else
					AddEdge(i * 9 + c[i], t, d[i][c[i]][1]);
				++c[i];
			}
		}
		while(M--)
		{
			int x, y;
			scanf("%d%d", &x, &y);
			--x, --y;
			if(y & 1)
				swap(x, y);
			for(i = 0; i < c[x]; ++i)
				for(j = 0; j < c[y]; ++j)
					if(d[x][i][0] == d[y][j][0])
						AddEdge(x * 9 + i, y * 9 + j, INF);
		}
		MaxFlow();
		return flow;
	}
} NetWork;
int main()
{
	printf("%d\n", NetWork.solve());
}

498D. Traffic Jams in the Land

题意:

现在有1 ~ {n + 1}个城市,城市i与城市{i + 1}之间由线段i来连通,第i条线段每隔a_i个时刻会产生一次交通阻塞,时刻0时所有的城市都在堵车。

现在考虑从城市x开到城市y,从时刻0出发,如果在a_i的倍数的时刻走到了城市i,则必须等待到线段i不堵车的一个时刻,然后在下一个时刻开到城市{i + 1}。

现在要支持两个操作,修改某个线段的堵车周期a_x变成y,查询0时刻开始从城市x到城市y所需的时间。

n, m <= 10 ^ 5, x < y, 任何时候2 <= a_i <= 6。

题解:

由于每个线段的堵车周期很小,可以考虑算出整体的堵车周期,LCM(2, 3, 4, 5, 6) = 60,每过60个时刻的结果是类似的。

建立60棵线段树,第i棵线段树存从周期时刻里的时刻i开始走过连续的一些线段所需的时间,向上合并操作也很好写。

时间复杂度O(n + qlogn)。

代码:

#include <cstdio>
const int maxn = 262144;
int n, q, seg[maxn][60];
void build(int o, int L, int R)
{
	if(L == R)
	{
		int x;
		scanf("%d", &x);
		for(int i = 0; i < 60; ++i)
			seg[o][i] = (i % x == 0) + 1;
		return;
	}
	int M = L + R >> 1;
	build(o + o, L, M);
	build(o + o + 1, M + 1, R);
	for(int i = 0; i < 60; ++i)
		seg[o][i] = seg[o + o][i] + seg[o + o + 1][(i + seg[o + o][i]) % 60];
}
void update(int o, int L, int R, const int &id, const int &x)
{
	if(L == R)
	{
		for(int i = 0; i < 60; ++i)
			seg[o][i] = (i % x == 0) + 1;
		return;
	}
	int M = L + R >> 1;
	if(id <= M)
		update(o + o, L, M, id, x);
	else
		update(o + o + 1, M + 1, R, id, x);
	for(int i = 0; i < 60; ++i)
		seg[o][i] = seg[o + o][i] + seg[o + o + 1][(i + seg[o + o][i]) % 60];
}
int query(int o, int L, int R, int l, int r, int sta)
{
	if(L == l && R == r)
		return seg[o][sta];
	int M = L + R >> 1;
	if(r <= M)
		return query(o + o, L, M, l, r, sta);
	if(M < l)
		return query(o + o + 1, M + 1, R, l, r, sta);
	int ret = query(o + o, L, M, l, M, sta);
	return ret + query(o + o + 1, M + 1, R, M + 1, r, (sta + ret) % 60);
}
int main()
{
	scanf("%d", &n);
	build(1, 1, n);
	scanf("%d", &q);
	while(q--)
	{
		char op[2];
		int x, y;
		scanf("%s%d%d", op, &x, &y);
		if(op[0] == 'A')
			printf("%d\n", query(1, 1, n, x, y - 1, 0));
		else
			update(1, 1, n, x, y);
	}
	return 0;
}

498E. Stairs and Lines

题意:

现在有7个由小正方形拼成的长条,第i个长条高度为i,长度为w_i,这样构成一个奇怪的图形,现在在这个图形的边缘已经涂上了颜色,问内部有多少种涂色方案,使得不存在一个小正方形的四个边都有颜色,答案对10 ^ 9 + 7取模。

w_i <= 10 ^ 5。

题解:

直接来算很难想,不妨将每一列的方案算出,然后经过一定的整合来统计答案。

每一列的方案涉及到该列内部横着的涂色方案和两边竖着的涂色方案,为了便于统计答案,且相邻的列受到竖着的涂色情况的制约,不妨将左右两边的涂色情况表示出来,再统计对应的状态下横着涂色有多少种方案,最终进行一个矩阵乘法加速递推即可。

我们用二进制表示对应的位置是否涂过色,0表示没涂色,1表示涂色,用dp[opt1][opt2]可以表示一列的左边竖着涂色情况为opt1,右边为opt2的方案,由于有效的情况是不存在一个格子四个边都涂了色,可以枚举横着涂色的情况opt3,其中opt1和opt2可以直接表示对应格子左右被涂色,opt3可以表示格子下方被涂色,(opt3 << 1)可以表示格子上方被涂色,那么有效的情况即以上四个状态不存在交集。

这样写有一点麻烦之处在于矩阵递推从第i个长条到第{i + 1}个长条的时候需要重新修改前面的答案矩阵,不妨定义0表示涂色,1表示没涂色,则答案矩阵不需要修改,答案即为最终的Ans[0][0](从所有边被涂色到所有边被涂色)。

时间复杂度O(2^7^3+2^7logw)。

代码:

#include <cstdio>
#include <cstring>
const int maxn = 1 << 7, mod = 1000000007;
struct Mat
{
	int r, c, num[maxn][maxn];
	void clear()
	{
		memset(num, 0, sizeof num);
	}
	Mat operator * (const Mat &x) const
	{
		Mat tmp = {};
		tmp.r = r;
		tmp.c = x.c;
		for(int i = 0; i < r; ++i)
			for(int j = 0; j < c; ++j)
				for(int k = 0; k < x.c; ++k)
					tmp.num[i][k] = (tmp.num[i][k] + (long long)num[i][j] * x.num[j][k]) % mod;
		return tmp;
	}
} A, B;
int main()
{
	for(int i = 0; i < 2; ++i)
		A.num[i][i] = 1;
	for(int h = 1; h <= 7; ++h)
	{
		int w, maxs = 1 << h;
		scanf("%d", &w);
		A.r = A.c = B.r = B.c = maxs;
		B.clear();
		for(int i = 0; i < maxs; ++i)
			for(int j = 0; j < maxs; ++j)
				for(int k = 0; k < (maxs >> 1); ++k)
					if((i | j | k | (k << 1)) == maxs - 1)
						++B.num[i][j];
		while(w)
		{
			if(w & 1)
				A = A * B;
			B = B * B;
			w >>= 1;
		}
	}
	printf("%d\n", A.num[0][0]);
	return 0;
}

小记

现场为div1,做出AC,赛后做出BDE。这一套题很不错,有很多有趣的题目,代码也很考察实力。C有两次WA源于边集数组与反弧的关系,以后需要改正习惯。

### Codeforces Round 1005 Div. 2 A-F Problem Solutions #### A. Money Change 为了处理货币转换问题,可以将所有的金额都转化为分的形式来简化计算。通过遍历输入数据并累加各个部分的金额,最后求得剩余的钱数并对100取模得到最终结果[^2]。 ```cpp #include &lt;iostream&gt; using namespace std; int main() { int s, xi, yi; cin &gt;&gt; s; int total_cents = 0; for (int i = 0; i &lt; s; ++i) { cin &gt;&gt; xi &gt;&gt; yi; total_cents += xi * 100 + yi; } cout &lt;&lt; (s * 100 - total_cents) % 100 &lt;&lt; endl; } ``` #### B. Odd and Even Pairs 此题目要求找到至少一对满足条件的索引:要么是一个偶数值的位置,或者是两个奇数值位置。程序会读入测试次数`t`以及每次测试中的数组长度`n`及其元素,并尝试找出符合条件的一对索引输出;如果没有这样的组合则返回-1[^3]。 ```cpp #include &lt;cstdio&gt; int main() { int t, n, num; scanf(&quot;%d&quot;, &amp;t); while (t--) { int evenIndex = 0, oddIndex1 = 0, oddIndex2 = 0; scanf(&quot;%d&quot;, &amp;n); for (int i = 1; i &lt;= n; ++i) { scanf(&quot;%d&quot;, &amp;num); if (num % 2 == 0 &amp;&amp; !evenIndex) evenIndex = i; else if (num % 2 != 0) { if (!oddIndex1) oddIndex1 = i; else if (!oddIndex2) oddIndex2 = i; } if ((evenIndex || (oddIndex1 &amp;&amp; oddIndex2))) break; } if (evenIndex) printf(&quot;1\n%d\n&quot;, evenIndex); else if (oddIndex1 &amp;&amp; oddIndex2) printf(&quot;2\n%d %d\n&quot;, oddIndex1, oddIndex2); else printf(&quot;-1\n&quot;); } return 0; } ``` 由于仅提供了前两道题的具体描述和解决方案,在这里无法继续给出完整的C至F题解答。通常情况下,每一道竞赛编程题都有其独特的挑战性和解决方法,建议查阅官方题解或社区讨论获取更多帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值