【题解】Educational Codeforces Round 145(CF1809)

A.Garland

题目描述:

4 只灯泡,第 i 只灯泡的颜色为 si

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

题目描述:

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

  • 1000ai1000
  • 序列有恰好 k 个子串的和为正数
  • 剩余 n(n+1)2k 个子串的和都是负数

你只需要输出任意一组解。
n30

题目分析:

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

但是如果 kx(x+1)2 其实也是简单的,如果我们找到最大的 x 使得 k<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. 交换相邻的两个元素,此操作需要花费 1012 个金币。

  2. 删除某个元素,此操作需要花费 1012+1 个金币。

你要通过以上两种操作使这个字符串单调不降,求最小金币花费。
1n3×105

题目分析:

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

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

但是如果直接按逆序对个数判断是不是应该交换的话就很难受,不好实现,但是注意到我们是一个 01 字符串,也就是说我们排序之后一定是形如 00000001111111,也就是说我们可以直接枚举 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 次操作,每次给定一个参数 vi,若 vi0,则从 A 中倒出 vi 升水给 B,否则从 B 中倒出 |vi| 升水给 A。

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

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

n104a,b,|v|1000

题目分析:

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

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

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

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

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

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

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

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

也就是说如果 clΔcc 会达到下界,crΔcc 会达到上界。

代码:

点击查看代码
#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 个点有两个权值 ai,bi,分别代表从点 i 到点 i+1modn 的消耗的油和点 i 处的油价。你的车的油箱最多只能装 k 升油,对于每个点,求出从这个点开始顺时针转一圈返回这个点的最小消耗。
  • 3n2×105,1k109,1aik,1bi2

题目分析:

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

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

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

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

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

所以对于从 l 开始走走到 l+n,我们可以找到 L,R 表示最靠近 lb=1 的位置,最靠近 l+nb=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 个人,每个人有能力值 ai。规定一个比赛顺序 p1,p2,,pn,第一场 p1,p2 比,胜者与 p3 比,胜者与 p4 比……,共进行 n1 轮比赛决出最终胜者。若能力值为 x,y 的两个人比赛,若 |xy|>k 则能力值高者胜;否则两人都有可能胜出。请计数比赛顺序的方案数,使得任意一次比赛都有唯一胜者。

题目分析:

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

i[2,n]  |aimaxj=1i1aj|>k

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

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

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

  • 若将 ai 插入到第一个位置,设 lsti 表示第一个满足 aialsti+1>k 的位置,则对于 (lsti,i) 其必然只能插入到第二个位置之后,也就是:dplsti+dpiAnlsti2lstii1dplsti
  • 若将 ai 插入到其它的位置,因为第一个转移的限制,所以其实插入的哪里都可以,也就是 dpi×(ni)dplsti

代码:

点击查看代码
#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;
}
posted @   linyihdfj  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
历史上的今天:
2022-09-05 【题解】做题记录(2022.9)
点击右上角即可分享
微信分享提示