ECR 166 (Div. 2)

前言

        这是本文作者的第一篇博客。

        算是高二退役之后复出的第二场(复出第一场是9个月前的校内ACM选拔)。太久没打题,现在的竞技状态感觉还不如初三,不过好在是可以在短时间内恢复。既然是一个新的开始,就想着以写博客的方式记录一下复出的历程。

        首先不得不夸赞一下Codeforces这个网站。比赛的方式和频率都很适合一练,功能齐全,全世界高手同台竞技,并且可以通过自己的rating分值变化检测是否进步、带来成就感。要说缺点倒也有,一是比赛时间经常在 22:00 (UTC+8) 以后,而且毕竟是国外的供全世界使用的平台,在国内使用时不免有时候会产生卡顿,不过大部分时间还是比较顺畅的。

        第一次打cf,div2前2/3题拼的就是手速,于是事先已经打好了框架静候比赛开始。

        题目链接:Dashboard - Educational Codeforces Round 166 (Rated for Div. 2) - Codeforces

        比赛时间:2024-5-30  22:35(UTC+8) ~ 2024-5-31  00:35(UTC+8)

A. Verify Password

        简单暴力拼手速,没什么可讲的。提交的时候网有点卡,第6分钟AC。

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;

int T,n;
char s[50];

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d",&n);
		cin >> s;
		int flag = 1;
		for (int i = 1;i < n;++ i)
			if((s[i - 1] >= 'a' && s[i - 1] <= 'z') && (s[i] >= '0' && s[i] <= '9'))
			{
				flag = 0;
				break;
			}
			else if((s[i - 1] >= 'a' && s[i - 1] <= 'z') && (s[i] >= 'a' && s[i] <= 'z') && (s[i - 1] - 'a' > s[i] - 'a'))
			{
				flag = 0;
				break;
			}
			else if((s[i - 1] >= '0' && s[i - 1] <= '9') && (s[i] >= '0' && s[i] <= '9') && (s[i - 1] - '0' > s[i] - '0'))
			{
				flag = 0;
				break;
			}
			if(flag) printf("YES\n");
			else printf("NO\n");
	}
	return 0;
}

B. Increase/Decrease/Copy

        简单暴力拼手速,没什么可讲的。第一发WA了,有点紧张,盯着程序看了半天才看到不开longlong见祖宗(果然刚复出的时候都会出现一点小问题),第24分钟AC。

#include<cstdio>
#include<cstring>
using namespace std;

int T,n,a[200005],b[200005];

int abs(int x) { return x > 0 ? x : -x ; }

int min(int x,int y) { return x < y ? x : y ; }

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d",&n);
		for (int i = 1;i <= n;++ i) scanf("%d",&a[i]);
		for (int i = 1;i <= n + 1;++ i) scanf("%d",&b[i]);
		int x = b[n + 1];
		int tmp = 1000000001;
		long long ans = 0ll;
		for (int i = 1;i <= n;++ i)
		{
			ans += (long long)abs(a[i] - b[i]);
			if(a[i] < b[i])
			{
				if(a[i] <= x && x <= b[i]) tmp = 0;
				else if(a[i] > x) tmp = min(tmp,a[i] - x); 
				else if (x > b[i]) tmp = min(tmp,x - b[i]);
			}
			else
			{
				if(b[i] <= x && x <= a[i]) tmp = 0;
				else if(b[i] > x) tmp = min(tmp,b[i] - x); 
				else if (x > a[i]) tmp = min(tmp,x - a[i]);
			}
		}
		ans += tmp + 1;
		printf("%lld\n",ans);
	}
	return 0;
}

C. Job Interview

        稍微思考一分钟可以想出来。先从1到n+m+1按顺序聘用,对每个人做一些必要标记(被录用为哪一类,是否能替代由于前面人缺席产生的职位空缺),找到第一个满名额的人,这个人的位置就作为临界点。再从1到n+m+1对每个人操作,计算出这个人缺席时的总贡献,分3种情况讨论:

        1.这个人和临界点的人是同一职位(显然这种情况这个人一定在前面)

        2.这个人和临界点的人是不同职位且在临界位置之前

        3.这个人和临界点的人是不同职位且在临界位置之后

       时间复杂度:O(n)                第60分钟AC。

#include<cstdio>
#include<cstring>
using namespace std;

