Codeforces Round #678 (Div. 2)A-E题解

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;}
}

猜你喜欢

转载自blog.csdn.net/StandNotAlone/article/details/109378133