Codeforces 1042 Div3(ABCDEFG)

前言

沟槽的D卡了我一个多小时,快结束看了眼E发现E这么简单,结果就是比赛结束了才过了E,之后发现群友的D的思路是真的妙……还是太菜了T^T

一、A. Lever

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

void solve()
{
    int n;
    cin>>n;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<int>b(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }

    int ans=0;
    while(true)
    {
        bool flag=true;
        for(int i=1;i<=n;i++)
        {
            if(a[i]>b[i])
            {
                a[i]--;
                flag=false;
                break;
            }
        }

        for(int i=1;i<=n;i++)
        {
            if(a[i]<b[i])
            {
                a[i]++;
                break;
            }
        }

        ans++;

        if(flag)
        {
            break;
        }
    }
    cout<<ans<<endl;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

这个题可以看到数据量很小,那就直接模拟做就可以了。那就是每次迭代都先过一遍a数组,如果有一个比b数组大那就减小,然后直接break。之后再过一遍a数组,如果比b数组小就增加,然后break。最后看如果第一次操作没发生过那就直接退出输出答案即可。

二、B. Alternating Series

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

void solve()
{
    int n;
    cin>>n;
    
    if(n%2==0)
    {
        for(int i=1;i<n;i++)
        {
            if(i%2==1)
            {
                cout<<-1<<" ";
            }
            else
            {
                cout<<3<<" ";
            }
        }
        cout<<2<<endl;
    }
    else
    {
        for(int i=1;i<=n;i++)
        {
            if(i%2==1)
            {
                cout<<-1<<" ";
            }
            else
            {
                cout<<3<<" ";
            }
        }
        cout<<endl;
    }
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

这就是一个简单的构造题。

既然要求绝对值的字典序最小,那么贪心一下肯定是由-1开始,正负正负这样交替。又因为必须满足子数组的累加和是正数,所以对于中间的每个正数,累加和最小的情况肯定是选左右的负数组成的长度为三的子数组。又因为为了保证字典序最小,所以负数肯定都要选-1,那么只要中间的正数是3就一定能保证累加和是正数。而如果最后以正数结尾,那么因为右侧没有-1了,所以填2就能保证累加和是正数。

三、C. Make it Equal

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

void solve()
{
    ll n,k;
    cin>>n>>k;
    vector<ll>s(n+1);
    for(int i=1,x;i<=n;i++)
    {
        cin>>x;
        s[i]=x%k;
    }
    multiset<ll>t;
    for(int i=1,x;i<=n;i++)
    {
        cin>>x;
        t.insert(x%k);
    }

    for(int i=1;i<=n;i++)
    {
        if(t.find(s[i])!=t.end())
        {
            t.erase(t.find(s[i]));
        }
        else if(t.find(k-s[i])!=t.end())
        {
            t.erase(t.find(k-s[i]));
        }
        else
        {
            cout<<"NO"<<endl;
            return ;
        }
    }
    cout<<"YES"<<endl;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

这题真的坑,上来看了半天没想明白,后来纯靠猜还真给猜过了。

观察发现,因为可以无限次加减k,所以其实这些数的原始数根本不重要,是需要考虑模k后的余数即可,就是把所有数都一直减k减到再减就小于0为止。这样操作后,对于可以通过反复加k相等的数,这两个数模k的余数肯定是一样的。之后,只需要过一遍s数组,每次考察这个余数和进行一次减k操作后的数在t中是否存在即可,如果两个都不存在就肯定没法凑成。

四、D. Arboris Contractio

还得加训树形结构……

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

const int MAXN=2e5+5;

void solve()
{
    int n;
    cin>>n;
    vector<vector<int>>g(n+1);
    for(int i=0,u,v;i<n-1;i++)
    {
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    //叶节点的总个数
    int sum=0;
    //连接到同一个节点的叶节点数的最大值
    int mx=0;
    for(int i=1;i<=n;i++)
    {
        int tmp=0;

        //自己就是叶节点
        if(g[i].size()==1)
        {
            sum++;
            tmp++;
        }

        for(int v:g[i])
        {
            //孩子是叶节点
            if(g[v].size()==1)
            {
                tmp++;
            }
        }

        mx=max(mx,tmp);
    }
    //选择有最多叶节点的节点为中心点
    //操作数就是全部叶节点的个数sum减去最多叶节点的个数mx
    cout<<sum-mx<<endl;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

群友的这个思路太妙了……

首先贪心一下,上来选择叶节点最多的节点肯定不会亏。因为整个过程就是每次把最长链上的节点都连到中心节点,那么每次剩下没连的就是叶节点。所以就是先考察每个节点,统计叶节点的总数和叶节点最多的节点的叶节点个数,并选择这个节点为中心节点。那么之后一次重连肯定只能解决一个叶节点,所以操作数就是叶节点的总数减去最多叶节点个数。

注意力惊人了属于是,赛时根本没往叶节点这方面考虑……

五、E. Adjacent XOR

赛时最后五分钟搓的,一塌糊涂但能过()

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

void solve()
{
    int n;
    cin>>n;
    vector<ll>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<ll>b(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }

    if(a[n]!=b[n])
    {
        cout<<"NO"<<endl;
        return ;
    }

    vector<ll>dp(n+1);
    dp[n]=a[n];
    for(int i=n-1;i>=1;i--)
    {
        if(a[i]!=b[i])
        {
            if((a[i]^dp[i+1])==b[i])
            {
                dp[i]=a[i]^dp[i+1];
            }
            else if((a[i]^a[i+1])==b[i])
            {
                dp[i]=a[i]^a[i+1];
            }
            else
            {
                cout<<"NO"<<endl;
                return ;
            }
        }
        else
        {
            dp[i]=a[i];
        }
    }
    cout<<"YES"<<endl;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先这个题可以发现修改操作只依赖右侧的值,那么对于最右侧的数,根本没法进行修改。所以就先判断最右侧的两个数,如果不同那就根本不可能凑成。因为每个操作只依赖右侧的值,那么可以考虑从后往前遍历。之后可以考虑dp一下,用dp数组存最终i位置的数。那么如果两数本来就相等,那根本就不用改,直接设置eor数组为原始数。如果不同的话,就需要修改了。修改的策略有两种,要么异或右侧原始的数,要么异或右侧修改后的数。那么就是如果跟右侧最终的数,即dp值异或后相等,那就设置当前位置的dp值为异或右侧dp值的结果。如果不行,那么如果跟右侧原始数异或能相等,就设置dp值为异或右侧原始值的结果。如果都不相等,那就根本不可能完成。

六、F. Unjust Binary Life

这b题是真的难……

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

//大于等于x的最左位置
int bs(ll x,int n,vector<array<ll,3>>&cnts)
{
    int l=0;
    int r=n-1;
    int m;
    int ans=n;
    while(l<=r)
    {
        m=(l+r)/2;
        if(cnts[m][0]>=x)
        {
            ans=m;
            r=m-1;
        }
        else
        {
            l=m+1;
        }
    }
    return ans;
}

void solve()
{
    int n;
    cin>>n;
    string a,b;
    cin>>a>>b;

    //如果(i,j)->(i+1,j),那么有Ai^Bj=0,A(i+1)^Bj=0
    //所以可以合并得到Ai^A(i+1)=0,即Ai==A(i+1)
    //而如果(i,j)->(i,j+1),那么有Ai^Bj=0,Ai^B(j+1)=0
    //所以可以得到Bj^B(j+1)=0,即Bj==B(j+1)
    //又因为Ai^Bj=0,即Ai==Bj
    //所以如果能从(1,1)->(i,j),必须有A1~Ai和B1~Bj的所有数全相等
    //所以最小操作次数就是min(ones,zeros)

    //所以可以考虑先预处理a串中从头到i的0的个数zeros和1的个数ones
    //再根据zeros-ones从小到大排序,生成预处理数组help
    //之后遍历b串,每次统计b串的zeros和ones
    //去help里二分找大于等于b串ones-zeros的最左位置l
    //此时对答案的贡献就是a串0~l上每个位置0的个数的累加和加上b串目前0的个数乘l
    //再加上a串后续每个位置1的个数的累加和加上b串目前1的个数乘后续的长度

    //举个例子,假如help={-1,-1,0,0}
    //当来到b串的2位置,ones=2,zeros=1,ones-zeros=1
    //说明b串到这个位置1的个数比0的个数多1
    //所以说明a串中有四个位置补不上0比1少的个数,那么操作数就是0的个数
    //那么对答案的贡献就是a串中0的个数加上b串目前0的个数乘以4
    //再加上右侧操作数为1的个数的答案,即a串中后续1的个数加上b串目前1的个数乘以4-4=0

    vector<array<ll,3>>cnts(n);
    ll ones=0;
    ll zeros=0;
    for(int i=0;i<n;i++)
    {
        if(a[i]=='0')
        {
            zeros++;
        }
        else
        {
            ones++;
        }

        cnts[i]={zeros-ones,zeros,ones};
    }

    sort(cnts.begin(),cnts.end(),[&](const array<ll,3>&x,const array<ll,3>&y)
    {
        return x[0]<y[0];
    });

    vector<ll>pre0(n);
    vector<ll>pre1(n);
    pre0[0]=cnts[0][1];
    pre1[0]=cnts[0][2];
    for(int i=1;i<n;i++)
    {
        pre0[i]=pre0[i-1]+cnts[i][1];
        pre1[i]=pre1[i-1]+cnts[i][2];
    }

    zeros=0;
    ones=0;
    ll ans=0;
    for(int i=0;i<n;i++)
    {
        if(b[i]=='0')
        {
            zeros++;
        }
        else
        {
            ones++;
        }

        int pos=bs(ones-zeros,n,cnts);

        if(pos==0)
        {
            ans+=pre1[n-1]+ones*n;
        }
        else
        {
            ans+=pre0[pos-1]+zeros*pos+pre1[n-1]-pre1[pos-1]+ones*(n-pos);
        }
    }
    cout<<ans<<endl;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

这个题上来的这个思路就想不到。根据定义,假如能从(i,j)走到(i,j+1),那么因为Ai^Bj等于0,又因为Ai^B(j+1)等于0,所以把这两个式子再异或起来可以得到Bj^B(j+1)等于0。同理可以得到Ai^A(i+1)等于0,所以如果能从(1,1)走到(i,j),那么必须有从A1到Ai,从B1到Bj,所有的数都完全一样。那么要保证能走到的最小操作数就是这些数字里,0的个数和1的个数的最小值。所以,所有操作的代价就是对于所有的二元对(i,j)的最小操作数。

之后就需要考虑如何将复杂度降下来了。考虑先遍历一遍a串,每次统计从头到当前i位置,0的个数zeros和1的个数ones,以及zeros减ones的个数。之后,根据zeros减ones从小到大排序,然后分别求整个数组里zeros和ones的前缀和。之后,遍历b串,同样统计从头到当前位置的zeros和ones。

之后是重点,举个例子,假如b串从头到当前位置的zeros为2,ones为4,那么ones减zeros就是2,表明1的个数比0的个数多2个。之后,去之前a串统计出的数组里,二分查找大于等于2的最左位置pos。那么对于这个位置pos,左侧部分都是zeros减ones小于等于2的,即0的个数和1的个数弥补不了b串当前的差距,所以此时合起来0的个数和1的个数的最小值就是0的个数。同理,这个位置pos的右侧位置都是zeros减ones大于2的,即合起来后1最小值是1的个数。所以,0的个数和1的个数可以直接从前缀和数组里查,再加上b串当前的zeros和ones乘以对应个数即可。

真的离谱这个思路……

七、G. Wafu!

这个G感觉也不过如此嘛。

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll>pll;

const int MOD=1e9+7;

//找小于等于的最右位置
int bs(ll x,int n,vector<ll>&cnts)
{
    int l=1;
    int r=n;
    int m;
    int ans=0;
    while(l<=r)
    {
        m=(l+r)/2;
        if(cnts[m]<=x)
        {
            ans=m;
            l=m+1;
        }
        else
        {
            r=m-1;
        }
    }
    return ans;
}

void solve()
{
    ll n,k;
    cin>>n>>k;
    vector<ll>s(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
    }

    sort(s.begin()+1,s.end());

    //cnts[i]:1~i乘完的次数
    vector<ll>cnts(32);
    //mult[i]:1~i乘完
    vector<ll>mult(32);
    cnts[0]=0;
    mult[0]=1;
    for(int i=1;i<32;i++)
    {
        cnts[i]=cnts[i-1]*2+1;
        mult[i]=((mult[i-1]*mult[i-1])%MOD*i)%MOD;
    }

    ll ans=1;
    int i=1;
    while(k>0)
    {
        if(i<=n&&s[i]-1<32&&k>=1+cnts[s[i]-1])//能乘数组里的并把新增的乘完
        {
            ans=(ans*s[i])%MOD;
            ans=(ans*mult[s[i]-1])%MOD;
            k-=1+cnts[s[i]-1];
            i++;
        }
        else
        {
            //还能乘数组里的
            if(i<=n)
            {
                ans=(ans*s[i])%MOD;
                k--;
                i++;
            }

            //只能乘新增的
            int pos=31;
            while(k>0)
            {
                //二分找最多乘到的位置
                pos=bs(k,pos,cnts);
                ans=(ans*mult[pos])%MOD;
                k-=cnts[pos];
                //还能再乘一次
                if(k>0)
                {
                    ans=(ans*(pos+1))%MOD;
                    k--;
                }
            }
        }
    }
    cout<<ans<<endl;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先,因为每次从集合里删除一个数后,都要把从1到这个数中间的所有数都加入集合。又因为每次都选集合中最小的数,所以每次肯定会优先处理新加入的数。那么就可以考虑用dp的思想,先预处理一个cnts数组,cnts[i]表示集合的前i个数正好为1~i,把这i个数删完需要的次数。再预处理一个mult数组,其中mult[i]还是表示集合的前i个数正好为1~i,把这i个数删完答案一共要乘的数。再观察可以发现,在删除的过程中,在删第i个数i之前,肯定要把1~i-1删完,之后才能删i。接着因为又加入了1~i-1,所以还得再删一遍1~i-1,那么就可以得到如上面写的转移公式。通过观察cnts[i]的数可以发现,cnts[i]其实就等于2^i-1。又因为k小于等于10^9,没超过int的范围,所以cnts和mult都只需要开32长度即可。又因为每次在删完集合中的数x后,都要处理1~x-1的部分,那么只有当这个x-1小于32时,才有可能把新增的全删完。

之后,在对数组从小到大排序后,只要k大于0,那么如果数组里还有数,即原本的集合没被删完,且集合中的数-1小于32,即能把新增的全删完,且k的次数比删完一轮的次数还大,那就先删集合内的数,再把1~x-1全删完。否则,说明k不够全删完的了,那如果此时集合里还有数,那就先删集合里的。之后,因为新增的数删不完,只要k还大于0,那么每次能完整删完的1~i就可以在cnts里二分查找小于等于k的最右位置,然后把1~i这一部分全删了。如果还能再删一次,那么就再把数字i+1给删了,去后续看能否接着删了。

总结

一步一步来,天道酬勤,加油!!

END

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值