int T,n,m,a[200005],b[200005],p[200005],bl[200005],las[200005];
long long ans[200005];

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d%d",&n,&m);
		for (int i = 1;i <= n + m + 1;++ i) scanf("%d",&a[i]);
		for (int i = 1;i <= n + m + 1;++ i)
		{
			scanf("%d",&b[i]);
			if(a[i] > b[i]) p[i] = 1;
			else p[i] = 2;
		}
		int t1,t2,f,l;
		long long tot = 0ll;
		t1 = t2 = f = l = 0;
		for (int i = 1;i <= n + m;++ i)
		{
			bl[i] = 0;
			if(p[i] == 1)
			{
				if(t1 < n)
				{
					++ t1;
					tot += a[i];
				}
				else
				{
					++ t2;
					tot += b[i];
					bl[i] = 1;
				}
			}
			else
			{
				if(t2 < m)
				{
					++ t2;
					tot += b[i];
				}
				else
				{
					++ t1;
					tot += a[i];
					bl[i] = 1;
				}
			}
			if(!n) f = 1,l = 0;
			else if(!m) f = 2,l = 0;
			else if(t1 == n && t2 < m)
			{
				f = 1;
				l = i;
			}
			else if(t2 == m && t1 < n)
			{
				f = 2;
				l = i;
			}
		}
		if(f == 1) tot += b[n + m + 1];
		else tot += a[n + m + 1];
		las[n + m + 1] = n + m + 1;
		for (int i = n + m;i >= 1;-- i)
		{
			if(bl[i]) las[i] = i;
			else las[i] = las[i + 1];
		}
		if(f == 1)
		{
			for (int i = 1;i <= n + m + 1;++ i)
			{
				if(p[i] == 2) ans[i] = tot - b[i];
				else ans[i] = tot - a[i] - b[las[i]] + a[las[i]];
			}
		}
		else
		{
			for (int i = 1;i <= n + m + 1;++ i)
			{
				if(p[i] == 1) ans[i] = tot - a[i];
				else ans[i] = tot - b[i] - a[las[i]] + b[las[i]];
			}
		}
		for (int i = 1;i <= n + m + 1;++ i) printf("%lld ",ans[i]);
		printf("\n");
	}
	return 0;
}

D. Invertible Bracket Sequences

        比赛时考虑的是先用+1/-1标记括号,通过前缀后缀找出一段一段“连续的0”。当时只想到了翻转前的前缀和为0时不能翻转,否则会导致翻转后的右括号无法配对;但是其实翻转前的前缀和不为0时也不一定能翻转,取决于翻转的这一段括号是否会产生无法配对的情况,而“翻转前的前缀和为0”只是一种特殊情况。

        于是第一遍敲的思路是用桶标记然后O(n)做,WA了后很长一段时间自己出的数据都不能hack掉,还是同学给了个“(((((((((())))))))))”的数据让我恍然大悟,可惜已经快没有时间改了。

        正解是通过前缀和计算合法的翻转区间数目。暴力的做法是找出前缀和中每一对相等的非0数字,如果该区间合法则统计贡献。两点优化:

        1. 判断区间是否合法时可以用线段树/树状数组查询区间最大值

        2.无需枚举每一对可能的合法区间,用指针从后往前一个一个跳,边跳边记录子贡献,当遇到不合法区间时子贡献清零,再接着往前跳。

        时间复杂度:O(n log(n))

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;

int T,n,a[200005],dif[200005],b[400005],pre[200005],tr[200005];
char s[200005];
long long ans;

int lowbit(int x) { return x & (-x) ; }

void add(int x,int y)
{
	for (int i = x;i <= n;i += lowbit(i))
		tr[i] = max(tr[i],y);
	return;
}

int ask(int x,int y)
{
	if(y < x) return 0;
	if(y - lowbit(y) + 1 >= x) return max(tr[y],ask(x,y - lowbit(y)));
	else return max(dif[y],ask(x,y - 1));
}

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		cin >> s;
		n = strlen(s),ans = 0ll;
		for (int i = 0;i <= n;++ i) b[i] = 0,tr[i] = 0;
		dif[0] = 0;
		for (int i = 0;i < n;++ i)
		{
			a[i + 1] = (s[i] == '(') ? 1 : -1;
			dif[i + 1] = dif[i] + a[i + 1],add(i + 1,dif[i + 1]);
		}
		for (int i = 1;i <= n;++ i)
		{
			pre[i] = b[dif[i]];
			b[dif[i]] = i;
		}
		for (int i = 1;i <= n;++ i)
		{
			if(!b[i]) break;
			int now = b[i];
			int num = 1;
			while (now)
			{
				int tmp = pre[now];
				if(!tmp)
				{
					ans += (long long)(num - 1) * num / 2;
					break;
				}
				if(ask(tmp + 1,now) <= 2 * i) ++ num;
				else
				{
					ans += (long long)(num - 1) * num / 2;
					num = 1;
				}
				now = tmp;
			}
		}
		printf("%lld\n",ans);
	}
	return 0;
}

