【题解】Educational Codeforces Round 154 A-E(CF1861)

发布时间 2023-09-01 10:52:10作者: linyihdfj

感觉很不在状态啊,竟然没做出来 E。

A.Prime Deletion

题目描述:

质数是一个正整数,它正好有两个不同的正除数 \(1\)和整数本身。例如,\(2\)\(3\)\(13\)\(101\)是质数;\(1\)\(4\)\(6\)\(42\)不是质数。

给你一个从\(1\)\(9\)的数字序列,其中\(1\)\(9\)的每个数字都正好出现一次

您可以进行以下操作 多次(也许是零次):从序列中任选一个数字并删除它。但是,如果序列中只有两个数字,则不能执行此操作

您的目标是得到一个代表质数的序列。注意,不能对序列中的数字重新排序。

打印得到的序列,或者报告说不可能进行操作使得到的序列是质数。

题目分析:

显然要么存在子序列 \(13\) 要么存在子序列 \(31\),而这两个数都是质数,所以判一下是哪个就好了。
(为什么大家找到的都是 \(1\)\(3\) 呢,离谱)

代码:

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

B.Two Binary Strings

题目描述:

给你两个长度相等的字符串\(a\)\(b\),它们都由字符 0 和/或 1 组成;两个字符串都以字符 0 开始,以字符 1 结束。

您可以执行任意次数(可能为零)的以下操作:

  • 选择其中一个字符串和其中两个相等的字符;然后将它们之间的所有字符都变成这些字符。

形式上,您从这两个字符串中选择一个(让所选字符串为\(s\)),然后选取两个整数\(l\)\(r\),如\(1 \le l < r \le |s|\)\(s_l = s_r\),然后用\(s_l\)替换每个字符\(s_i\)\(l < i < r\)

例如,如果选择的字符串是 010101,那么只需进行一次操作,就可以将其转换为以下字符串之一:

  • 如果您选择\(l = 1\)\(r = 3\),则为 000101;
  • 如果您选择\(l = 1\)\(r = 5\),则为 000001;
  • 如果您选择\(l = 3\)\(r = 5\),则为 010001;
  • 如果您选择\(l = 4\)\(r = 6\),则为 010111;
  • 如果您选择\(l = 2\)\(r = 6\),则为 011111;
  • 如果您选择\(l = 2\)\(r = 4\),则为 011101。

您必须确定是否有可能通过任意多次应用此操作使给定的字符串相等。

数据满足两个字符串的第一个字符均为 \(0\),最后一个字符均为 \(1\)

题目分析:

考虑一次操作 \((l,r)\) 满足 \(a_l = a_r = 0\),这个操作造成的贡献显然不如 \((1,r)\) 优,因为这样的话 \([1,l)\) 中的字符是否相等我们都不用管了。
所以只需要判断是否存在 \(a_l = 0,b_l = 0\)\(a_{l+1} = 1,b_{l+1} = 1\) 的情况即可。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
char a[N],b[N];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		scanf("%s%s",a+1,b+1);
		int n = strlen(a + 1);
		bool flag = false;
		for(int i=1; i<n; i++){
			if(a[i] == '0' && b[i] == '0' && a[i+1] == '1' && b[i+1] == '1'){
				printf("YES\n");flag = true;
				break;
			}
		}
		if(!flag)	printf("NO\n");
	}
	return 0;
}

C.Queries for the Array

题目描述:

Monocarp 有一个由整数组成的数组 \(a\)。最初,这个数组是空的

Monocarp 对这个数组进行了三种查询:

  • 选择一个整数,并将其到数组的末尾。每当 Monocarp 执行一次这种类型的查询时,他都会写出一个字符 +;
  • 从数组中删除结尾的元素。每次 Monocarp 执行这种类型的查询时,他都会写出一个字符 -。Monocarp 从来没有在一个空数组上执行过这种查询;
  • 检查数组是否以非降序排序,即 \(a_1 \le a_2 \le \dots \le a_k\),其中 \(k\)是当前数组中元素的个数。每个元素少于\(2\)的数组都被认为是排序过的数组。如果在 Monocarp 执行该查询时数组已经排序,他就会写出字符 1。否则,他就写出一个字符 0。

给你一个由 0、1、+ 和 - 组成的 \(s\)个字符序列。这些都是 Monocarp 写出的字符,并按照他写出的顺序给出。

你必须检查这个顺序是否合法,即 Monocarp 有可能的某个查询,从而使他写出的字符串正好是 \(s\)

