CF1557和CF1530部分题解

前言

CF1557 是第一场 VP 前一天晚上的比赛。CF1530 是第一场 VP,题目没有全部做完。这里把我写了代码或者从同学那里学习的算法放上来,以供参考。

CF1557

四题,A-D,E 是个交互题很有意思,有时间会去做,做完补在这里。

CF1557A

给定一个序列,将这个序列分成两个子序列。使得两个子序列的所有数的平均数之和最大。求最大值。n3×105n\leqslant 3\times 10^5

考虑贪心的做法。把最大的那个数单独放起来,其他的所有数放在一起。假定数组已经小到大排序好,最大的为 a1a_1,答案则为:

a1+i=2nann1a_1+\dfrac{\sum_{i=2}^n a_n}{n-1}

考虑证明:首先如果同时存在多个最大值,把几个最大值放在一起显然不行,因为这些最大值造成的贡献为 00,反而放在另外一组可以把较小的数平均值拉上来;如果将最大值和别的数字放在一起,对于这一个序列而言,别的数字造成了负贡献,但是因为最大值在这个序列,另一个序列无论如何平均值无法超过最大值,从而无法弥补负贡献。

因此最大值单独一组,别的另一组是最优。因为我只关心最大值是什么,不需要排序。整体复杂度 O(n)\operatorname{O}(n) 级别,绰绰有余。

代码如下:

#include<cstdio>
#include<string.h>
#include<iostream>
#define int long long
const int max_n=100005;
inline int read(){
	int x=0;bool w=0;char c=getchar();
	while(c<'0' || c>'9') w|=c=='-',c=getchar();
	while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return w?-x:x;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10^48);
}
inline int min(int a,int b){return a<b?a:b;}
inline int max(int a,int b){return a>b?a:b;}

int n,a[max_n],mx,b;

signed main(){
	int T=read();
	while(T--){
		n=read(),mx=-2000000000,b=0; // 最大值初始化为极小值
		for(register int i=1;i<=n;++i)
			a[i]=read(),mx=max(mx,a[i]); // O(n) 找最大值,和输入放一起了
		bool fl=0;
		for(register int i=1;i<=n;++i){
			if(a[i]==mx && !fl){ // 注意只要放一个最大值,开 fl 记录一下是不是放一个了
				fl=1;
				continue;
			}
			b+=a[i]; // 其他的数字放到另一个序列,求和
		}
		double p=mx+1.0*b/(n-1); // 平均数之和的式子,见上。
		printf("%.10lf\n",p);
	}
	return 0;
}

CF1557B

判断是否存在一个方案,将一个长度为 nn 的数组(所有元素各不同)划分成恰好 kk 段,使得重新排列这 kk 段后使得原数组为严格上升序列。只需要判断可不可以,kn3×105k\leqslant n\leqslant 3\times 10^5

因为 nn 的数值相对不大,可以用 O(nlogn)\operatorname{O}(n\log n) 的做法解决。因为数字范围在 int 之内不好琢磨,很容易想到先离散化。排序后重新编号,使得值域范围为 [1,n][1,n]

此时对于某一个 ii,如果有 ai+1ai+1a_{i+1}\neq a_i+1,说明这两个数直接是缺了东西的,或者这两个数是下降趋势的,显然不可以放在一组。如果相等的话,显然这两个数已经符合题意,可以放在一组。按照这种方法,我们求出要几组。

注意组数是最少需要划分的组数,因为即使两个数最开始就已经符合条件,我还是可以把他们分来,只不过排完序这两个数相对位置不改变。因此只要组数小于等于 kk,就可以完成,否则不可以完成。

代码如下:

#include<cstdio>
#include<string.h>
#include<algorithm>
#define int long long
const int max_n=1000005;
inline int read(){
	int x=0;bool w=0;char c=getchar();
	while(c<'0' || c>'9') w|=c=='-',c=getchar();
	while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return w?-x:x;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10^48);
}

int n,k,a[max_n];

