【题解】Educational Codeforces Round 147(CF1821)

发布时间 2023-08-11 21:51:50作者: linyihdfj

自己做出来了 A-E,F 算是搞懂 GF 后第一道符合我这个菜鸡水平的实战,可惜的是根本没意识到可以 GF。

A.Matching

题目描述:

整数模板是每位均为数字或问号的字符串。

如果可以用数字替换模板中的每个问号,从而获得该正整数(严格大于 \(0\) 的十进制表示形式,且不带任何前导零,则该正整数与整数模板匹配。

例如:
\(42\) 匹配 4?
\(1337\) 匹配 ????
\(1337\) 匹配 1?3?
\(1337\) 匹配 1337
\(3\) 不匹配 ??
\(8\) 不匹配 ???8
\(1337\) 不匹配 1?7

你将获得一个最多包含 \(5\) 个字符的整数模板。计算与其匹配的正整数(严格大于 \(0\))的数量。

\(1 \le t \le 2 \times 10^{5}\)\(t\) 为数据组数。
\(1 \le |s| \le 5\)\(|s|\) 为每组数据中字符串(整数模板)的长度。

题目分析:

除了第一个位置只可以选择 \([1,9]\) 其他的都可以选择 \([0,9]\) 所以直接乘一下就可以了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 200;
char s[N];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		scanf("%s",s+1);
		int n = strlen(s+1);
		int ans = 1;
		if(s[1] == '?')	ans = ans * 9;
		if(s[1] == '0')	ans = 0;
		for(int i=2; i<=n; i++){
			if(s[i] == '?')	ans = ans * 10;
		}
		printf("%d\n",ans);
	}
	return 0;
}

B.Sort the Subarray

题目描述:

有一个序列 \(a\),cff 排序了其中一段区间 \([l,r]\),得到了另一个新的序列。给出这两个序列,求 cff 排序的那一段区间的左右端点 \(l,r\)。输出最长的可能的区间。

题目分析:

如果有一些位置两个序列上的值不同也就是肯定排序经过这里,所以只需要找到排完序的序列的最长递增的一段且满足这一段包含值不同的位置即可。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int a[N],b[N];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		bool flag = false;
		int n;scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++)	scanf("%d",&b[i]);
		for(int i=1; i<=n; i++){
			if(a[i] != b[i])	flag = true;
		}
		int ans = 0,l = 0,r = 0;
		for(int i=1; i<=n; ){
			bool tag = false;
			int pos = i;
			while(b[pos+1] >= b[pos] && pos <= n)	++pos;
			for(int j=i; j<=pos; j++){
				if(a[j] != b[j])	tag = true;
			}
			if(pos - i + 1 > ans && flag == tag){
				ans = pos - i + 1;
				l = i,r = pos;
			}
			i = pos+1;
		}
		printf("%d %d\n",l,r);
		for(int i=1; i<=n; i++)	a[i] = 0,b[i] = 0;
	}
}

C.Tear It Apart

题目描述:

现有一个由小写字母组成的字符串,你将对这个字符串进行操作。每次操作你可以选择任意多个(可以只选一个)两两在字符串中不相邻的位置,把它们从字符串中删除。求至少进行多少次操作,字符串里的所有字母相同。

题目分析:

可以直接枚举最后相同的字母是什么,这样这些字母就将字符串划分成了若干段,每一段的花费次数就是 \(\lfloor \log_2{len} \rfloor + 1\),然后取一个最大值的最小值就好了。
因为注意到我们选择的位置不一定必须是 \(x,x+2,x+4,...\) 这种性质,所以可以所有段一起选,这样上面的过程就很合理了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
char s[N];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		scanf("%s",s+1);
		int n = strlen(s+1);
		int ans = n + 1;
		for(char c='a'; c<='z'; c++){
			int now = 0,len = 0;
			for(int i=1; i<=n; i++){
				if(s[i] == c){
					len = max(len,now),now = 0;
				}
				else	now++;
			}
			len = max(len,now);
			if(!len)	ans = 0;
			else{
				len = ((int)log2(len)) + 1;
				ans = min(ans,len);
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

D.Black Cells

题目描述:

在一条数轴上有无穷个点,下标为 \(0, 1, 2, \ldots\),初始时每个点都是白色的。

你控制着一个机器人,初始时机器人位于坐标为 \(0\) 的那个点。

机器人有两种状态:激活状态 和 非激活状态。

当处于激活状态时,机器人所在的点都会被染成黑色。

处于非激活状态时,机器人所在的点不会被染成黑色。

初始时机器人处于非激活状态。

你可以对这个机器人进行若干次操作,操作分为三种类型。每一次操作,你可以选择如下三种操作之一执行:

  • 将机器人的位置像数轴的正方向移动一个单位(即:若当前机器人在坐标 \(x\),则执行一次该操作后机器人将移动到坐标 \(x+1\) 的那个点);
  • 激活机器人:该操作只有当机器人处于非激活状态时才能执行,执行该操作后机器人将变为 激活状态;
  • 撤销激活机器人:该操作只有当机器人处于激活状态时才能执行,执行该操作后机器人将变为 非激活状态。

\(n\) 个区间,第 \(i\) 个区间用 \([l_i, r_i]\) 表示。

你需要使用最少的操作次数,将至少 \(k\) 个点染成黑色,但是有一个限制,就是:这些染成黑色的点必须包含在至少一个给定的区间中,这也就是说,如果你要将坐标为 \(x\) 的那个点染成黑色,则必须保证存在一个 \(i(1 \le i \le n)\) 满足 \(l_i \le x \le r_i\)

同时,本题也要求操作结束时机器人恢复到非激活状态(这也就意味着最少操作次数对应的最后一次操作是 撤销激活机器人)。

问:至少需要进行几次操作能够使至少 \(k\) 个点被染成黑色,且最终机器人处于非激活状态?

\(1 \le n \le 2\cdot 10^5,1 \le k \le 10^9,1 \le l_1 < l_2 < \cdots < l_n \le 10^9,1 \le r_i \le 10^9,l_i \le r_i < l_{i+1} - 1\)

题目分析:

一个显然的想法就是从前到后每次遇到一个可以选的段就选,这样看上去就很优。

但是如果这么简单会放在 EDU D 嘛,显然不是。

那么不优可能是什么情况下呢,就是我们如果选择一段必然伴随的开始一个激活和结尾的一个撤销,但是如果我们不在这里激活和撤销而直接走过去,走到下一段就只需要一个激活操作,可以手摸一下几个情况看看,比如下面两种情况(\(0\) 代表白色,\(1\) 代表黑色):\(00101100\)\(0011011100\)

所以仿佛只有长度为 \(1\) 的区间会导致这种情况,所以就直接分长度为 \(1\) 和长度大于等于 \(2\) 的区间处理就好了。

具体就是直接枚举长度为 \(1\) 的区间选多少个,然后在长度大于等于 \(2\) 的区间上二分找到最靠前的一个位置使得选择的黑色格子数为 \(k\),答案就很好算了。
当然也可以不用二分,就是随着长度为 \(1\) 的区间变多,我们二分的位置单调不增,所以可以双指针,但是仿佛细节有点多,不大会写。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
using namespace std;
const int N = 5e5+5;
const int INF = 1e17+5;
int pre[N],l[N],r[N];
vector<PII> v1,v2;
PII get(int x){
	if(v2.size() == 0 && x > 0)	return {INF,INF};
	if(pre[v2.size()-1] < x)	return {INF,INF};
	if(x == 0)	return {0,0};
	int pos = lower_bound(pre+1,pre+v2.size(),x)-pre;
	x -= pre[pos-1];
	return {v2[pos].first + x - 1,pos};
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%lld",&T);
	while(T--){
		int n,k;scanf("%lld%lld",&n,&k);
		int sum = 0;
		for(int i=1; i<=n; i++)	scanf("%lld",&l[i]);
		for(int i=1; i<=n; i++)	scanf("%lld",&r[i]);
		for(int i=1; i<=n; i++)	sum += r[i] - l[i] + 1;
		if(sum < k){
			printf("-1\n");
			continue;
		}
		for(int i=1; i<=n; i++){
			if(l[i] == r[i])	v1.push_back({l[i],r[i]});
			else	v2.push_back({l[i],r[i]});
		}
		v1.insert(v1.begin(),{0,0});
		v2.insert(v2.begin(),{0,0});
		for(int i=1; i<(int)v2.size(); i++){
			pre[i] = pre[i-1] + (v2[i].second - v2[i].first + 1);
		}
		int ans = INF;
		for(int i=0; i<(int)v1.size(); i++){  //枚举多少个 len = 1 
			PII pos = get(k-i);  //在 len >= 2 的上面二分 
			ans = min(ans,max(pos.first,v1[i].first) + (pos.second + i) * 2);
			//走的贡献和激活的贡献 
		}
		v1.clear();v2.clear();
		printf("%lld\n",ans);
	}
	return 0;
}

E.Rearrange Brackets

题目描述:

  • 本题一个测试点内有多组测试数据
  • 对于一个匹配的括号串,定义它的权值为进行以下操作多次将它清空的最小总代价:
    • 选取两个相邻的左右括号删除,并将代价加上原右括号右边的括号数量。
  • 你可以进行 不超过 \(\bm k\) 以下操作,将给定的匹配括号串 \(S\) 变为另一个匹配括号串:
    • 选取 一个 括号,将它移动到串的任意位置。
  • 求最终括号串的权值最小值。
  • \(1\leq |S|,\sum |S|\leq2\times10^5\)\(0\leq k\leq5\)

题目分析:

首先考虑如果给定一个括号串怎么找到最小的总代价,显然就是从右到左每一次选择可以选的一个然后删除即可,这个贪心显然很对吧。

所以可以发现我们其实每一次删除的都是一个合法括号匹配,也就是说我们如果把代价放到右括号右边的括号上,只有右括号有贡献,且贡献为这个括号匹配中间的合法括号匹配数,也就是说对于右括号 \(i\) 若其匹配的左括号为 \(match_i\)\(i\) 的代价为 \([match_i,i]\) 中合法括号匹配数。

所以要求移动 \(k\) 个括号就可以转化为将 \(k\) 个右括号对应的贡献删掉,也就是直接将这个右括号移动到其对应的左括号的旁边。

根据贪心的想法每一次找到贡献最大的一个右括号,然后删去这个右括号就是最优的,这样直接暴力模拟复杂度就是 \(O(nk)\),可以通过。

但是这个东西是可以优化的,因为我们代价最大的右括号一定是所在的一段合法括号序列的最外的一层匹配,所以删去之后不会对任何其他右括号的贡献产生影响,所以直接将右括号按代价排序从大到小选 \(k\) 个和其对应的左括号删去即可。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
char s[N],t[N];
int sum[N],match[N],n;
void get_sum(char *s){
	stack<pair<char,int> > st;
	for(int i=1; i<=n; i++){
		if(s[i] == '(')	st.push({s[i],i});
		else{
			int pos = st.top().second;
			sum[i]++;
			match[pos] = i,match[i] = pos;
			st.pop();
		}
	}
	for(int i=1; i<=n; i++)	sum[i] += sum[i-1];  //计算 [1,i] 有多少对完美匹配的括号
}
int solve(char *s){
	get_sum(s);
	int ans = 0,pos = 0,tot = 0;
	for(int i=1; i<=n; i++){
		if(s[i] == ')')	tot += sum[i] - sum[match[i]] - 1;
		if(s[i] == ')' && sum[i] - sum[match[i]] - 1 > ans){
			ans = sum[i] - sum[match[i]] - 1;
			pos = i;
		}
	}
	for(int i=1; i<=n; i++)	sum[i] = 0;
	tot -= ans;
	int cnt = 0;
	for(int i=1; i<=n; i++){
		if(i != pos && i != match[i])	t[++cnt] = s[i];
	} 
	for(int i=1; i<=cnt; i++)	s[i] = t[i];
	n = cnt;	
	return tot;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%lld",&T);
	while(T--){
		int k;scanf("%lld",&k);
		scanf("%s",s+1);
		n = strlen(s+1);
		for(int i=1; i<=k; i++){
			if(i == k){
				int ans = solve(s);
				printf("%lld\n",ans);
				break;
			}
			else	solve(s);
		}
		if(k == 0){
			get_sum(s);
			int ans = 0;
			for(int i=1; i<=n; i++)	if(s[i] == ')')	ans += sum[i] - sum[match[i]] - 1;
			printf("%lld\n",ans);
			for(int i=1; i<=n; i++)	sum[i] = 0;
		}
	}
	return 0;
}

F.Timber

题目描述:

\([1, n]\) 的区间里放 \(m\) 棵树,每棵树的高度为 \(k\)。求有多少种放置树的方法,满足:

  1. 每个树都在整点上,且每个点最多只能放一棵树。
  2. 存在一种砍倒树的方案,使得树倒了之后不会越界,也不会有某个点被超过一棵树占据。你可以自由选择树向左倒(也就是占据区间 \([x - k, x]\))或者向右倒(也就是占据区间 \([x, x + k]\))。

\(1\leq n, m, k\leq 3\times 10 ^ 5\)

题目分析:

考虑如果给定了一个选树的局面,我们该怎么快速判断是否合法。显然我们可以让能向左倒的就向左倒,不然再向右倒,因为这样可以在后面留出尽可能多的位置方便操作。

所以为了描述这个局面,显然的 \(dp\) 就是,设 \(f[i][j]\) 表示前 \(i\) 棵树最后倒在 \(j\) (也就是倒的区间的右端点)的方案数,转移就是新加入一棵树,如下:

\[\begin{aligned} 2\times f[i][j] &\to f[i+1][x] &x\in[j+k+1,j+2k] \\ f[i][j] &\to f[i+1][x] &x\in[j+2k+1,\infty] \end{aligned} \]

第一个转移有一个 \(2\) 的系数是因为我们能找到两个位置,使得按照上述的策略一个位置向左可以倒在 \(x\) 另一个可以向右倒在 \(x\)

如果我们知道了 \(f[i]\) 的生成函数 \(F_i\) 考虑怎么得到 \(F_{i+1}\),其实就是通过多项式乘法进行一下转移,即乘:

\[\sum_{i=k+1}^{2k} 2x^i + \sum_{i=2k+1}^{\infty} x^i \]

所以根据上述式子,我们就可以得到答案的表达式:

\[\sum_{p=0}^n [x^p] (\sum_{i=k+1}^{2k} 2x^i + \sum_{i=2k+1}^{\infty}x^i)^m \]

后面是一个多项式快速幂的形式,所以直接暴力做就可以做到 \(O(n \log n)\),但是写多项式板子这种情况怎么可能发生呢,考虑再推推更优美的形式。

\[\begin{aligned} &[x^p] (\sum_{i=k+1}^{2k} 2x^i + \sum_{i=2k+1}^{\infty}x^i)^m \\ &= [x^{p-m(k+1)}] (\sum_{i=0}^{k-1} 2x^i + \sum_{i=k}^{\infty}x^i)^m \\ &= [x^{p-m(k+1)}] (\dfrac{2-x^k}{1-x})^m \end{aligned} \]

上述化简第一步就是直接提了一个 \(x^{k+1}\),第二步就是等比数列求和。

把分子和分母拆开都是我们熟悉的封闭形式,所以直接设:

\[h(t) = [x^{tk}] (2-x^k)^m = \binom{m}{t} (-1)^{t} 2^{m-t} \\ g(t) = [x^t] (\frac{1}{1-x})^m = \binom{t+m-1}{m-1} \]

\(S(g,n) = \sum_{i=0}^n g(i)\),则我们的答案为 \(\sum_{ki+j \le n - m(k+1)} h(i)g(j) = \sum_{i=0}^m h(i)S(g,n - m(k+1) - ki)\)

这东西就可以线性了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+6;
const int MOD = 998244353;
int fac[N],inv[N],h[N],f[N];
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
int binom(int n,int m){
	if(n < m || n < 0 || m < 0)	return 0;
	return mod(fac[n] * mod(inv[m] * inv[n-m]));
}
signed main(){
	int n,m,k;scanf("%lld%lld%lld",&n,&m,&k);
	fac[0] = 1;
	for(int i=1; i<=n+m; i++)	fac[i] = mod(fac[i-1] * i);
	inv[n+m] = power(fac[n+m],MOD-2);
	for(int i=n+m-1; i>=0; i--)	inv[i] = mod(inv[i+1] * (i+1));
	
	for(int i=0; i<=m; i++)	f[i] = power(MOD-1,i) * power(2,m-i)%MOD * binom(m,i)%MOD;
	for(int i=0; i<=n; i++)	h[i] = mod(h[i-1] + binom(i+m-1,m-1));
	
	int ans = 0;
	for(int i=0; i<=m; i++)	
		if(n - m * (k + 1) - k * i >= 0)	ans = mod(ans + f[i] * h[n - m * (k + 1) - k * i]);
	
	printf("%lld\n",ans);
	
	return 0;
}