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

Codeforces Round #669 (Div. 2)A-D题解
//写于rating值1987/2184
//本场A-C过得比较慢,D没出,rating值-46

这场比赛的话,A题摁想了一个神奇的方法,写起来也复杂,花费了16分钟才过掉,B题正常速度10分钟,C题交互题自己硬加了个题意(询问的? x y必须要满足x<y),然后又摁想了一个奇怪的算法,wa掉后重读题意发现没这个条件后才过掉,写了40分钟左右。做D题的时候推出了dp的思路和求dp转移路径的方针,但是具体的逻辑关系没有理清,A和C题有点影响到心态。
状态不好可能是一个原因,但是能发现自己的缺陷,从失败中吸取教训就可。

比赛链接:https://codeforces.ml/contest/1407
A题
分类讨论,贪心

题意为给你一个长度为偶数n的,只包含0和1的数列,你最多删除其中的n/2个数字,要使得最后的数列中,偶数下标上的1和奇数下标上的1个数相同。

这里简单的想法是,如果我们最后构造的是一个只包含0或者只包含1的数列,并且长度是个偶数,那必然是满足条件的。

记录原数列中0出现的个数num0,1出现的个数num1。
当num0>=num1的时候,num1必定小于等于n/2,我们删光1,剩下全0数列即可,长度偶数奇数均满足要求。
当num0<num1的时候,num0<n/2,我们删光0,至少还有1次删除的机会。此时剩下全1的数列,长度必须为偶数才能满足条件,因此如果num1为奇数我们再删去一个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;

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        int n;
        cin>>n;
        int num0=0,num1=0;
        for(int i=0;i<n;i++)
        {
    
    
            int x;
            cin>>x;
            if(x) num1++;
            else num0++;
        }
        if(num0>=num1)//如果0的个数大于等于1的个数,代表1的个数小于等于n/2,删光1直接输出全0数列即可
        {
    
    
            cout<<num0<<endl;
            for(ll i=0;i<num0;i++)
            {
    
    
                if(i) cout<<' ';
                cout<<0;
            }
        }
        else
        {
    
    
            if(num1&1) num1--;//如果1的个数大于0的个数,0的个数小于n/2,删光0,如果1的个数是奇数,则再删一个1,变为全1数列
            cout<<num1<<endl;
            for(ll i=0;i<num1;i++)
            {
    
    
                if(i) cout<<' ';
                cout<<1;
            }
        }
        cout<<endl;
    }
}

B题
构造,贪心,暴力

题意为给定n个数字,你需要把他们重新排列顺序,满足得到的数列生成的c[]数组字典序最大。c[i]等于排列后的数组中前i个数字的gcd。多种满足条件的构造数列输出任意一种即可。

此处n的值比较小,我们可以记录目前构造的i个数字的gcd为temp,然后贪心去剩下没用过的数字里找与temp的gcd最大的值。
我们会发现与temp的gcd最大的值可能有多个,但是我们可以继续推导出这些数字的先后顺序是不会被影响的。
因为gcd(a,b)=c,c必定满足小于等于a和b。
假设gcd(temp,x)最大值为y,此时对应的x有多个,我们从其中任意选一个,temp便变为了y,然后再去寻找gcd(y,x)的最大值,此时gcd仍然最大是y,满足的数仍然是上次寻找gcd(temp,x)的那几个数字。由此推得那几个数字任意选择均可。

按照这个思路施行即可。

#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 flag[1007];//flag[i]记录i这个数字是否已经被使用

