Codeforces Round #670 (Div. 2)A-D题解

Codeforces Round #670 (Div. 2)A-D题解
//写于rating值1987/2184
//补档

比赛链接:https://codeforces.ml/contest/1406

A题
贪心,施行

题意为给你一堆自然数,让你把他们分成两个部分。两个部分当中的各自没出现的最小的自然数相加,要求这个结果最大,询问这个最大结果是多少。

直接贪心依次取两个部分的数就可以了。
从0开始连续+1最多可以取到x,那么这个部分没出现的最小自然数就是x+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;

int num[107];//记录每个数字出现了几次

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        memset(num,0,sizeof(num));
        int n;
        cin>>n;
        for(int i=0;i<n;i++)
        {
    
    
            int x;
            cin>>x;
            num[x]++;
        }
        int ans=0;
        for(int j=0;j<2;j++)//执行两次,分为两个部分
        {
    
    
            for(int i=0;1;i++)
            {
    
    
                if(i==101) {
    
    ans+=101;break;}//循环到101就可以结束了,输入数据最大也只有100
                if(num[i]==0) {
    
    ans+=i;break;}//当前数字不存在,则就为当前部分的最小未出现自然数,累加到答案并结束循环
                else num[i]--;//代表使用一个数字到当前部分里
            }
        }
        cout<<ans<<endl;
    }
}

B题
贪心,分类讨论,简单整体思维

题意为给定一个数列,你可以从中任意挑选5个数字乘起来(题意理解成这样就很简单了),希望你求出最大的结果。

这里任意挑选5个数字么,根据最后结果是正还是负(0可以包含在任意一种情况里),我们对最后结果的绝对值倾向是不同的。
如果结果为正,那么我们希望绝对值尽可能大。
如果结果为负,那么我们希望绝对值尽可能小。

而挑选5个数字,我们可以按照其中负数出现的次数,来决定最后结果的正负性。
由此我们可以把负数出现0,2,4次,和出现1,3,5次分为两种情况分别讨论,取各自情况的最大值即可。

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

vector<ll>zhengshu,fushu;

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        int n;
        cin>>n;
        for(int i=0;i<n;i++)
        {
    
    
            ll x;
            cin>>x;
            if(x<0) fushu.push_back(x);
            else zhengshu.push_back(x);
        }
        sort(fushu.begin(),fushu.end());
        sort(zhengshu.begin(),zhengshu.end());
        ll ans=-llINF;
        for(ll i=1;i<=5;i+=2)//正数或者0的出现次数为奇数,负数的出现次数为偶数
        {
    
    
            ll j=5-i;//i为正数出现次数,j为负数出现次数
            if(zhengshu.size()>=i&&fushu.size()>=j)//此时结果为正数,我们要使得绝对值尽可能大,这样值更大
            {
    
    
                ll temp=1;
                for(ll k=1;k<=i;k++) temp*=zhengshu[zhengshu.size()-k];//正数绝对值大的在末尾
                for(ll k=0;k<j;k++) temp*=fushu[k];//负数绝对值大的在开头
                ans=max(ans,temp);
            }
        }
        for(ll i=0;i<5;i+=2)//正数或者0的出现次数为偶数,负数的出现次数为奇数
        {
    
    
            ll j=5-i;
            if(zhengshu.size()>=i&&fushu.size()>=j)//此时结果为负数,我们要使得绝对值尽可能小,这样值更大
            {
    
    
                ll temp=1;
                for(ll k=0;k<i;k++) temp*=zhengshu[k];//正数绝对值小的在开头
                for(ll k=1;k<=j;k++) temp*=fushu[fushu.size()-k];//负数绝对值小的在末尾
                ans=max(ans,temp);
            }
        }
        zhengshu.clear();
        fushu.clear();
        cout<<ans<<endl;
    }
}

C题
树,构造,图结论

题意为,给你一棵n个节点的树,现在从这棵树中取走一个节点,要使得剩下的图中,拥有最大节点数量的树的节点数要尽可能小。
现在不希望这个取走的节点有多种取法,你可以去除掉原树中的一条边,再重连一条边,够成一课新树,使得那个取走的节点取法变为唯一。

