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

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

//写于rating值1987/2184

比赛链接:https://codeforces.com/contest/1420
A题
排序相关,思维水题

题意为给定一个长度为n的数列,问你能否在n × \times ×(n-1)/2-1的交换次数后,使得这个数列按照值从小大排序。

不太理解为什么有些人会尝试着去从正面硬想
基于数列原始下标来进行交换的排序算法,在最糟糕的情况下,需要的交换次数是累加1到n-1,值就是n × \times ×(n-1)/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;

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        int n;
        cin>>n;
        vector<int>num(n);
        for(auto &x:num) cin>>x;
        bool flag=1;
        for(int i=1;i<n;i++) if(num[i]>=num[i-1]) flag=0;
        if(flag) cout<<"NO"<<endl;
        else cout<<"YES"<<endl;
    }
}

B题
位运算,简单结论

题意为给定一个数列num[],询问你存在多少组下标i和j满足i<j,并且num[i]&num[j]>=num[i]^num[j]。

首先要认识到一点,&与运算是不会得到一个比参与运算的两个值更大的值的。对于一个数字x来说,以x=6为例,6的二进制表示为000…000110,对于左侧的所有0来说,在和另一个数字进行&与运算后都不可能得到1,但是对于 ^ 异或运算来说,如果另一个数的二进制中对应位是1的话,^ 异或运算就会得到一位1,这样的话^ 运算得到的结果就必然会大于&与运算的结果,因此左侧的所有0对应的另一个数的二进制上也应该全是0。
而对于x=6的最高位的1来说,如果另一个数对应的该位是1的话,那么&与运算的结果会是1,而^ 异或运算的结果会是0,必然是满足要求的。如果另一个数对应的该位是0的话,那么&与运算的结果会是0,而^ 异或运算的结果会是1,必然是不满足要求的。

由此我们可以得到结论,对于数字num[j]来说,能与它构成满足题目要求的下标i,必然要满足num[i]的最高位1和num[j]的最高位1相同,因此我们直接用一个数组cas[i]记录最高位是第i位的数字在下标j之前出现了几次。
利用cas数组即可在O(n)的时间得到答案。

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

ll cas[100],sum[100];

int32_t main()
{
    
    
    IOS;
    cas[0]=1;
    for(ll i=1;;i++)
    {
    
    
        cas[i]=cas[i-1]*2;
        if(cas[i]>1e9) break;
    }
    int t;
    cin>>t;
    while(t--)
    {
    
    
        int n;
        cin>>n;
        vector<ll>num(n);
        for(auto &x:num) cin>>x;
        memset(sum,0,sizeof(sum));
        ll ans=0;
        for(int i=0;i<n;i++)
        {
    
    
            int tar=0;
            while(cas[tar]<=num[i]) tar++;
            ans+=sum[tar-1];
            sum[tar-1]++;
        }
        cout<<ans<<endl;
    }
}

C题
C1很容易就可以看出来dp可以写,但是C2由于要进行对数组数据的交换更改操作,每次修改都dp一遍O(n)的话是必然超时的。C2需要进行一个结论总结。

题意为,给定一个长度为n的数列,你需要从中选择一部分下标构成一个新的数列,新数列的值被定义为新数列中的所有奇数下标的数值减去所有偶数下标的数值,现在询问其最大值是多少。
C2的话多了q次对原数列的修改操作,每次修改会交换原数列中的两个数字位置,对于每次修改,都要输出可以构成的数列最大值。

C1的话,还是很容易看出用dp就可以迅速解决。dp[i][0]代表,使用了第i个数字在新数列中的最后一个数字是奇数下标的情况下前i个数字可以构成的最大值,dp[i][1]代表第i个数字在新数列中的最后一个数字是偶数下标的情况下前i个数字可以构成的最大值。
转移方程的话,dp[i][0],分为第i个数字选择和不选择两种转移,第i个数字不选择,那么dp[i][0]就从dp[i-1][0]转移而来,如果第i个数字选择,dp[i][0]就从dp[i-1][1]+num[i]转来,两者取个max。dp[i][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 maxn=3e5+7;

ll dp[maxn][2];
ll num[maxn];
ll n,q;

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        cin>>n>>q;
        for(ll i=0;i<n;i++)
        {
    
    
            cin>>num[i];
            dp[i][0]=dp[i][1]=-llINF;
        }
        dp[0][0]=num[0];dp[0][1]=0;
        for(ll i=1;i<n;i++)
        {
    
    
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]+num[i]);
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]-num[i]);
        }
        cout<<max(dp[n-1][0],dp[n-1][1])<<endl;
    }
}

C2题的话,有q次调换操作,会调换数列中的两个数字的位置,要求对每次调换操作后都输出最大的值。此时明显的继续用C1的dp解法会超时,那么我们必然要想到一个在每次修改后O(1)或者O(logn)的求解方案。
这里的话归纳总结一个规律,我们最后最优的选择,一定是这个数列中值的“波峰”和“波谷”,证明方式的话,可以自己举例类似1,4,3,2,7这样的例子,针对4,3,2这一个区间,用反证的方式证明我们必然是要选择4和2。利用这样的证明方案证得该结论,并且数列首和尾的“波谷”不取。
对于每次修改调换下标i和j的数字,可能产生影响的位置(产生或者消失波峰波谷)为i-1,i,i+1,j-1,j,j+1,六个位置,对每个位置原来的波峰波谷的值进行逆运算,然后对修改后形成的波峰波谷重新计算即可。此处要注意i和j相等的情况直接跳过,并且要注意i+1与j-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 maxn=3e5+7;