struct data{ // 离散化用到的数组
	int a,d;
}b[max_n];

inline bool cmp1(data a,data b){
	return a.a<b.a;
}
inline bool cmp2(data a,data b){
	return a.d<b.d;
}

signed main(){
	int T=read();
	while(T--){
		n=read(),k=read();
		for(register int i=1;i<=n;++i){
			a[i]=read();
			b[i].a=a[i],b[i].d=i;
		}
		std::sort(b+1,b+1+n,cmp1); // 得到每个数大小关系
		for(register int i=1;i<=n;++i)
			b[i].a=i; // 重新编号
		std::sort(b+1,b+1+n,cmp2);
		for(register int i=1;i<=n;++i)
			a[i]=b[i].a; // 对原数组重新赋值
		
		int cnt=1; // 注意这样子统计的是分割数,根据植树问题,一共分成了隔板数+1段
		for(register int i=1;i<n;++i)
			if(a[i]+1!=a[i+1])
				++cnt;
		if(cnt<=k) puts("Yes");
		else puts("No");
	}
	return 0;
}

CF1557C

求有多少种给一个长度为 nn 的序列赋值的方式,满足所有数都是自然数,且严格小于 2k2^k,且满足这个序列所有数的按位与和大于等于异或和。求方案数。n,k2×105n,k\leqslant 2\times 10^5

考虑到按位与的特性,两个数都是 11 这一位才会是 11。而这种情况下这两位异或起来是 00。考虑得更全面一些,如果有奇数个 11 的话,这一位与起来和异或和相同;如果有偶数个 11 的话,这一位与起来大于异或和;

从最高位往低位看,显然如果这一位已经比异或和大了,后面的位就不用考虑了,后面乱填有几种做法呢。假设有 mm 位可以随便填,那么就有 2m2^m 种做法。

而这一位在 nn 个里面我要保证有奇数个 11 或者偶数个 11,不就是组合数?

因此不难推出式子。因为 nn 比较小,可以处理阶乘和逆元,卢卡斯定理不需要用。

代码:

#include<cstdio>
#include<string.h>
#include<iostream>
#define int long long

const int max_n=200005,mod=1000000007;
inline int read(){
	int x=0;bool w=0;char c=getchar();
	while(c<'0' || c>'9') w|=c=='-',c=getchar();
	while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return w?-x:x;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10^48);
}

inline int mi(int a,int p){ // 快速幂
	int res=1;
	while(p){
		if(p&1) res*=a,res%=mod;
		a*=a,a%=mod,p>>=1;
	}
	return res%mod;
}

int jc[max_n],iv[max_n]; // jc是阶乘,iv是逆元

inline void init(){ // 预处理
	jc[0]=1,iv[0]=1;
	for(register int i=1;i<=200000;++i){
		jc[i]=jc[i-1]*i%mod;
		iv[i]=mi(jc[i],mod-2)%mod;
	}
}

inline int C(int n,int m){ // 组合数
	return (jc[n]*iv[m]%mod*iv[n-m]%mod+mod)%mod;
}

int n,k;

signed main(){
	init();
	int T=read();
	while(T--){
		n=read(),k=read();int t=0,ans=0;
		for(register int i=0;i<n;i+=2)
			t+=C(n,i),t%=mod;
		if(n&1) t+=C(n,n),t%=mod;
// 分奇偶性讨论,因为偶数个1填满和奇数个1填满效果不同
		if(n&1){
			ans=mi(t,k),ans%=mod;
		}
		else{
			int tmp=1;
			for(register int i=k;i;--i){
				ans+=tmp*C(n,n)%mod*mi(2,(i-1)*n)%mod,ans%=mod;
				tmp*=t,tmp%=mod;
			}
			ans+=tmp,ans%=mod;
		}
		write(ans%mod),putchar('\n');
	}
	return 0;
}

CF1557D