E. Splittable Permutations

        比赛的时候没来得及开这题,比赛后发现仔细想一想其实不难,是一道相对简单的计数题。

        首先可以发现,对于每一对给出的 l 和 r ,在分离之前应该属于同一个区块,并且知道了 l 在原数列中位于 r 的左边,那么我们可以从最后一对 l,r 到第一对 l,r,借用并查集和链表得出所有给出的数字在原数列中的相对位置。

        那么现在问题就转化为了:已知 1~n 中 t 个数的相对顺序,向其中插入剩下的 n-t 个数,能得到多少个“合法”的序列。乍一看是不是很像高中的数学题?没错,只不过要稍微思考一下如何才能使插入后得到的序列“合法”。

        先给出一个显然的结论:插入的这个数一定要小于他两边的数的最大值。要知道插入的数字是在 l , r 中没有出现过的,而在每一次分离操作后这个数字要么跟随左边的区间,要么跟随右边的区间,倘若这个数字大于他两边的数字的最大值,那么最后呈现出来的 l,r 中就应该出现这个数字,与前提相矛盾。

        所以我们只需要从大到小地插入每一个需要插入的数字,就能保证序列的合法性。具体地,原来给出的 t 个数会产生 t+1 个空位,其中合法的空位有 m 个,假设之前已经插入的数字个数为 k ,这 k 个数会产生额外的 k 个空位(即每次插入都能将一个空位一分为二,变成两个空位),所以此次插入操作的贡献为 m+k ,最后乘法原理相乘就好了。说明一点:先给原来的 t 个数从大到小排个序,然后线性一个一个往后走就能算出 m 。

        时间复杂度:O(n log(n))

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 300005
#define M 998244353

int n,q,fa[N],l[N],r[N],las[N],a[N],tl[N],tr[N],bg,cnt,buc[N],vis[N];
long long ans;

int max(int x,int y) { return x > y ? x : y ; }

int find(int x)
{
	if(fa[x] != x) fa[x] = find(fa[x]);
	return fa[x];
}

void merge(int x,int y)
{
	int rl = find(x);
	int rr = find(y);
	fa[rr] = rl;
	las[r[rl]] = l[rr];
	r[rl] = r[rr];
	bg = l[rl];
	return;
}

int cmp(int x,int y) { return x > y ; }

int main()
{
	memset(a,0,sizeof a);
	memset(vis,0,sizeof vis);
	memset(buc,0,sizeof buc);
	scanf("%d%d",&n,&q),bg = cnt = 0;
	for (int i = 1;i <= n;++ i) fa[i] = las[i] = l[i] = r[i] = i;
	for (int i = 1;i <= q;++ i) scanf("%d",&tl[i]);
	for (int i = 1;i <= q;++ i) scanf("%d",&tr[i]);
	for (int i = q; i ;-- i) merge(tl[i],tr[i]);
	while (las[bg] != bg) a[++ cnt] = bg,bg = las[bg];
	a[++ cnt] = bg;
	for (int i = 1;i <= cnt + 1;++ i) buc[i] = max(a[i - 1],a[i]),vis[a[i]] = 1;
	++ cnt;
	sort(buc + 1,buc + cnt + 1,cmp);
	int now,tmp;
	ans = 1ll,now = 0,tmp = 1;
	for (int i = n; i ;-- i)
		if(!vis[i])
		{
			while (buc[tmp] > i && tmp <= n)
				++ tmp;
			ans = ans * (long long)(tmp - 1 + now) % M;
			++ now;
		}
	printf("%lld\n",ans);
	return 0;
}