ll num[maxn];
ll n,q;

ll check(ll i)//返回下标i的位置是波峰还是波谷,决定了这个位置上的数字是增加还是减少到最终结果上
//1代表波峰,-1代表波谷,0代表其他情况,恰好对应了其累加到结果上对应的系数
{
    
    
    if(n==1) return 1;//特判数列只有1个数的情况
    if(i==1)//如果当前位置是开头位置,如果是波峰的话就累加到结果上,其他情况都不取(开头的波谷不取)
    {
    
    
        if(num[1]>num[2]) return 1;//此时判断右侧即可
        else return 0;
    }
    if(i==n)//如果当前位置是末尾位置,如果是波峰的话就累加到结果上,其他情况都不取(末尾的波谷不取)
    {
    
    
        if(num[i]>num[i-1]) return 1;//此时判断左侧即可
        else return 0;
    }
    if(num[i]>num[i-1]&&num[i]>num[i+1]) return 1;//其他情况下如果既大于左边又大于右边则为波峰
    if(num[i]<num[i-1]&&num[i]<num[i+1]) return -1;//其他情况下入股既小于左右又小于右边则为波谷
    return 0;
}

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        cin>>n>>q;
        for(ll i=1;i<=n;i++) cin>>num[i];
        ll ans=0;
        for(ll i=1;i<=n;i++)//直接for一遍,每个数乘以其check对应的系数累加到结果上即可
        {
    
    
            ans+=check(i)*num[i];
        }
        cout<<ans<<endl;
        for(ll i=0;i<q;i++)//q次调换num[l]和num[r]
        {
    
    
            ll l,r;
            cin>>l>>r;
            if(l!=r)//l=r的情况直接跳过
            {
    
    
                if(l>1) ans-=check(l-1)*num[l-1];//l-1的位置如果存在,做逆运算
                if(l<n&&l+1!=r) ans-=check(l+1)*num[l+1];//l+1的位置如果存在,也做逆运算
                //下面同样的对r-1,r+1,l,r四个位置做逆运算
                if(r>1&&r-1>l+1) ans-=check(r-1)*num[r-1];//此处要注意特判r-1的位置与l+1不重合,否则会重复运算,l和r差值小于2的情况下都会有重合
                if(r<n) ans-=check(r+1)*num[r+1];
                ans-=check(l)*num[l];
                ans-=check(r)*num[r];
                swap(num[l],num[r]);//交换后把对应受影响位置的值加回来
                if(l>1) ans+=check(l-1)*num[l-1];
                if(l<n&&l+1!=r) ans+=check(l+1)*num[l+1];
                if(r>1&&r-1>l+1) ans+=check(r-1)*num[r-1];
                if(r<n) ans+=check(r+1)*num[r+1];
                ans+=check(l)*num[l];
                ans+=check(r)*num[r];
            }
            cout<<ans<<endl;
        }
    }
}

D题
扫描线思想的应用,不过这是一维的。

题意为有n盏灯,每盏灯都有一个开始亮的时间和结束亮的时间,现在需要询问你又对少组个数为k的灯的组合,在某一个瞬间他们全部都亮着。

这题其实就是一个线段树扫描线思想的一个简单应用,直接把开始时间和结束时间都放到同一个数组里,开始时间做个标记1,结束时间做个标记-1。按照时间的先后排序,直接for一遍,碰到标记为1的时间,当前区间可同时亮的灯总数+1,碰到标记为-1的时候减1。用一个num数组记录当前区间可同时亮灯数,如果num>=k则代表可以构成满足要求的个数为k的灯的组合,我们用类似dp的思想,当前刚加入的这盏灯一定在这个k个灯的组合里,就不会和前面已经计算的部分重复了,增加num-1中取k-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=998244353;
const ll maxn=3e5+7;

ll n,k;
struct Node
{
    
    
    ll data,ope;
};

vector<Node>node;

bool cmp(Node a,Node b)
{
    
    
    if(a.data==b.data) return a.ope>b.ope;//注意这里的cmp,时间点相同的时候,要把增加的点放到减少的点前面
    else return a.data<b.data;
};

ll cas[maxn];

ll qpow(ll a,ll p)
{
    
    
    ll ret=1;
    while(p)
    {
    
    
        if(p&1) ret=ret*a%mod;
        a=a*a%mod;
        p>>=1;
    }
    return ret;
}

ll cal(ll num)
{
    
    
    if(num<k) return 0;
    return cas[num-1]*qpow(cas[num-k]*cas[k-1]%mod,mod-2)%mod;//计算在num-1个数中取k-1个的组合数
}

int32_t main()
{
    
    
    IOS;
    cas[0]=cas[1]=1;
    for(ll i=2;i<maxn;i++) cas[i]=cas[i-1]*i%mod;
    cin>>n>>k;
    n<<=1;
    for(ll i=0;i<n;i++)
    {
    
    
        ll x;
        cin>>x;
        node.push_back({
    
    x,(i&1)?-1:1});
    }
    sort(node.begin(),node.end(),cmp);
    ll ans=0,num=0;
    for(ll i=0;i<n;i++)
    {
    
    
        if(node[i].ope==1)
        {
    
    
            num++;
            ans=(ans+cal(num))%mod;
        }
        else num--;
    }
    cout<<ans<<endl;
}

猜你喜欢

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