题目分析:

这个 + - 看上去和括号匹配很像所以就考虑放到括号匹配上理解这个过程。
考虑什么时候会出现写不出 \(s\) 呢,也就是我们原来判定了某一段为 \(1\) 而现在却认为这一段为 \(0\) 或原来判定了某一段为 \(0\) 而现在却认为这一段为 \(1\)
但是这里的“某一段”并不是指的一段 +,它仅仅只是括号匹配过程中的某一个状态。
通俗点说就是,我们括号匹配的栈中以某一个位置作为栈顶的时候,数组中的元素是唯一确定的。
所以可以直接记 \(f_i\) 表示以位置 \(i\) 为栈顶时对应的数组满足 \(1\) 还是满足 \(0\) 或者并未确定。
这样的话只需要在 \(s\)\(0\) 或者 \(1\) 时将对应的 \(f\) 值进行修改和判断即可。
需要注意的一点时当 \(s\)\(1\) 时不仅栈顶的 \(f\)\(1\) 以栈内的任何一个位置为栈顶的 \(f\) 都为 \(1\),因为数组的后面删去一些数不会对 \(1\) 的判断产生影响。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
char s[N];
int f[N],st[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%d",&T);
	while(T--){
		scanf("%s",s+1);
		int n = strlen(s+1);
		int tot = 0;
		int top = 0;
		bool flag = true;
		st[++top] = 0;f[0] = 1;  //多一个占位的,不然 sz=0 就很寄 
		for(int i=1; i<=n; i++){
			if(s[i] == '+' || s[i] == '-'){
				if(s[i] == '+')	++tot,f[tot] = 2,st[++top] = tot;
				else --top;
				if(top < 3)	f[st[top]] = 1;
			}
			if(s[i] == '1'){ 
				if(f[st[top]] == 0)	flag = false;
				else{  //若此时合法,则以往依旧合法 
					for(int i=top; f[st[i]] != 1 && i >= 1; i--){
						if(f[st[i]] == 0)	flag = false;
						else	f[st[i]] = 1;
					}
				}
			}
			else if(s[i] == '0'){
				if(f[st[top]] == 1)	flag = false;
				else	f[st[top]] = 0;
			}
		}
		if(flag)	printf("YES\n");
		else	printf("NO\n");
	}
	return 0;
}

D.Sorting By Multiplication

题目描述:

给你一个长度为 \(n\)的数组 \(a\),由 正整数 组成。

你可以在这个数组上执行以下操作任意次数(可能为零):

  • 选择三个整数\(l\)\(r\)\(x\),使得\(1 \le l \le r \le n\),并将每个\(a_i\)乘以\(x\),使得\(l \le i \le r\)乘以\(x\)

注意,你可以选择任何整数作为 \(x\),它不一定是正数。

您必须计算出使数组 \(a\)严格升序排序所需的最少操作次数(即必须满足条件 \(a_1 < a_2 < \dots < a_n\))。

题目分析:

注意到最终的数组肯定是一段负的 + 一段正的。
而这一段的负的可以集体将 \(-1\) 放到最后再乘,也就是上一步的状态就是一段严格单调递减 + 一段严格单调递增的。
所以我们可以枚举中间分隔的点,然后前后缀的最优答案拼接一下。
这就是问题转化为求 \(f_i\) 表示 \([1,i]\) 严格单调递减的最小操作次数。
\(a_i \ge a_{i-1}\) 则要对 \([1,i]\) 进行操作使得满足严格单调递减的条件,也就是 \(f_i = f_{i-1} + 1\)
否则显然就不用了直接加上就好,也就是 \(f_i = f_{i-1}\)
严格单调递增的处理方式同理。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
const int INF = 1e9+5;
int a[N],pre[N],suf[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%d",&T);
	while(T--){
		int n;scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		pre[1] = 0;  //前缀严格递减 
		for(int i=2; i<=n; i++)	pre[i] = pre[i-1] + (a[i] >= a[i-1]);
		suf[n] = 0;  //后缀严格递增 
		for(int i=n-1; i>=1; i--)	suf[i] = suf[i+1] + (a[i] >= a[i+1]);
		int ans = INF;
		for(int i=1; i<=n+1; i++){
			ans = min(ans,pre[i-1] + suf[i] + (i != 1));
		}
		printf("%d\n",ans);
	}
	return 0;
}

E.Non-Intersecting Subpermutations

题目描述:

给你两个整数 \(n\)\(k\)