给定一个 nn10910^9 列的 010-1 矩阵,最少要删掉多少行(删掉一行时,上下两行会放在一起),使得任意一行的上面和下面(如果是最上面一行,则只需要和下面;最下面一行同理)都有至少同一列都是 11。(n3×105n\leqslant 3\times 10^5,数据输入以某一行有哪些区间都是 11 的形式给出,区间个数与 nn 同级)

不难想到一个贪心算法,先把最开始连起来的行合并。然后如果这一行上面和下面都接通不了,根据这一行是多少个行合并得到为权值,删除权值小的。

但是这个贪心有较大的问题。例如对于第 ii 行,如果第 i1i-1 行和第 i+1i+1 行有共同的 11,且权值上满足 ai1aiai+1a_{i-1}\leqslant a_i\leqslant a_{i+1},则会删除前两个,实际上删除第 ii 行可能更优。

如果一个题很像贪心但是贪心做不出来,就说明这个题大概率是动态规划。

不妨假设 dpidp_i 表示我当前正在处理前 ii 行,且删完后第 ii 行是最后一行的最小删除行数。显然它可以从前面的任何一个和 ii 有相同列的 11 的行转移过来。但是中间所有的行都要删掉。第 ii 行和第 jj 行之间(不含边界)有 ij1i-j-1 行。因此有转移方程:

dpi=minj{dpj+(ij1)k,ai,k=aj,k=1}dp_i=\min_{j}\{dp_j+(i-j-1) \mid \exists k,a_{i,k}=a_{j,k}=1 \}

由于我还没上高中我也不知道可不可以这么写递推式www

显然这一波 DP 是 n2n^2 级别的。考虑优化。这个 DP 优化肯定只能从求最小值下手,一想到动态维护区间最小值,nlognn\log n 复杂度级别允许的话,很快就想到了功能强大的线段树。

因为这个还和 ii 有关,看起来很棘手,但是发现 ii 是可以提出来的。也就是说原转移方程为:

dpi=i+minj{dpjj1k,ai,k=aj,k=1}dp_i=i+\min_{j}\{dp_j-j-1 \mid \exists k,a_{i,k}=a_{j,k}=1 \}

当然 1-1 也可以提出来,但是这不是重点所在。现在考虑怎么用线段树维护 minj{dpjj1k,ai,k=aj,k=1}\min_{j}\{dp_j-j-1 \mid \exists k,a_{i,k}=a_{j,k}=1 \},可以用 vector 存储下每一行有 11 的区间并离散化。离散化的时候注意,也要同时把 l1l-1r+1r+1 离散化进去,不然会有隐性的查询查错的风险。

每次处理完成 dpidp_i 的时候,更新这一行是 11 的那一堆区间。查询的时候,查询每一行有 11 的区间,如果后续有一行和这里都有 11 的话,这个区间就会被查询到,所以不存在区间遗漏问题。虽然看起来是每一行的每一个区间都要进行,但是因为离散化了数组并不会开不下,而且这样跑一遍总的复杂度是区间个数,和输入同级。带上一个线段树的 log\log,还是绰绰有余。

没有写代码,是大佬教我的QAQ。

CF1530

四题,ABCE,虽然 D 也做出来了,但是只是感性理解了题解的做法,我还没有透彻分析,所以没有。额外还有原本的 vp 的题目,做了第一题没来得及交,应该是对的。

因为不是正题,这里简单说一下多的那个题,CF1546A,无解的情况就是两个数组的和不同。有解的情况只需要不断地把比目标大的数移到比目标小的数上即可。

CF1530A

给定一个数 nn,将其表示成若干个十进制数之和,且这些十进制数每一位必须是 1100,最少拆分为几个数。只需要输出个数,nn 在 int 范围内。

显然可以 11111111111\cdots 111 不断地加下去,形式上就是每一位上的数都加 11。如果这一位的数字已经 OK 了,就把后面加的数的这一位变成 00,如 110111111110111\cdots 111。显然答案是 nn 的十进制位中每一位数字的最大值。

代码:

#include<bits/stdc++.h>
using namespace std;

inline int read(){
	int x=0;bool w=0;char c=getchar();
	while(c<'0' || c>'9') w|=c=='-',c=getchar();
	while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return w?-x:x;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10^48);
}



signed main(){
	int T=read();
	while(T--){
		int n=read(),x=-1;
		while(n){ // 十进制位的数字拆分
			x=max(x,n%10);
			n/=10;
		}
		write(x),putchar('\n');
	}
	return 0;
}

CF1530B

给定一个 nmn\ast m 的矩阵,在矩阵的边缘上放 11,使得任意两个 11 不相邻(八连通),输出任意一种方案使得放 11 最大(3n,m203\leqslant n,m\leqslant 20)。

很明显的一个贪心,我们把边缘分成四个部分:上面,左边,下面,右边。值得注意的是,有的部分是有一个格子的交集的。如上面和右边,共用了一个右上角的格子,因此要注意一下。

首先处理上面,这个时候没什么约束,直接一个隔一个放。

for(register int i=1;i<=m;i+=2) a[1][i]=1;

然后考虑最下面,因为最上面和最下面没有交集(注意到 n,m3n,m\geqslant 3),也是隔一个放一个,这个时候要注意一点,如果这里隔一个放一个,左右的处理上下界就都要特殊约定;如果这里直接从第 33 列开始放到第 m2m-2 列,左右的处理上下界就没那么麻烦。虽然两者差不多,但是我选择的第二种。

for(register int i=3;i<=m-2;i+=2) a[n][i]=1;

然后处理左右,这个时候左右可以一起处理,反正是对称的,因为处理上面的时候已经用到了公共格子部分,所以左右的时候循环从 33 开始,因为处理下面的时候专门给他让了位置,所以不必追究哪里结束。

for(register int i=3;i<=n;i+=2) a[i][1]=a[i][m]=1;

总的代码如下:

#include<bits/stdc++.h>
using namespace std;
const int max_n=25;
inline int read(){
	int x=0;bool w=0;char c=getchar();
	while(c<'0' || c>'9') w|=c=='-',c=getchar();
	while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return w?-x:x;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10^48);
}

int n,m,a[max_n][max_n];

signed main(){
	int T=read();
	while(T--){
		n=read(),m=read();
		memset(a,0,sizeof(a)); // 这后面的处理上面一步一步提到了
		for(register int i=1;i<=m;i+=2)
			a[1][i]=1;
		for(register int i=3;i<=m-2;i+=2)
			a[n][i]=1;
		for(register int i=3;i<=n;i+=2)
			a[i][1]=a[i][m]=1;
		for(register int i=1;i<=n;++i){ // 输出
			for(register int j=1;j<=m;++j)
				write(a[i][j]);
			putchar('\n');
		}
		putchar('\n');
	}
	return 0;
}

CF1530C

两个人进行比赛,每轮两人都会得到一个 [0,100][0,100] 的正整数。假设进行了 kk 轮,则每个人的最终得分为其各轮得分的前 kk4k-\left \lfloor \frac{k}{4} \right \rfloor 大的分数之和。现在已经进行了 nn 轮。求至少还要经过多少轮,第一名选手的分数才能赶上(大于等于)第二名选手。(n105n\leqslant 10^5

注意如果 nn 轮后已经到第二个人分数的时候,答案为 00,我进行了特判。

有一个很显然的贪心,那就是后面进行的回合,第一个人都是 100100 分,第二个人都是 00 分。如果提前把两个人前 nn 轮的数组排个序,统计前缀和,就能很快得到再进行 kk 轮两人的分数。不妨二分查找这个 kk 值。每次如果第一个人分数没有赶上第二个人,另 lmid+1l\leftarrow mid+1,否则另 rmid1r\leftarrow mid-1

第二个人后续都是 00 分,所以前缀和的时候可以再处理一个 nnn<i2n,sumi=sumn\forall n<i\leqslant 2n,sum_i=sum_n。以方便我们的处理。不过还有一个方法就是如果 i>ni>n,直接用 sumnsum_n 的数据就好,没有必要提前多跑一个 nn

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int max_n=1000005;
inline int read(){
	int x=0;bool w=0;char c=getchar();
	while(c<'0' || c>'9') w|=c=='-',c=getchar();
	while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return w?-x:x;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10^48);
}