int gcd(int a,int b)
{
    
    
    return b?gcd(b,a%b):a;
}

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        int n;
        cin>>n;
        vector<int>num(n);
        for(auto &x:num) cin>>x;
        memset(flag,0,sizeof(flag));
        vector<int>ans;//ans为我们最后构造的数列
        int tar=0;
        for(int i=0;i<n;i++)//找到最大的那个数字,放在最后构造数列的第一个位置
        {
    
    
            if(num[i]>num[tar])
            {
    
    
                tar=i;
            }
        }
        ans.push_back(num[tar]);
        flag[tar]=1;

        int temp=num[tar];//temp记录当前构造的ans数组中最后一个位置的值
        for(int i=1;i<n;i++)
        {
    
    
            int Max=0,tar=-1;//tar记录剩下未被使用的数字中,与temp的gcd值最大tar为多少
            for(int j=0;j<n;j++)
            {
    
    
                if(!flag[j])
                {
    
    
                    if(gcd(temp,num[j])>Max)//此处的tar有多个,取任意一个均可,原因见题解
                    {
    
    
                        Max=gcd(temp,num[j]);
                        tar=j;
                    }
                }
            }
            ans.push_back(num[tar]);
            flag[tar]=1;
            temp=gcd(temp,num[tar]);
        }

        for(int i=0;i<ans.size();i++)
        {
    
    
            if(i) cout<<' ';
            cout<<ans[i];
        }
        cout<<endl;
    }
}

C题
交互,施行,总结小结论

题意为有一个长度为n的排列num[],每次交互你可以询问两个下标x和y,会返回给你num[x]%num[y]的结果。现在你需要在最多进行2n次交互的情况下得到每一个下标位置上的具体数值。

首先我们肯定要思考,我们通过怎么样的查询能确定某一个下标位置上的值呢。
注意到如果num[x]<num[y],那么返回给我们的结果就是num[x],但是关键是我们询问的时候,是不知道num[x]和num[y]哪个大的。
那么如果我们询问完num[x]%num[y]后,交换一下xy,再循环num[y]%num[x]的值呢?

假设num[x]%num[y]=c,num[y]%num[x]=d
我们考虑num[x]<num[y]的情况(num[x]>num[y]的情况同理,不再赘述)
此时c=num[x],而d必定是小于num[x]的,所以c>d。
也就是说我们两次查询中较大的那个数字就是num[x]和num[y]中较小的那个数,这种情况下就是num[x],而x是查询得到c的那次查询中的第一个数字,由此我们就可以通过两次查询获得确定num[x]和num[y]中较小的那个数字,并和他的对应下标确定下来。

由此我们用tar记录当前未确定的最大值下标,直接for一遍,一路查询[tar,i]和[i,tar],确定num[i]和num[tar]中较小的那个,更新tar下标,施行即可。

#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=1e4+7;

int num[maxn];
int n;

int32_t main()
{
    
    
    IOS;
    cin>>n;
    int tar=1;//tar指示当前扫过部分的最大值的下标,for循环时该位置的值是不确定的
    for(int i=2;i<=n;i++)
    {
    
    
        int x,y;
        cout<<"? "<<tar<<' '<<i<<endl;cout.flush();
        cin>>x;
        cout<<"? "<<i<<' '<<tar<<endl;cout.flush();
        cin>>y;
        if(x>y)//代表x是num[tar]和num[i]中较小的那个,并且x=num[tar]
        {
    
    
            num[tar]=x;//确定下标tar的值为x
            tar=i;//更新不确定的最大值位置下标为i
        }
        else num[i]=y;//num[tar]>num[i]时,不需要更新tar,直接确定下标i的值为y
    }
    num[tar]=n;//for结束后还剩下最后一个tar位置'不确定',但是他是这n排列中最大的,自然只能是n了
    cout<<"! ";
    for(int i=1;i<=n;i++)
    {
    
    
        if(i>1) cout<<' ';
        cout<<num[i];
    }
    cout<<endl;
}

D题
dp,dp转移路径需自己预处理出来,且关系逻辑需要先推导

题意为有n栋高楼,你从第1栋楼开始要到第n栋楼。每次操作你可以移动到相邻位置的高楼,或者一次性跳跃到右侧的某一栋高楼上,满足这一段中间的高楼的高度,全部都高于或者低于起点和终点的高度。问你最少需要的操作次数。

这道题很容易想到使用dp来转移,暴力的dp肯定是不行的,然后就去思考是否可以处理出题意的四种情况的所有转移路径再dp。
这里我比赛时没有理清的是这样的一个情况,对于这种数据:
4 1 2 3 5
4可以直接跳到2,也可以直接跳到3,都是满足中间部分的高度低于起点终点高度的,这种要如何高效处理出来没能想清。