扫描二维码关注公众号,回复: 11794045 查看本文章

这里要先推出来,什么情况下会使得这个取走的节点有多种取法。
这里的结论是,如果有一条边的左右两侧节点数都恰好是n/2(此时n必然是偶数),那么我们分别取走这条边左右两侧的节点,剩下的部分最大节点数均为n/2。

推导这个结论可以用反证法,剩下的部分最大结点数大于n/2的反例就不说明了。
如果剩下的部分最大节点数小于n/2,那么代表取走那个点后,剩下的部分至少有三个,其中一个部分是当前的最大部分有x个结点,另外两个部分分别有y个和z个结点,y<=x,z<=x且x+y+z=n。
取走的这个点是三个部分的连接节点。
那么我们如果不取这个点的话,这三个部分的至少两个部分会连接起来构成一个更大的部分,
由于x+y>x,x+z>x,y+z>x
也就是说我们取其他的点都是会得到比x更大的部分,由此推翻。

寻找这样一条左右两侧节点数均为n/2的“中间边”我们可以通过dfs来得到,那么得到后我们要如何删边加边构造没有这种“中间边”的树呢。
我们注意到这个“中间边”的存在,其实也就是这个子树可以分成两个恰好拥有n/2节点的部分,且两个部分只有一条连边。
我们只需要让它们之间有多条连边即可。
我们用dfs找到这条“中间边”,这条边两侧的点中,u为更接近根节点的点,d为更远离根节点的点。那么我们可以从u的这一侧切出一个点出来(用树根这个点就可以了),把它和d接起来。
这样的话u这一侧就只有n/2-1个点了,再加上d这个点才凑满n/2个点,而d这个点和原本自己一侧至少有一个连边,切下来的原根节点与其又有一条连边,就不再有上述结论的情况了。

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

struct Edge
{
    
    
    ll to,next;
}edge[maxn*2];

ll head[maxn],ru[maxn],tot,n;//ru[i]数组代表i结点有几条边,用于寻找只有一条边的点作为树根起点
ll u,d;

void init()
{
    
    
    for(ll i=1;i<=n;i++) head[i]=-1,ru[i]=0;
    tot=0;
}

void add(ll u,ll v)
{
    
    
    edge[tot].to=v;
    edge[tot].next=head[u];
    ru[v]++;
    head[u]=tot++;
}

ll dfs(ll now,ll pre)//dfs返回值为now以及它的子树总共有几个结点,pre为之前的一个节点,用于dfs防止走回头路
{
    
    
    ll ret=1;
    for(ll i=head[now];i!=-1;i=edge[i].next)
    {
    
    
        ll to=edge[i].to;
        if(to!=pre) ret+=dfs(to,now);
    }
    if(ret==n/2)//如果当前结点的个数恰好等于总结点数的一半,则记录这条“中间边”两侧的两个点位置,u是更接近根节点那一侧的点
    {
    
    
        u=pre;
        d=now;
    }
    return ret;
}


