【题解】Educational Codeforces Round 145(CF1809)

发布时间 2023-09-05 09:50:09作者: linyihdfj

A.Garland

题目描述:

\(4\) 只灯泡,第 \(i\) 只灯泡的颜色为 \(s_i\)

一开始,所有灯泡都是关着的,你需要把它们都打开。你可以进行数次操作,每次操作改变一个灯泡的状态,即打开原本关着的灯泡或关上原本亮着的灯泡。第一次操作可选择任何灯泡,此后每一次被操作的灯泡的颜色都不得与上次操作的灯泡颜色相同。现求最少需要的操作数,如果无法打开所有灯泡输出 \(-1\)

题目分析;

因为只有四只灯泡,所以我们可以直接分类讨论一下出现最多的颜色的个数,然后就做完了。

代码:

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

B.Points on Plane

题目描述:

给你一个二维平面,你需要在其上放置 \(n\) 个棋子。

你只能将棋子放在整数坐标的点上。将棋子放在坐标点 $ (x, y) $ 上的成本等于 $ |x| + |y| $ (其中,\(|a|\) 表示 \(a\) 的绝对值)。

放置 \(n\) 个棋子的成本等于其中最大的单个棋子的成本。

你需要在平面上布置 \(n\) 个棋子,使得每对棋子之间的欧几里得距离严格大于 \(1\) ,且成本尽可能小。

题目分析:

我们一个显然的想法肯定就是从尽可能靠近原点的开始放,然后一层层向外放,使得每一个棋子之间满足距离严格大于 \(1\)
但是要注意的一点就是,我们不一定要将棋子放在原点上,可能放在外围的一圈会更优。

这样就是考虑我们这样放出来的话棋子数是多少,如下图所示:

黄色方格构成的就是 \(d = 4\) 的最优放置方式,而黄色方格中间的红色方格就是 \(d = 3\) 的最优放置方式,那么这个数量是多少呢,我们可以将图旋转 \(45°\),这样其实就是对应一个正方形,也就是说 \(d = x\) 的最优放置方案下可以放置 \((x+1)^2\) 个棋子。

所以说对于 \(n\) 就找到最小的 \(x\) 使得 \((x+1)^2 \le n\) 即可,这个直接可以调用 sqrt 函数。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
	int T;scanf("%lld",&T);
	while(T--){
		int x;scanf("%lld",&x);
		int p = sqrt(x);
		if(p * p > x) --p;
		if(p * p < x) ++p;
		printf("%lld\n",p-1);
	}
	return 0;
}

C.Sum on Subarrays

题目描述:

给定两个正整数 \(n\)\(k\),构造一个长度为 \(n\) 的序列 \(a\) 满足:

  • \(-1000\le a_i\le 1000\)
  • 序列有恰好 \(k\) 个子串的和为正数
  • 剩余 \(\frac{n(n+1)}2-k\) 个子串的和都是负数

你只需要输出任意一组解。
\(n \le 30\)

题目分析:

对于连续的 \(x\) 个正数,其会产生 \(\frac{x(x+1)}{2}\) 个符合条件的子串,因为 \(n \le 30\),所以直接在外侧放一个 \(-1000\) 就直接杜绝了其它的子串也是正数的情况了。

但是如果 \(k \not= \frac{x(x+1)}{2}\) 其实也是简单的,如果我们找到最大的 \(x\) 使得 \(k < \frac{x(x+1)}{2}\) 的话,那么我们缺少的符合条件的子串数量一定不超过 \(x\),也就是说我们可以通过在外侧放一个较小的负数,使得缺的补上就好了。

一个细节就是我们不能让子串和为 \(0\),所以我们在放正数的时候不能放 \(1\),这样会导致我们补的较小的负数为 \(0\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int a[1000];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		int n,k;scanf("%d%d",&n,&k);
		int p = 0;
		while(p * (p + 1) / 2 < k)	p++;
		if(p * (p + 1) / 2 == k){
			for(int i=1; i<=p; i++)	a[i] = 11;
			for(int i=p+1; i<=n; i++) a[i] = -1000;
		}
		else{
			--p;
			int tmp = k - p * (p + 1) / 2;
			tmp = p - tmp;
			for(int i=1; i<=p; i++)	a[i] = 11;
			a[p+1] = -tmp * 11 - 1;
			for(int i=p+2; i<=n; i++)	a[i] = -1000;
		}
		for(int i=1; i<=n; i++)	printf("%d ",a[i]);
		printf("\n");
	}
	return 0;
} 

D.Binary String Sorting

题目描述:

给定一个 \(01\) 字符串,你可以进行以下两种操作:

  1. 交换相邻的两个元素,此操作需要花费 \(10^{12}\) 个金币。

  2. 删除某个元素,此操作需要花费 \(10^{12}+1\) 个金币。