int n,a[max_n],b[max_n];

int s1[max_n],s2[max_n];

inline bool cmp(int a,int b){
	return a>b;
}

inline bool ck(int k){
    // m 表示取前 m 次比赛成绩
	int m=(n+k)-(n+k)/4;
	return k*100+s1[m-k]<s2[min(m,n)];
}

inline int bs(int l,int r){ // 二分板子
	while(l<=r){
		int mid=(l+r)>>1;
		if(ck(mid)) l=mid+1;
		else r=mid-1;
	}
	return l;
}

signed main(){
	int T=read();
	while(T--){
		n=read();
		for(register int i=1;i<=n;++i)
			a[i]=read();
		for(register int i=1;i<=n;++i)
			b[i]=read();
		sort(a+1,a+1+n,cmp),
		sort(b+1,b+1+n,cmp); // 按大到小排序
		for(register int i=1;i<=n;++i)
			s1[i]=s1[i-1]+a[i];
		for(register int i=1;i<=n;++i)
			s2[i]=s2[i-1]+b[i];
		if(s1[n-n/4]>=s2[n-n/4]){ // 特判
			puts("0");
			continue;
		}
		write(bs(0,n)),putchar('\n');
	}
	return 0;
}

CF1530E

给定一个小写字母组成的字符串 ss,将其重新排列,使得重排后的字符串的所有前缀的 failfail 值(见 kmp 做法的 fail,有的地方叫 next)的最大值最小。求重排后的字符串,如果有多种方法,输出字典序最小的。s105\left| s\right|\leqslant 10^5

显然是一个构造题。围绕字典序最小这个信息,可以先开个桶记录原串每个字母出现次数。方便直接从 a 到 z 跑。为了方便我们记这个最大值的最小值为 ff。然后进行分类讨论(讨论是递进的,也就是讨论到第 ii 种情况则默认前面 1i11\sim i-1 的情况都不符合):

  1. 如果给定的原串只由一种字母组成,显然重排没有什么用,只能输出原串。
  2. 如果给定的所有字母互不相同,显然不管怎么排 ff 都为 00,因此直接按 a 到 z 给字符串排序即可。
  3. 如果给定的字母中存在有的字母只出现了一次,找到字典序最小的那个只存在一次的字母,放在第一个,使得后面的 fail 全部是 00。剩下的排序输出即可。整体的 f=0f=0。如果放一个出现过多次的字母在第一个,后面一定会有几处 fail=1fail=1,此时 ff 至少为 11,没有那么做优。
  4. 如果字典序最小的那个字母出现数量小于 s+12\dfrac{\left| s\right|+1}{2},这个时候发现轮流放一个这个字母和另一个字母,最后字典序最小的这个字母会先用完。如果第一个是这个字母的话,至少后面的 failfail 就都是 00 了。但是前面如果都是轮流放,failfail 大小会较大。考虑最开始先放一个字典序最小的字母,再依次轮流放字典序最小的字母和别的字母,f=1f=1
  5. 否则,这样子轮流放的话会有多的字典序最小字母剩余,放在后面会导致 f=2f=2。但是如果只有两种字母,不妨直接第一个放字典序最小的,然后把别的字母全部放后面,再把字典序最小的字母补在后面,f=1f=1
  6. 如果有多种字母,考虑第三小的字母可以怎么放。如果第一个放字典序最小的,把字典序第二小的放一个在第二个,后面再补上字典序最小的。此时这一部分的 fail=1fail=1,后面补充一个字典序第三小的字母,然后再把没补上的字母放在后面。此时 f=1f=1,且字典序理论最小。