F. Remove Bridges

        也是比赛时没时间开,考完思考了一下发现也是道思维题。

        题意:给出一棵根节点为1的树,其中根节点有且仅有1个儿子,让你加边连接若干对点,使得最后呈现出来的树上桥的数目最少(限制:对于最后树的每个桥,桥上深度较深的那个节点的子树里的边也一定是桥),i 从 1 到 n-1 依次输出连接了 i 对点之后的最少桥的数量。

        随便画棵树观察,可以直观地发现要满足每次连边的限制条件,必须先把最上面的桥消灭。那么得出的第一个结论就是:第一次连边一定要包括根节点。再者,为了使最后桥的数目最少,就要让连边的两个点之间的路径最长,所以得到第二个结论:连边的点一定是根节点或叶子节点

        于是得到了大体的解题思路:第一次先连根节点和最远的那个叶子节点,第二次开始依次连接贡献最大的两个叶子节点。那么问题来了:如何知道哪两个点连接之后贡献最大呢?

        对于已经产生过贡献的路径(即变成“非桥”的路径),我们让这条路径上面的点变成灰色,即无法产生重复的贡献,那么对于尚未产生贡献的叶子节点,他们对答案的贡献就是从自己出发向上直到碰到灰色节点,这条路上的所有边都可以记作新的贡献,简单画个图就可以发现,每个叶子节点的贡献都是独立的,所以我们不必每次都寻找配对贡献最大的两个节点,而是可以独立地考虑自己贡献最大的两个节点,并把他们的贡献相加。

        故此,我们对每个节点记录一个 num ,表示该节点到其最远的叶子节点的距离+1,这表示的是每个临近灰色节点的节点,其最远叶子节点对答案的贡献。这样记录的原因是方便知道叶子节点向上碰到灰色节点的贡献值,而倘若把这个值记在叶子节点上则比较麻烦。同时开一个vis记录所有已经产生贡献的节点(当一个点变灰时,它到最远叶子节点路上的所有节点都要变灰,因为都产生了贡献)。将所有节点的 num 值从大到小排个序,就可以清楚地知道每次该选取作为贡献的节点了。

        时间复杂度:O(n log(n))

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 300005

int T,n,cnt,st[N],ans[N],rk[N];

struct Edge
{
	int next,to;
}ed[N << 1];

struct Tree
{
	int num,vis,son,id;
}tr[N];

void add(int u,int v)
{
	ed[++ cnt].next = st[u];
	ed[cnt].to = v;
	st[u] = cnt;
	return;
}

void dfs(int x,int fa)
{
	for (int i = st[x]; ~i ;i = ed[i].next)
	{
		int rec = ed[i].to;
		if(rec == fa) continue;
		dfs(rec,x);
		if(tr[rec].num + 1 > tr[x].num)
		{
			tr[x].num = tr[rec].num + 1;
			tr[x].son = rec;
		}
	}
	return;
}

int cmp(Tree x,Tree y) { return x.num > y.num ; }

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d",&n),cnt = 0;
		for (int i = 1;i <= n;++ i)
			st[i] = -1,tr[i].vis = ans[i] = 0,tr[i].num = 1,tr[i].son = tr[i].id = i;
		for (int i = 1,u,v;i < n;++ i)
		{
			scanf("%d%d",&u,&v);
			add(u,v),add(v,u);
		}
		dfs(1,0),tr[1].num = -1;
		sort(tr + 1,tr + n + 1,cmp);
		for (int i = 1;i <= n;++ i) rk[tr[i].id] = i;
		ans[1] = n - 1 - tr[1].num;
		int now = tr[1].id;
		tr[1].vis = cnt = 1;
		while (tr[rk[now]].num != 1) now = tr[rk[now]].son,tr[rk[now]].vis = 1;
		int flag = 0;
		for (int i = 2;i < n;++ i)
		{
			now = tr[i].id;
			if(tr[rk[now]].vis) continue;
			tr[rk[now]].vis = 1;
			if(!flag) ++ cnt,ans[cnt] = ans[cnt - 1],flag = 1;
			else flag = 0;
			ans[cnt] -= tr[i].num;
			while (tr[rk[now]].num != 1) now = tr[rk[now]].son,tr[rk[now]].vis = 1;
		}
		for (int i = 1;i < n;++ i) printf("%d ",ans[i]);
		printf("\n");
	}
	return 0;
}

总结

        第一次打cf,感觉div2的思维题巨多,真正考查纯算法和数据结构的较少,也不会出现码农题(纯属猜测),大多时候这些算法和数据结构只是作为一个小小的辅助,这也从另一个角度证明了为什么比赛时间很短,一般只有两个小时。这样的比赛首先得减少失误(像忘开long long这种),节约时间,提升速度,再者就是看比赛的状态,脑子灵不灵光,毕竟大多数都是思维题。

  • 26
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值