你要通过以上两种操作使这个字符串单调不降,求最小金币花费。
\(1 \le n \le 3\times 10^5\)

题目分析:

这种离谱的贡献其实就是让我们在最小化总操作次数的基础上,最小化交换的操作次数。

在逆序对的角度来看,交换操作相当不优,因为交换只能减少一个逆序对,而删除可以直接干掉很多个。

但是如果直接按逆序对个数判断是不是应该交换的话就很难受,不好实现,但是注意到我们是一个 \(01\) 字符串,也就是说我们排序之后一定是形如 \(0000 \cdots 000111 \cdots 1111\),也就是说我们可以直接枚举 \(01\) 的分界点,这样我们就是要求前面全部变成 \(0\) 后面全部变成 \(1\)

稍微自己讨论一下就会发现除了在分界点处的 \(10\) 这种情况直接交换是优于删除的,其它的情况直接删除就是最优的。

所以就预处理出前缀的 \(1\) 和后缀的 \(0\) 的个数,然后特判一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF = 1e18;
const int base = 1e12 + 1;
const int N = 1e6+5;
char s[N];
int pre[N],suf[N];
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%lld",&T);
	while(T--){
		scanf("%s",s+1);
		int n = strlen(s+1);
		for(int i=1; i<=n; i++)	pre[i] = pre[i-1] + (s[i] == '1');
		suf[n+1] = 0;
		for(int i=n; i>=1; i--)	suf[i] = suf[i+1] + (s[i] == '0');
		int ans = INF;
		for(int i=0; i<=n; i++){
			if(s[i] == '1' && s[i+1] == '0')	ans = min(ans,(pre[i] + suf[i+1] - 2) * base + (base - 1));
			else ans = min(ans,(pre[i] + suf[i+1]) * base);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

E.Two Tanks

题目描述:

有两个水桶 A 和 B,容量分别为 \(a,b\) 升。

顺次进行 \(n\) 次操作,每次给定一个参数 \(v_i\),若 \(v_i\ge 0\),则从 A 中倒出 \(v_i\) 升水给 B,否则从 B 中倒出 \(|v_i|\) 升水给 A。

倒水过程中,若接收桶满或倒水桶空,则立即停止。

你需要对两桶水每种可行的初始水量 \((c,d)\) 求出,以该初始水量顺次进行这 \(n\) 次操作后,A 桶的水量。

\(n\le 10^4\)\(a,b,|v|\le 1000\)

题目分析:

\(n\) 次操作看上去就没什么性质。

那么想法就是能不能预处理一些东西支持快速操作,或者把一些 \((c,d)\) 放在一起处理。

可以发现的无论我们怎么操作 \(c + d\) 的值都不会变,所以可以考虑将 \(c+d\) 一样的 \((c,d)\) 先放在一起处理,看看有什么性质。

可以发现的是如果在某一次操作中 \(c\)\(d\) 达到了其能达到的上界或下界,那么无论原来的 \((c,d)\) 是什么,最后操作的答案都是一样的。

显然的一点就是不用考虑 \(d\) 达到上下界,因为 \(c\) 达到上界意味着 \(d\) 达到下界,\(c\) 达到下界意味着 \(d\) 达到上界。

那么如果我们按 \(c\) 排序之后会发现,能达到下界的 \((c,d)\) 对应序列的一段前缀,能达到上界的 \((c,d)\) 对应序列的一段后缀,所以这一段前缀对应的答案相同,这一段后缀对应的答案相同,分别暴力做一遍即可,当然如果前后缀有交,则所有的满足条件的 \((c,d)\) 答案都相同。

而对于既没有达到上界也没有达到下界的 \((c,d)\),就是直接 \(\sum v_i\) 就是对应的影响,因为没有任何一次操作使得这个操作实际的水的变化量不等于 \(v\)

那么我们该怎么找这个前后缀呢,我们设 \(c\) 的上下界为 \([l,r]\),对于一段操作使得 \(c\) 的变化量为 \(\Delta c\),那么如果前面完全没有达到上下界的话,这个时候的 \(c\) 就是 \(c + \Delta c\) 如果此时满足 \(c + \Delta c \le l\) 也就是意味着 \(c\) 即使前面的操作没有达到上下界,也会在此时达到下界。

也就是说如果 \(c \le l - \Delta c\)\(c\) 会达到下界,\(c \ge r - \Delta c\)\(c\) 会达到上界。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int p[N][N],v[10 * N];
int solve(int a,int b,int c,int d,int n){
	for(int i=1; i<=n; i++){
		if(v[i] > 0){
			int tmp = min(c,min(v[i],b - d));
			c -= tmp,d += tmp;
		}
		else{
			int tmp = min(d,min(-v[i],a - c));
			c += tmp,d -= tmp;
		}
	}
	return c;
}
int main(){
	int n,a,b;scanf("%d%d%d",&n,&a,&b);
	for(int i=1; i<=n; i++)	scanf("%d",&v[i]);
	for(int i=0; i<=a+b; i++){  //这里都是对于 A 桶来计算的 
		int l = max(0,i - b),r = min(a,i);   // l,r 是对应的上下界 
		int L = l,R = r;  //L,R 就是前后缀 
		int sum = 0;  //A 桶的变化量 
		for(int j=1; j<=n; j++){
			sum -= v[j];
			L = max(L,l - sum);
			R = min(R,r - sum);
		}
		if(L > R){
			int ans = solve(a,b,l,i-l,n);
			for(int j=l; j<=r; j++)	p[j][i-j] = ans;
		}
		else{
			int t1 = solve(a,b,L,i-L,n);
			int t2 = solve(a,b,R,i-R,n);
			for(int j=l; j<L; j++)	p[j][i-j] = t1;
			for(int j=L; j<=R; j++)	p[j][i-j] = j + sum;
			for(int j=R+1; j<=r; j++)	p[j][i-j] = t2;
		}
	}
	for(int i=0; i<=a; i++){
		for(int j=0; j<=b; j++){
			printf("%d ",p[i][j]);
		}
		printf("\n");
	}
	return 0;
}

F.Traveling in Berland

题目描述:

  • 你在一个有 \(n\) 个点的环上顺时针前进,环上第 \(i\) 个点有两个权值 \(a_i,b_i\),分别代表从点 \(i\) 到点 \(i+1 \bmod n\) 的消耗的油和点 \(i\) 处的油价。你的车的油箱最多只能装 \(k\) 升油,对于每个点,求出从这个点开始顺时针转一圈返回这个点的最小消耗。
  • \(3 \leq n \leq 2\times 10^5,1\leq k \leq 10^9,1\leq a_i\leq k,1\leq b_i\leq2\)

题目分析:

发现这个过程没啥性质,所以考虑是不是可以预处理一些东西使得可以快速处理,或者把一些情况相同的放在一起处理。

首先环的处理可以考虑二倍环长,发现一个关键点就是 \(b_i \in [1,2]\),也就是我们有贪心策略每一次在 \(b_i = 1\) 的地方尽可能多买,然后没办法了再从 \(b_i = 2\) 的地方买。

\(b_i = 1\) 的位置构成序列 \(p\) 所以可以考虑按 \(p\) 将序列分段,一个想法就是直接处理从 \(p_i\)\(p_{i+1}\) 的最优花费,这样直接把两端拼起来看上去就很可以,但是拼起来这个事情必须要求我们在结尾的时候油量为 \(0\),这个要求其实就是不浪费油量很好做。

那么就是考虑这个最优花费怎么做,设 \(s\) 表示这一段花费的油量,则:

  • \(s \le k\),那么只需要在 \(b=1\) 的位置买 \(s\) 个即可
  • \(s > k\),则只能在 \(b=1\) 的位置买 \(k\) 个,然后在 \(b=2\) 的位置买完剩下的 \(s-k\) 个即可。

所以对于从 \(l\) 开始走走到 \(l+n\),我们可以找到 \(L,R\) 表示最靠近 \(l\)\(b = 1\) 的位置,最靠近 \(l+n\)\(b=1\) 的位置,这样我们就可以将贡献拆为:\([l,L]\)\([L,R]\)\([R,l+n]\),第一个和第三个显然可以 \(O(1)\),第二个的贡献可以通过我们预处理的贡献数组,然后通过双指针 \(O(n)\) 维护。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
int a[2 * N],b[2 * N],p[2 * N];
vector<int> v;
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 tmp = 100;
		for(int i=1; i<=n; i++)	scanf("%lld",&a[i]),a[i+n] = a[i];
		for(int i=1; i<=n; i++)	scanf("%lld",&b[i]),tmp = min(tmp,b[i]),b[i+n] = b[i];
		for(int i=1; i<=2 * n; i++)	a[i] += a[i-1];
		for(int i=1; i<=2*n; i++){
			if(b[i] == 1)	v.push_back(i);
		}
		for(int i=0; i + 1 < v.size(); i++){
			p[i] = a[v[i+1]-1] - a[v[i]-1];
			if(p[i] > k) p[i] = (p[i] - k) * 2 + k * 1;
			else	p[i] = p[i] * 1; 
		}
		if(tmp == 2){
			for(int i=1; i<=n; i++)	printf("%lld ",a[n] * 2);
			printf("\n");
		}
		else{
			int l=0,r=0,ans = 0;
			for(int L=1; L<=n; L++){
				int R = L + n;
				while(r + 1 < v.size() && v[r+1] <= R)	ans += p[r],++r;
				while(v[l] < L)	ans -= p[l],++l;
				int tmp = ans;
				tmp += (a[v[l]-1] - a[L-1]) * 2;
				int res = a[R-1] - a[v[r] - 1];
				if(res > k)	tmp += (res - k) * 2 + k * 1;
				else	tmp += res * 1;
				printf("%lld ",tmp);
			}
			printf("\n");
		}
		v.clear();
		for(int i=1; i<=2 * n; i++)	a[i] = 0;
		for(int i=0; i<v.size(); i++) p[i] = 0;
	}
	return 0;
}