对于长度为 \(n\)的数组,让我们把它的代价定义为该数组的最大连续子数组数:

  • 每个元素最多属于一个子数组;
  • 每个子数组的长度正好是 \(k\)
  • 每个子数组恰好包含一次从 \(1\)\(k\) 的整数。

例如,如果\(n = 10\)\(k = 3\)和数组是\([1, 2, 1, 3, 2, 3, 2, 3, 1, 3]\),那么它的代价是\(2\),原因如下、我们可以选择从第\(2\)个元素到第\(4\)个元素的子数组,以及从第\(7\)个元素到第\(9\)个元素的子数组,而且我们可以证明不可能选择超过\(2\)个子数组。

计算由 \(1\)\(k\) 个整数组成的所有长度为 \(n\) 的数组的代价总和,并打印出它的模数 \(998244353\)
\(2 \le k \le n \le 4000\)

题目分析:

做法一:
考虑怎么求一个给定数组的代价。
显然就是贪心的策略,从前到后扫,每次如果找到了一个长度为 \(k\) 的且满足条件的段就选择。
所以可以想到一个 \(dp\) 就是设 \(f[i][j]\) 表示前 \(i\) 个极长的符合条件的后缀长度为 \(j\) 的方案数,\(g[i][j]\) 表示前 \(i\) 个极长的符合条件的后缀长度为 \(j\) 的代价之和。
转移的话就是考虑 \(f[i][j]\) 的第 \(i+1\) 个填的是什么。
可以填一个这 \(j\) 个里都没有的数,也就是 \(f[i][j] \times (k - j) \to f[i+1][j]\)
或者可以填一个这 \(j\) 个里面的数,让极长的符合条件的后缀变短,也就是 \(f[i][j] \to f[i+1][p] (p \in [1,j])\)
对于 \(g\) 的转移显然是同理的。
要注意的一点就是 \(f[i][k-1]\) 的转移,显然没有必要转移到 \(f[i+1][k]\) 可以直接转移到 \(f[i+1][0]\) 比较方便后面的转移。
以及注意 \(g[i+1][0]\) 的转移就是:\(f[i][k-1] + g[i][k-1] \to g[i+1][0]\)

做法二:
可以考虑对于每一个长度为 \(k\) 的段计算贡献了多少次。
也就是设 \(f[i]\) 表示考虑了前 \(i\) 个最后一个长度为 \(k\) 的段以 \(i\) 为结尾的贡献次数。
显然可以先将 \(f[i]\) 的值设为 \(k! \times k^{i-k}\) 然后再考虑哪些是不合法的。
根据贪心的策略,我们以 \(i\) 为结尾的这一段会被选择当且仅当前面和它相交的这些段没有一个是合法的,不然我们就会尽可能地选择靠前的了。
所以我们要减去的就是:\(f[j] \times (i-j)! \ \ (j \in [i-k+1,i))\),这个系数就是因为中间这一部分在我们的统计中显然是一个阶乘的贡献。
统计答案的时候就是 \(ans = \sum_{i} f_i k^{n-i}\),因为剩下的哪些肯定可以随便选。

代码:

做法一:

点击查看代码
#include <cstdio>
using namespace std;
const int N = 4001, p = 998244353;
 
int n, k, f[N][N], g[N][N];
 
int main(){
    scanf("%d%d", &n, &k);
    f[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = k - 2; j >= 0; j--) {
            f[i - 1][j] = (f[i - 1][j] + f[i - 1][j + 1]) % p;
            g[i - 1][j] = (g[i - 1][j] + g[i - 1][j + 1]) % p;
        }
        for (int j = 1; j < k; j++) {
            f[i][j] = (f[i][j] + f[i - 1][j]) % p;
            g[i][j] = (g[i][j] + g[i - 1][j]) % p;
            f[i][j] = (f[i][j] + 1ll * (k - j + 1) * (f[i - 1][j - 1] - f[i - 1][j])) % p;
            g[i][j] = (g[i][j] + 1ll * (k - j + 1) * (g[i - 1][j - 1] - g[i - 1][j])) % p;
        }
        f[i][0] = f[i - 1][k - 1];
        g[i][0] = (g[i - 1][k - 1] + f[i - 1][k - 1]) % p;
    }
    int ans = 0;
    for (int i = 0; i < k; i++)
        ans = (ans + g[n][i]) % p;
    ans = (ans + p) % p;
    printf("%d", ans);
    return 0;
}

做法二:

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