分类讨论的过程比较冗杂。代码如下:

#include<bits/stdc++.h>
using namespace std;
const int max_n=100005;;

int n,cnt[max_n],tot;
string a;

char tmp[max_n];
bool ok1,ok2,ok3,ok4;

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);

	int T;cin>>T;
	while(T--){
		memset(cnt,0,sizeof(cnt));
		memset(tmp,0,sizeof(tmp));
		ok1=ok2=1,ok3=ok4=0;tot=n=0;
		a.clear();
		cin>>a;
		n=a.size();
		for(register int i=0;i<n;++i){
			int x=(int)(a[i]-'a'+1);
			if(!cnt[x]) ++tot;
			++cnt[x];
			if(cnt[x]>1)
				ok1=0;
			if(cnt[x]!=i+1)
				ok2=0;
		}
		int frst=0;
		for(register int i=1;i<=26;++i){
			if(cnt[i] && !frst) frst=i;
			if(i==frst && cnt[i]<=(n+2)/2 && !ok4)
            // 因为下标从0开始,所以 (n+1)/2 改为 (n+2)/2
				ok4=1;
			if(cnt[i]==1 && !ok3)
				ok3=1;
		}

		if(ok1){ // 所有字母不同,直接sort
			for(register int i=0;i<n;++i)
				tmp[i]=a[i];
			sort(tmp,tmp+n);
			cout<<tmp<<"\n";
			continue;
		}
		if(ok2){ // 只有一种字母,输出原串
			cout<<a<<"\n";
			continue;
		}
		if(ok3){ // 有字母出现次数为 1
			int np=0;
			for(register int i=1;i<=26;++i)
				if(cnt[i]==1){
					np=i;
					--cnt[i];
					cout<<(char)(i-1+'a');
					break;
				}
			for(register int i=1;i<=26;++i)
				if(i!=np && cnt[i])
					while(cnt[i]--)
						cout<<(char)(i-1+'a');
			cout<<"\n";
			continue;
		}
		if(ok4){ // 字典序最小的字母出现次数小于 (n+1)/2
			cnt[frst]-=2;
			cout<<(char)(frst-1+'a')<<(char)(frst-1+'a');
			for(register int i=1;i<=26;++i)
				if(i!=frst && cnt[i])
					while(cnt[i]--){
						cout<<(char)(i-1+'a');
						if(cnt[frst]){
							--cnt[frst];
							cout<<(char)(frst-1+'a');
						}
					}
			cout<<"\n";
			continue;
		}
		if(tot==2){ // 只有两种字母
			--cnt[frst];
			cout<<(char)(frst-1+'a');
			for(register int i=1;i<=26;++i)
				if(i!=frst && cnt[i])
					while(cnt[i]--)
						cout<<(char)(i-1+'a');
			while(cnt[frst]--)
				cout<<(char)(frst-1+'a');
			cout<<"\n";
			continue;
		}
        // 以上条件均不成立(分类讨论的第六种情况)
		int secd=0;
		--cnt[frst];
		cout<<(char)(frst-1+'a');
		for(register int i=1;i<=26;++i)
			if(i!=frst && cnt[i]){
				secd=i;
				--cnt[i];
				cout<<(char)(i-1+'a');
				break;
			}
		while(cnt[frst]--)
			cout<<(char)(frst-1+'a');
		for(register int i=1;i<=26;++i)
			if(cnt[i]>0 && i!=frst && i!=secd){
				--cnt[i];
				cout<<(char)(i-1+'a');
				break;
			}
		for(register int i=1;i<=26;++i)
			if(i!=frst && cnt[i])
				while(cnt[i]--)
					cout<<(char)(i-1+'a');
		cout<<"\n";
	}
	return 0;
}

后记

两次比赛的整体发挥大体都比较正常。CF1557 的线段树优化 DP 没有想到。考场上对那些题目我的感觉就是会做的都会做,不会做的都不会做,就很离谱。