G.Prediction

题目描述:

\(n\) 个人,每个人有能力值 \(a_i\)。规定一个比赛顺序 \(p_1,p_2,\dots,p_n\),第一场 \(p_1,p_2\) 比,胜者与 \(p_3\) 比,胜者与 \(p_4\) 比……,共进行 \(n-1\) 轮比赛决出最终胜者。若能力值为 \(x,y\) 的两个人比赛,若 \(|x-y|>k\) 则能力值高者胜;否则两人都有可能胜出。请计数比赛顺序的方案数,使得任意一次比赛都有唯一胜者。

题目分析:

注意题目里是对于任意一次比赛都有唯一的胜者,而显然既然是唯一的,则必然是两个人中的最大值,所以本质上我们就是对满足如下条件的排列 \(p\) 计数:

\[\begin{aligned} \forall i\in[2,n] \ \ \left| a_i - \max_{j=1}^{i-1} a_j \right| > k \end{aligned} \]

那么就可以考虑 \(dp\) 来插入数,也就是按照从大到小或者从小到大的顺序插入 \(a_i\)

如果从小到大插入,也就是说我们每一次插入都会使得一些位置的前缀 \(\max\) 发生改变,看上去就有点难以维护。

如果从大到小插入,发现其不会对任何的前缀 \(\max\) 产生影响,所以比较有前途,也就是可以直接设 \(dp_i\) 表示插入了 \((i,n]\) 的情况下合法的方案数,下面就是分类讨论一下转移:

  • 若将 \(a_i\) 插入到第一个位置,设 \(lst_i\) 表示第一个满足 \(a_i - a_{lst_i+1} > k\) 的位置,则对于 \((lst_i,i)\) 其必然只能插入到第二个位置之后,也就是:\(dp_{lst_i} + dp_i\text{A}_{n-lst_i-2}^{lst_i-i-1} \to dp_{lst_i}\)
  • 若将 \(a_i\) 插入到其它的位置,因为第一个转移的限制,所以其实插入的哪里都可以,也就是 \(dp_i \times (n-i) \to dp_{lst_i}\)

