Codeforces Round #678 (Div. 2)A-E题解
比赛链接:https://codeforces.com/contest/1436
A题
水题,简单规律总结
题意为,给定一个长度为n的数列a[]。然后对于x取1到n,对于所有的满足x<=y<=n的y,累加a[y]/y(不取整,取确切的实数值),问能否通过改变原数列中数字的排列顺序,使得上述累加和等于m。
结合样例,自己稿纸上写一下,会发现这是个很花哨的题。对于a[i]这个数来说,每次它被计算都是a[i]/i,而它会被计算i次。所以最后的和仍然是a[i],也就是说整个数列不论怎么排列,最后的结果都是数列中所有值的累加和。
因此直接判断下整个数列的和是否等于m即可。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
int n,m;
cin>>n>>m;
ll sum=0;
for(int i=0;i<n;i++)
{
ll x;
cin>>x;
sum+=x;
}
if(sum==m) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
B题
构造
题意为给定一个整数n,要求你输出一个n × \times ×n的整数矩阵,满足每一行和每一列的数加起来,都是质数。
这里的想法,当然是怎么简单怎么来。最小的质数是2和3,而且2和3值刚好相邻。我们简化我们构造的矩阵,只考虑0和1的话,对于每一行或者每一列来说,它都会经过两条对角线。因此我们只需要在两条对角线上全部放置1,其余位置放置0,即可让每一行和每一列的和加起来都为2了。
但是当n为奇数的时候,中间一行和中间一列上,两条对角线相交了,导致只有一个1,如下:
1 0 0 0 1
0 1 0 1 0
0 0 1 0 0
0 1 0 1 0
1 0 0 0 1
此时我们在左侧的中间和上侧的中间补上一个1,使得中间行和中间列也变为2,左侧行和上侧列此时相加为3,仍满足条件:
1 0 1 0 1
0 1 0 1 0
1 0 1 0 0
0 1 0 1 0
1 0 0 0 1
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
bool field[107][107];
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
memset(field,0,sizeof(field));
int n;
cin>>n;
for(int i=1;i<=n;i++) field[i][i]=field[i][n-i+1]=1;//两条对角线上放置1
if(n&1) field[n/2+1][1]=field[1][n/2+1]=1;//特判n为奇数的情况,此时中间排和中间列只有一个1,在左侧中间和上测中间各加一个1即可
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) cout<<field[i][j]<<' ';
cout<<endl;
}
}
}
C题
二分算法原理,组合数
题意为给定一个二分算法的函数,用途为在一个已经从小到大有序的数列中查找某个数是否存在。现在给定数列长度n,并且该数列为1-n的全排列,查找的数值x,以及x所在的位置pos。
现在询问该数列有多少种排列方式,可以使得该二分查找函数成功在pos位置上找到x。
首先要认识到,二分算法是有一个左右区间的标记,在不断的二分区域缩小范围后,最终确定位置。对于每次二分的过程,选择左侧还是右侧,我们必然是要和正常排列时候的选择相同,才能找到目标位置。也就是说我们模拟一遍二分的过程,如果mid位置在pos左侧的话,该位置的值要比x小(注意这里要特判下mid刚好为pos的情况),如果mid位置在pos右侧的话,该位置的值要比x大。
我们记录下有low个位置必须放置比x值小的数,有high个位置必须放置比x值大的个数。
之后先特判下,比x值小的个数和比x值大的个数,是否足够满足low和high的需求。
如果能满足,再计算组合数:
比x小的数有x-1个,从中取low个,并且做一个low个位置的排列。
比x大的数由n-x个,从中取high个,并且做一个high个位置的排列。
剩余的数字有n-low-high-1个,做一个n-low-high-1个位置的排列。
上述三个乘起来,即为答案。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll mod=1e9+7;
ll cas[1007];//cas[i]储存i的阶乘
int n,x,pos;
void init()
{
cas[0]=cas[1]=1;
for(ll i=2;i<=1000;i++) cas[i]=cas[i-1]*i%mod;
}
ll qpow(ll x,ll p)//快速幂算乘法逆元
{
ll ret=1;
while(p)
{
if(p&1) ret=ret*x%mod;
x=x*x%mod;
p>>=1;
}
return ret;
}
int32_t main()
{
IOS;
init();
cin>>n>>x>>pos;
int high=0,low=0;
int l=0,r=n;
while(l<r)
{
int mid=(l+r)>>1;
if(mid<=pos)
{
l=mid+1;
if(mid!=pos) low++;//注意特判位置刚好为目标位置的情况,这个位置我们已经固定为x
}
else
{
r=mid;
high++;
}
}
if(low>=x||x+high>n) cout<<0<<endl;
else
{
ll ans=cas[x-1]*qpow(cas[low]*cas[x-1-low]%mod,mod-2)%mod;//乘法逆元计算C(low,x-1)
ans=cas[n-x]*qpow(cas[high]*cas[n-x-high]%mod,mod-2)%mod*ans%mod;//乘法逆元计算C(high,n-x)
ans=ans*cas[low]%mod*cas[high]%mod*cas[n-high-low-1]%mod;//乘上low,high,n-low-high-1的阶乘
cout<<ans<<endl;
}
}
D题
树上博弈
题意为在一棵n个结点并以结点1为根节点的树上,每个节点上一开始都有一个初始的人数,现在有一个怪物出现在了根节点1上,人类和怪物交替操作。
人类先移动,怪物后移动,都只能在树上朝着更深的地方去移动,直到到达某个叶子结点无路可走,此时怪物会抓住这个叶子结点上所有的人。
现在人类和怪物都用最优策略来决策,询问最后怪物抓住了多少人。
推得最基础的一个结论还是简单的,那就是不考虑父结点的情况下,当前结点和其子树上所有的人数加起来,作为人类会选择尽可能平均得分配到各个叶子结点方向上去。
困难之处在于当前结点的人数比较少,无法实现平均分配的情况该如何是好呢?
此时怪物必然是选择人多的那个方向去了,并且我们这时候不会将当前结点的人分配到这个人多的方向去,否则就是可以平均分配的情况了,也就是说当前位置的人数不会影响该方向子树的总人数(重要)。
此时我们计算人多的那个方向子树的平均分配方案,可以得到该方向上的最优结果。
这里运用了递归的思想,但是由于给定数据是排序后的特别数据,反向for循环即可实现。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=2e5+7;
int p[maxn];//由于题目说了p[i]的值必定小于i,图是经过排序后给定边的,因此直接用一个一维数组即可存图
ll a[maxn];
int son[maxn];//son[i]记录结点i的子树有多少叶子结点
int32_t main()
{
IOS;
int n;
cin>>n;
for(int i=1;i<=n;i++) son[i]=1;
for(int i=2;i<=n;i++) {cin>>p[i];son[p[i]]=0;}
for(int i=1;i<=n;i++) cin>>a[i];
ll ans=0;
for(int i=n;i;i--)//反向从叶子结点"递归"回来
{
ans=max(ans,a[i]/son[i]+(a[i]%son[i]?1:0));//当前位置以及其子树上所有的所有人,我们按照尽可能平均的原则分配到各个叶子结点的方向上去
//如果某一个方向的人数特别多或者特别少,当前结点的位置人数较少,会无法在当前结点上实现平均分配到各个叶子结点方向上去
//但是此时怪物必然是选择人多的那个方向去了,并且我们这时候不会将当前结点的人分配到这个人多的方向去,否则就是可以平均分配的情况了,也就是说当前位置的人数不会影响该方向子树的总人数(重要)
//此时我们计算人多的那个方向子树的平均分配方案,可以得到该方向上的最优结果
//这里运用了递归的思想,但是由于给定数据是排序后的特别数据,反向for循环即可实现
a[p[i]]+=a[i];
son[p[i]]+=son[i];
}
cout<<ans<<endl;
}
E题
结论,线段树维护
题意为定义一个针对数列的MEX计算,其值为该数列中未出现的最小的整数值。现在给定一个数列a[],对于其每一个练习子序列,求取他们的MEX值,这些MEX值写在一起构成一个新的数列,询问该数列的MEX值为多少。
这里先推个前置结论,对于每一个具体的值x,我们的目标是在原数列中找到能构造MEX值为x的部分,对于这个数列中每一个x出现的位置作为隔板,隔开若干个区域,根据贪心的原则对于每一个区域我们都是选择整段。对于这一整段,我们计算其MEX值是否等于x,检测每一个区域能否出现一个可以构造MEX等于x的区域即为我们能否利用该数列构造出MEX值为x。
按照这个结论直接暴力去写的话是会tle的,在最劣情况下复杂度为n2。
这里我们需要用线段树来维护L-R这个范围的值,在当前数列中出现的最右侧下标的最小值。
我们记录每个数字出现的上一个位置pre[x],我们在线段树中,找寻区间最小值不小于pre[x]+1的区间最右侧下标,该区间的所有值,出现位置都在pre[x]后,在当前for循环位置前,也就是我们当前的这个检测区域。该区间的所有下标对应的值,都在该区域有出现,其最右侧即为该区域对应的MEX值。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=1e5+7;
int a[maxn];
struct Node
{
int l,r,data;//线段树维护的是,l-r区间的值,各自在数列中出现的最右侧位置下标的最小值是多少
}node[maxn<<2];
void build(int now,int l,int r)
{
node[now].l=l;node[now].r=r;node[now].data=0;
if(l==r) return;
int mid=(l+r)>>1;
build(now<<1,l,mid);
build(now<<1|1,mid+1,r);
}
void change(int now,int tar,int x)//更改值为tar的数字的最右侧位置下标为x
{
if(node[now].l==node[now].r)
{
node[now].data=x;
return;
}
int mid=(node[now].l+node[now].r)>>1;
if(mid>=tar) change(now<<1,tar,x);
else change(now<<1|1,tar,x);
node[now].data=min(node[now<<1].data,node[now<<1|1].data);
}
int ask(int now,int tar)//询问下标从1开始的区间中,其维护的最右侧下标最小值不超过tar的最右侧下标为多少,实际上就是在求针对tar到主函数for循环参数i这个区间的数列,其对应的MEX值
{
if(node[now].l==node[now].r) return node[now].l;
if(node[now<<1].data<tar) return ask(now<<1,tar);
else return ask(now<<1|1,tar);
}
bool flag[maxn];//记录数列a各个子序列能得到那些MEX值
int pre[maxn];//pre[i]记录数值i上次出现的下标位置,用于ask函数的区间范围确定
int32_t main()
{
IOS;
int n;
cin>>n;
build(1,1,maxn);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
if(pre[a[i]]+1<i) flag[ask(1,pre[a[i]]+1)]=1;//注意这里和下一个for循环的特判区间是否为空,如果不特判的话会导致算出一个MEX值1,使得结果出错
pre[a[i]]=i;
change(1,a[i],i);
}
for(int i=1;i<maxn;i++) if(pre[i]&&pre[i]<n) flag[ask(1,pre[i]+1)]=1;//对于每个数字,其最后一次出现到数列末尾的这个区间未在上述for循环中询问
flag[ask(1,1)]=1;//注意上述情况是不包括询问整个数列的
for(int i=1;;i++) if(!flag[i]) {cout<<i<<endl;break;}
}