但是实际上4是2左侧第一个不小于2的数,也是3左侧第一个不小于3的数。

这里我们可以正向for利用stack处理,得到当前位置左侧第一个不小于/大于当前位置的下标,然后再反向for利用stack处理,得到当前位置右侧第一个不小于/大于当前位置的下标,就能包括所有的转移情况,并且还把移动到相邻位置的转移也包含进去了。(此处结合上面数据理解理解)

预处理出dp转移路径后,按照这些路径转移就是了…此处我的代码没有像官方题解一样预处理出每个位置有几个转移位置,而是直接利用上面预处理出的四种情况的四个数组转移的。

#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=3e5+7;

ll n;
ll h[maxn];
ll dp[maxn];//dp[i]记录从下标0位置开始到下标i位置最少需要多少次跳跃
ll r_higher[maxn];//r_higher[i]记录在下标i的右侧,第一个不小于h[i]的下标位置,指示dp转移方向i到r_higher[i]
ll r_lower[maxn];//r_lower[i]记录在下标i的右侧,第一个不大于h[i]的下标位置,指示dp转移方向i到r_lower[i]
ll l_higher[maxn];//l_higher[i]记录在下标i的左侧,第一个不小于h[i]的下标位置,指示dp转移方向从l_higher[i]到i
ll l_lower[maxn];//l_lower[i]记录在下标i的左侧,第一个不大于h[i]的下标位置,指示dp转移方向从l_lower[i]到i

int32_t main()
{
    
    
    IOS;
    cin>>n;
    for(ll i=0;i<n;i++)
    {
    
    
        dp[i]=r_higher[i]=r_lower[i]=l_higher[i]=l_lower[i]=llINF;//初始化状态代表不存在
        cin>>h[i];
    }
    stack<ll>S;//定义一个栈实现我们后面分四种情况记录dp转移路径的过程
    //只需要能够理解其中一种,就能理解其他三种
    for(ll i=0;i<n;i++)//求l_higher[],也就是左侧的第一个不小于h[i]的位置下标
    {
    
    
        while(S.size()&&h[S.top()]<h[i]) S.pop();//这里我们把栈中记录的小于h[i]的位置全部弹出
        if(S.size()) l_higher[i]=S.top();//如果栈中还有剩余,那么栈顶的位置就是最近的一个位置下标
        S.push(i);//把当前位置推入栈中,在栈中已存在的下标中不存在比当前的h[i]更小的h值(关键)
    }

    while(S.size()) S.pop();
    for(ll i=0;i<n;i++)
    {
    
    
        while(S.size()&&h[S.top()]>h[i]) S.pop();
        if(S.size()) l_lower[i]=S.top();
        S.push(i);
    }

    while(S.size()) S.pop();
    for(ll i=n-1;i>=0;i--)
    {
    
    
        while(S.size()&&h[S.top()]<h[i]) S.pop();
        if(S.size()) r_higher[i]=S.top();
        S.push(i);
    }

    while(S.size()) S.pop();
    for(ll i=n-1;i>=0;i--)
    {
    
    
        while(S.size()&&h[S.top()]>h[i]) S.pop();
        if(S.size()) r_lower[i]=S.top();
        S.push(i);
    }

    dp[0]=0;
    for(ll i=0;i<n;i++)
    {
    
    
        if(l_lower[i]!=llINF) dp[i]=min(dp[i],dp[l_lower[i]]+1);//这里dp转移的时候一定要先转移l_的两个部分
        if(l_higher[i]!=llINF) dp[i]=min(dp[i],dp[l_higher[i]]+1);//因为是更前面的位置转移到当前位置,要比下面两种r_过程的当前位置转移到更后面要优先
        if(r_lower[i]!=llINF) dp[r_lower[i]]=min(dp[r_lower[i]],dp[i]+1);
        if(r_higher[i]!=llINF) dp[r_higher[i]]=min(dp[r_higher[i]],dp[i]+1);
    }

    cout<<dp[n-1]<<endl;
}

猜你喜欢

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