前言
CF1557 是第一场 VP 前一天晚上的比赛。CF1530 是第一场 VP,题目没有全部做完。这里把我写了代码或者从同学那里学习的算法放上来,以供参考。
CF1557
四题,A-D,E 是个交互题很有意思,有时间会去做,做完补在这里。
CF1557A
给定一个序列,将这个序列分成两个子序列。使得两个子序列的所有数的平均数之和最大。求最大值。。
考虑贪心的做法。把最大的那个数单独放起来,其他的所有数放在一起。假定数组已经小到大排序好,最大的为 ,答案则为:
考虑证明:首先如果同时存在多个最大值,把几个最大值放在一起显然不行,因为这些最大值造成的贡献为 ,反而放在另外一组可以把较小的数平均值拉上来;如果将最大值和别的数字放在一起,对于这一个序列而言,别的数字造成了负贡献,但是因为最大值在这个序列,另一个序列无论如何平均值无法超过最大值,从而无法弥补负贡献。
因此最大值单独一组,别的另一组是最优。因为我只关心最大值是什么,不需要排序。整体复杂度 级别,绰绰有余。
代码如下:
#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
判断是否存在一个方案,将一个长度为 的数组(所有元素各不同)划分成恰好 段,使得重新排列这 段后使得原数组为严格上升序列。只需要判断可不可以,。
因为 的数值相对不大,可以用 的做法解决。因为数字范围在 int 之内不好琢磨,很容易想到先离散化。排序后重新编号,使得值域范围为 。
此时对于某一个 ,如果有 ,说明这两个数直接是缺了东西的,或者这两个数是下降趋势的,显然不可以放在一组。如果相等的话,显然这两个数已经符合题意,可以放在一组。按照这种方法,我们求出要几组。
注意组数是最少需要划分的组数,因为即使两个数最开始就已经符合条件,我还是可以把他们分来,只不过排完序这两个数相对位置不改变。因此只要组数小于等于 ,就可以完成,否则不可以完成。
代码如下:
#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
求有多少种给一个长度为 的序列赋值的方式,满足所有数都是自然数,且严格小于 ,且满足这个序列所有数的按位与和大于等于异或和。求方案数。。
考虑到按位与的特性,两个数都是 这一位才会是 。而这种情况下这两位异或起来是 。考虑得更全面一些,如果有奇数个 的话,这一位与起来和异或和相同;如果有偶数个 的话,这一位与起来大于异或和;
从最高位往低位看,显然如果这一位已经比异或和大了,后面的位就不用考虑了,后面乱填有几种做法呢。假设有 位可以随便填,那么就有 种做法。
而这一位在 个里面我要保证有奇数个 或者偶数个 ,不就是组合数?
因此不难推出式子。因为 比较小,可以处理阶乘和逆元,卢卡斯定理不需要用。
代码:
#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
给定一个 行 列的 矩阵,最少要删掉多少行(删掉一行时,上下两行会放在一起),使得任意一行的上面和下面(如果是最上面一行,则只需要和下面;最下面一行同理)都有至少同一列都是 。(,数据输入以某一行有哪些区间都是 的形式给出,区间个数与 同级)
不难想到一个贪心算法,先把最开始连起来的行合并。然后如果这一行上面和下面都接通不了,根据这一行是多少个行合并得到为权值,删除权值小的。
但是这个贪心有较大的问题。例如对于第 行,如果第 行和第 行有共同的 ,且权值上满足 ,则会删除前两个,实际上删除第 行可能更优。
如果一个题很像贪心但是贪心做不出来,就说明这个题大概率是动态规划。
不妨假设 表示我当前正在处理前 行,且删完后第 行是最后一行的最小删除行数。显然它可以从前面的任何一个和 有相同列的 的行转移过来。但是中间所有的行都要删掉。第 行和第 行之间(不含边界)有 行。因此有转移方程:
由于我还没上高中我也不知道可不可以这么写递推式www
显然这一波 DP 是 级别的。考虑优化。这个 DP 优化肯定只能从求最小值下手,一想到动态维护区间最小值, 复杂度级别允许的话,很快就想到了功能强大的线段树。
因为这个还和 有关,看起来很棘手,但是发现 是可以提出来的。也就是说原转移方程为:
当然 也可以提出来,但是这不是重点所在。现在考虑怎么用线段树维护 ,可以用 vector 存储下每一行有 的区间并离散化。离散化的时候注意,也要同时把 和 离散化进去,不然会有隐性的查询查错的风险。
每次处理完成 的时候,更新这一行是 的那一堆区间。查询的时候,查询每一行有 的区间,如果后续有一行和这里都有 的话,这个区间就会被查询到,所以不存在区间遗漏问题。虽然看起来是每一行的每一个区间都要进行,但是因为离散化了数组并不会开不下,而且这样跑一遍总的复杂度是区间个数,和输入同级。带上一个线段树的 ,还是绰绰有余。
没有写代码,是大佬教我的QAQ。
CF1530
四题,ABCE,虽然 D 也做出来了,但是只是感性理解了题解的做法,我还没有透彻分析,所以没有。额外还有原本的 vp 的题目,做了第一题没来得及交,应该是对的。
因为不是正题,这里简单说一下多的那个题,CF1546A,无解的情况就是两个数组的和不同。有解的情况只需要不断地把比目标大的数移到比目标小的数上即可。
CF1530A
给定一个数 ,将其表示成若干个十进制数之和,且这些十进制数每一位必须是 或 ,最少拆分为几个数。只需要输出个数, 在 int 范围内。
显然可以 不断地加下去,形式上就是每一位上的数都加 。如果这一位的数字已经 OK 了,就把后面加的数的这一位变成 ,如 。显然答案是 的十进制位中每一位数字的最大值。
代码:
#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
给定一个 的矩阵,在矩阵的边缘上放 ,使得任意两个 不相邻(八连通),输出任意一种方案使得放 最大()。
很明显的一个贪心,我们把边缘分成四个部分:上面,左边,下面,右边。值得注意的是,有的部分是有一个格子的交集的。如上面和右边,共用了一个右上角的格子,因此要注意一下。
首先处理上面,这个时候没什么约束,直接一个隔一个放。
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;
总的代码如下:
#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
两个人进行比赛,每轮两人都会得到一个 的正整数。假设进行了 轮,则每个人的最终得分为其各轮得分的前 大的分数之和。现在已经进行了 轮。求至少还要经过多少轮,第一名选手的分数才能赶上(大于等于)第二名选手。()
注意如果 轮后已经到第二个人分数的时候,答案为 ,我进行了特判。
有一个很显然的贪心,那就是后面进行的回合,第一个人都是 分,第二个人都是 分。如果提前把两个人前 轮的数组排个序,统计前缀和,就能很快得到再进行 轮两人的分数。不妨二分查找这个 值。每次如果第一个人分数没有赶上第二个人,另 ,否则另 。
第二个人后续都是 分,所以前缀和的时候可以再处理一个 ,。以方便我们的处理。不过还有一个方法就是如果 ,直接用 的数据就好,没有必要提前多跑一个 。
代码如下:
#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
给定一个小写字母组成的字符串 ,将其重新排列,使得重排后的字符串的所有前缀的 值(见 kmp 做法的 fail,有的地方叫 next)的最大值最小。求重排后的字符串,如果有多种方法,输出字典序最小的。。
显然是一个构造题。围绕字典序最小这个信息,可以先开个桶记录原串每个字母出现次数。方便直接从 a 到 z 跑。为了方便我们记这个最大值的最小值为 。然后进行分类讨论(讨论是递进的,也就是讨论到第 种情况则默认前面 的情况都不符合):
- 如果给定的原串只由一种字母组成,显然重排没有什么用,只能输出原串。
- 如果给定的所有字母互不相同,显然不管怎么排 都为 ,因此直接按 a 到 z 给字符串排序即可。
- 如果给定的字母中存在有的字母只出现了一次,找到字典序最小的那个只存在一次的字母,放在第一个,使得后面的 fail 全部是 。剩下的排序输出即可。整体的 。如果放一个出现过多次的字母在第一个,后面一定会有几处 ,此时 至少为 ,没有那么做优。
- 如果字典序最小的那个字母出现数量小于 ,这个时候发现轮流放一个这个字母和另一个字母,最后字典序最小的这个字母会先用完。如果第一个是这个字母的话,至少后面的 就都是 了。但是前面如果都是轮流放, 大小会较大。考虑最开始先放一个字典序最小的字母,再依次轮流放字典序最小的字母和别的字母,。
- 否则,这样子轮流放的话会有多的字典序最小字母剩余,放在后面会导致 。但是如果只有两种字母,不妨直接第一个放字典序最小的,然后把别的字母全部放后面,再把字典序最小的字母补在后面,。
- 如果有多种字母,考虑第三小的字母可以怎么放。如果第一个放字典序最小的,把字典序第二小的放一个在第二个,后面再补上字典序最小的。此时这一部分的 ,后面补充一个字典序第三小的字母,然后再把没补上的字母放在后面。此时 ,且字典序理论最小。
分类讨论的过程比较冗杂。代码如下:
#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 没有想到。考场上对那些题目我的感觉就是会做的都会做,不会做的都不会做,就很离谱。