int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        cin>>n;
        init();
        for(ll i=1;i<n;i++)
        {
    
    
            ll u,v;
            cin>>u>>v;
            add(u,v);add(v,u);
        }
        if(n&1)//如果总结点数是奇数个则无需进行操作,任意切掉一条边再重新把这条边构造还原即可
        {
    
    
            ll now=1;
            ll to=edge[head[now]].to;
            cout<<now<<' '<<to<<endl;
            cout<<now<<' '<<to<<endl;
        }
        else//如果结点数是偶数个,那么去找是否有哪边边的两侧恰好结点数都是总数的一半,就会出现去除该条边两侧的两个点后,得到的剩余部分的最大结点数相等的情况
        {
    
    //此时最大结点数都是n/2,我们需要构造使得这种情况不再出现
            ll start=-1;//start为树根节点,挑选只有一条边的叶子作为树根(如果选择树内部而不是叶子作为起点去dfs检测子树节点数量是否等于n/2是不正确的,因为还可能通过树根和其他子树的部分加起来构成n/2的一个子树,以另一个点为树根的情况下)
            for(ll i=1;i<=n;i++) if(ru[i]==1) start=i;
            u=d=0;//u为那条“中间边”两侧的点中离根节点较近的点,d为较远的
            dfs(start,0);//dfs检测是否存在这样一条“中间边”,左右两侧的节点数都是n/2
            ll to=edge[head[start]].to;//to为与根节点相连的唯一一个点的位置,用于后面构造操作
            if(u==0)//如果不存在这样的“中间边”则无需操作,任意切掉一条边再重新把这条边构造还原即可
            {
    
    
                cout<<start<<' '<<to<<endl;
                cout<<start<<' '<<to<<endl;
            }
            else//如果存在这样的“中间边”,从这条边的u那一侧切割下一个点,这里切割下树根节点,然后拼接到另一侧的d点上即可
            {
    
    
                cout<<start<<' '<<to<<endl;
                cout<<start<<' '<<d<<endl;
            }
        }
    }
}

D题
贪心,结论,差分

题意为给定一个长度为n的数列A,你需要构造一个长度为n的值不下降的数列B和一个长度为n的值不上升的数列C,满足B[i]+C[i]=A[i]。现在问你数列B和数列C中的最大数值的最小值是多少。

注意到我们只需要关注数列B和C中的最大值,数列B中的最大值是最后一个数字,数列C中的最大值是第一个数字。
我们可以假设固定了数列C中的第一个数字为x,那么数列B中的第一个数字就是A[1]-x。
之后我们依次向后看Ai的值,
如果A[i]<=A[i-1],如果我们仍然按照i-1的位置构造B和C的话,加起来会大于A[i],我们需要减小数字,我们可以直接减少数列C的数字即可,并且不会影响到B和C中的最大值是度搜好。
如果A[i]>A[i-1],如果我们仍然按照i-1的位置构造B和C的话,加起来会小于A[i],我们需要增大数字,我们不能增加数列C的数字值,只能增加数列B的,而这个增加值会影响B中最后一个数字的大小,也就是会影响到最后的最大值。

我们累加A[i]-A[i-1]中大于0的部分,值为temp,那么这样贪心构造后的数列B中的最大值就位A[1]-x+temp,而数列C中的最大值为x,我们希望他们中的最大值尽可能小。也就是两者相加平均分成两部分后较大的那部分了,而两者相加值为A[1]-x+temp+x=A[1]+temp。
因此我们实际上只需要关注第一个位置的数,和后面的数字的差分数组中大于0的部分即可。
我们可以通过差分数组O(1)进行修改操作并得到结果。

另外注意下A[1]+temp是负数的时候,除以2是向上取整的。

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

vector<ll>num;

int32_t main()
{
    
    
    IOS;
    int n;
    cin>>n;
    num.resize(n+1);
    for(ll i=1;i<=n;i++) cin>>num[i];
    ll ans=num[1];
    for(ll i=n;i>1;i--)//差分操作
    {
    
    
        num[i]-=num[i-1];
        if(num[i]>0) ans+=num[i];
    }
    cout<<(max(ans/2,ans-ans/2))<<endl;//注意负数除以2是向上取整而不是向下,所以这里这么写是最稳妥的
    ll q;
    cin>>q;
    for(ll i=0;i<q;i++)
    {
    
    
        ll l,r,x;
        cin>>l>>r>>x;
        if(l>1)
        {
    
    
            if(num[l]>0) ans-=num[l];
            if(num[l]+x>0) ans+=num[l]+x;
        }
        if(l==1) ans+=x;//ans是num[1]加上后序差分数组大于0的部分,如果改变区间左侧就是1的话,直接要把x加到ans上
        if(r<n)
        {
    
    
            if(num[r+1]>0) ans-=num[r+1];
            if(num[r+1]-x>0) ans+=num[r+1]-x;
        }
        num[l]+=x;
        if(r<n) num[r+1]-=x;
        cout<<max(ans/2,ans-ans/2)<<endl;
    }
}

猜你喜欢

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