代码:

点击查看代码
#include <stdio.h>

typedef long long ll;

const int mod = 998244353;
int a[1000007], lst[1000007];
ll fac[1000007], inv_fac[1000007], dp[1000007];

inline ll quick_pow(ll x, ll p, ll mod){
	ll ans = 1;
	while (p){
		if (p & 1) ans = ans * x % mod;
		x = x * x % mod;
		p >>= 1;
	}
	return ans;
}

inline void init(int n){
	fac[0] = 1;
	for (int i = 1; i <= n; i++){
		fac[i] = fac[i - 1] * i % mod;
	}
	inv_fac[n] = quick_pow(fac[n], mod - 2, mod);
	for (int i = n - 1; i >= 0; i--){
		inv_fac[i] = inv_fac[i + 1] * (i + 1) % mod;
	}
}

inline int read(){
	int ans = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9'){
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9'){
		ans = ans * 10 + (ch ^ 48);
		ch = getchar();
	}
	return ans;
}

inline ll arrange(int n, int m){
	if (m == 0) return 1;
	if (n < 0 || m < 0 || n < m) return 0;
	return fac[n] * inv_fac[n - m] % mod;
}

int main(){
	int n = read(), k = read();
	init(n);
	for (int i = 1; i <= n; i++){
		a[i] = read();
	}
	for (int i = 1; i <= n; i++){
		lst[i] = lst[i - 1];
		while (a[i] - a[lst[i] + 1] > k) lst[i]++;
	}
	dp[n] = 1;
	for (int i = n; i >= 1; i--){
		dp[i - 1] = (dp[i - 1] + dp[i] * (n - i) % mod) % mod;
		dp[lst[i]] = (dp[lst[i]] + dp[i] * arrange(n - lst[i] - 2, i - lst[i] - 1) % mod) % mod;
	}
	printf("%lld", dp[0]);
	return 0;
}