Codeforces 1095 Div2(ABCDE)

前言

感觉这场的题全是 guess 和 attention,做起来难的一批......

一、A. Disturbing Distribution

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

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

    int ans=0;
    int one=0;
    for(int i=1;i<=n;i++)
    {   
        if(a[i]==1)
        {
            one=1;
        }
        else
        {
            ans+=a[i];
            one=0;
        }
    }
    cout<<ans+one<<endl;
}

void init()
{
}

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

对于正整数,越乘代价肯定越大,所以肯定是每次只选一个数最优。而对于 1,其可以和大于 1 的数组合消耗掉,最后对于剩下的若干个 1 一起选只计算一次代价即可。那么就是顺序遍历,每找到一个大于 1 的数就让其和前面的 1 配对,维护一下上次配对之后有无 1 即可。

二、B. Everything Everywhere

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
T gcd(T a,T b){
    return b==0?a:gcd(b,a%b);
}

template<typename T>
T lcm(T a,T b){
    return a/gcd(a,b)*b;
}

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

    ll ans=0;
    for(int i=2;i<=n;i++)
    {
        if(gcd(a[i],a[i-1])==max(a[i],a[i-1])-min(a[i],a[i-1]))
        {
            ans++;
        }
    }
    cout<<ans<<endl;
}

void init()
{
}

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

令区间 gcd 为 g,那么区间 min/max 肯定都是 g 的倍数,那么就有 。那么也就有 ,所以就要求最大最小值之间不能有任何其他 g 的倍数出现。又因为是排列,所以合法的区间长度必定为 2。那么就只需要遍历一遍,每次 check 相邻两个位置即可。

三、C. Mental Monumental (Easy Version)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

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

    auto check=[&](int mid)->bool
    {
        multiset<int>st;
        vector<int>vis(mid);
        for(int i=1;i<=n;i++)
        {
            if(a[i]>=mid||vis[a[i]])
            {
                st.insert(a[i]);
            }
            else
            {
                vis[a[i]]=1;
            }
        }

        for(int i=0;i<mid;i++)
        {
            if(vis[i])
            {
                continue;
            }

            auto iter=st.upper_bound(2*i);
            if(iter==st.end())
            {
                return false;
            }

            st.erase(iter);
        }
        return true;
    };

    int l=0;
    int r=mx+1;
    int m;
    int ans;
    while(l<=r)
    {
        m=l+r>>1;
        if(check(m))
        {
            ans=m;
            l=m+1;
        }
        else
        {
            r=m-1;
        }
    }
    cout<<ans<<endl;
}

void init()
{
}

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

因为 MEX 具有单调性,所以可以二分答案,那么就是每次检查能否冲到 mid 这个数。那么对于大于等于 mid 的数就都可以作为候选数,而对于小于 mid 的数,肯定是直接贪心地让其保持不变更优。其中,这些数如果出现多次,那么也是可以作为候选数的。

对于候补数 x,考虑其能通过取模变成什么数。令其变成的数为 i,那么就有 。将 看作模数本身,那么式子就变为 。由于又要求 ,那么就有 。所以对于候补数 x,其可以变成 。那么在从 0 到 mid-1 枚举判断时,对于没被填过的数 x,二分判断候补数中是否有大于 即可。

四、D. Reserved Reversals

太变态了这个题......

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

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

    vector<int>vmax(2);
    vector<int>vmin(2,INF);
    for(int i=1;i<=n;i++)
    {
        int p=a[i]%2;
        vmax[p]=max(vmax[p],a[i]);
        vmin[p]=min(vmin[p],a[i]);
    }

    auto check=[&](int p)->bool
    {
        int cur=0;
        for(int i=1;i<=n;i++)
        {
            if(a[i]%2!=p)
            {
                continue;
            }

            if(cur>a[i])
            {
                if(vmax[p^1]<cur&&vmin[p^1]>a[i])
                {
                    return false;
                }
            }

            cur=max(cur,a[i]);
        }
        return true;
    };

    if(check(0)&&check(1))
    {
        YES;
    }
    NO;
}

void init()
{
}

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

首先,不难想到若相邻两数一奇一偶,那么肯定是可以任意交换的。那么对于一个数,其肯定可以通过交换穿过一个所有数奇偶性均与其不同的区间。

由这点得到启发,开始对脑电波。考虑先让奇数和偶数分开,然后让其分别有序。那么最后再像归并排序一样,把两部分整合到一起即可。

那么对于奇偶性相同的两数 (x,y),其不合法当且仅当其构成一个逆序对。那么当出现这种情况,此时就需要拉过来一个奇偶性和这两数不同的数 c,使得 c 是三者的最大或最小值。只有这样才可以通过 c 把 x,y 交换过来。

由于逆序对很多,发现可以不用每个都判断。在分别枚举奇偶数的时候,对于当前数字 v,只需要判断其和前缀最大值是否构成了逆序对。如果构成了就直接拿奇偶性不同的数中的最大最小值判断即可。

具体的构造策略是,在分开奇偶块以后,认为左侧全奇数右侧全偶数。对于当前的奇数逆序对 (x,y),考虑将被这两数包围的奇数区间全移动到右侧最值 c 的前面。然后翻转这个区间,消除 (x,y) 这个逆序对,然后再移动回来,之后重复即可。

五、E. Mental Monumental (Hard Version)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Segment_Tree
{
    vector<T>data;
    //自定义
    vector<T>info;

    Segment_Tree(){}

    Segment_Tree(int n){
        data.assign(n<<2,0);
        info.assign(n<<2,0);
    }

    void add(int jobl,int jobr,T jobv,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            addLazy(i,jobv);
        }
        else{
            int m=(l+r)>>1;

            down(i);

            if(jobl<=m){
                add(jobl,jobr,jobv,l,m,i<<1);
            }
            if(m+1<=jobr){
                add(jobl,jobr,jobv,m+1,r,i<<1|1);
            }

            up(i);
        }
    }

    T query(int jobl,int jobr,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            return data[i];
        }
        
        int m=(l+r)>>1;

        down(i);

        //自定义
        T ans=INFLL;
        if(jobl<=m){
            ans=min(ans,query(jobl,jobr,l,m,i<<1));
        }
        if(m+1<=jobr){
            ans=min(ans,query(jobl,jobr,m+1,r,i<<1|1));
        }

        return ans;
    }

    //自定义
    void addLazy(int i,T v){
        data[i]+=v;
        info[i]+=v;
    }

    //自定义
    void up(int i){
        data[i]=min(data[i<<1],data[i<<1|1]);
    }

    //自定义
    void down(int i){
        if(info[i])
        {
            addLazy(i<<1,info[i]);
            addLazy(i<<1|1,info[i]);
            info[i]=0;
        }
    }
};

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

    int m=mx+1;

    Segment_Tree<ll>st(m+1);
    vector<int>vis(m+1);
    vector<int>cnts(m+1);
    int cur=0;
    for(int i=1;i<=n;i++)
    {
        //a[i]
        if(!vis[a[i]]&&a[i]<=cur)
        {
            vis[a[i]]=1;
            st.add(0,a[i],1,0,m,1);
        }
        //candidate
        else
        {
            cnts[a[i]]++;
            int L=(a[i]-1)/2;
            if(L>=0)
            {
                st.add(0,L,1,0,m,1);
            }
        }

        //push ans
        while(1)
        {
            //pick a[p]=cur
            if(cnts[cur]&&!vis[cur])
            {
                int L=(cur-1)/2;
                if(L>=0)
                {
                    st.add(0,L,-1,0,m,1);
                }

                if(st.query(0,m,0,m,1)>=0)
                {
                    vis[cur]=1;
                    cur++;
                }
                else
                {
                    if(L>=0)
                    {
                        st.add(0,L,1,0,m,1);
                    }
                    break;
                }
            }
            //use candidate
            else
            {
                st.add(0,cur,-1,0,m,1);
                if(st.query(0,m,0,m,1)>=0)
                {
                    cur++;
                }
                else
                {
                    st.add(0,cur,1,0,m,1);
                    break;
                }
            }
        }
        cout<<cur<<" ";
    }
    cout<<endl;
}

void init()
{
}

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

首先,根据 Easy Version 的结论,对于一个数 ,其能负责的只有 本身。那么还是一样的贪心策略,如果 这个位置目前还是空缺的,那么就让其知道占用单点位置。否则就可以让其变小,作为候补元素。

由于要输出每个位置的答案,且这个答案存在单调性,所以可以考虑维护一个答案指针 ans,每次看加入一个数后能否扩出去。那么首先,对于当前要填入的数字 ans,若前面存在某个 ,那么肯定是直接拿过来用最优,因为让更大的数去填这个空只会更困难!否则的话,才会考虑是否能用候补的数填上所有空缺的数。

要想解决这个问题,需要引入二分图匹配和 Hall 定理。

首先,图匹配是指从图中选出一些边,使得没有任何两条线连到同一个点上。之后,Hall 定理在二分图匹配中的应用为,给定一个二分图,判断某侧的所有点能否都被匹配上。对于定理的内容,首先可以理解为,在男女生相亲的二分图问题中,若有 K 个男生,他们喜欢的女生总数小于 K,那么就不可能完成匹配。那么推广一下就是,对于二分图 (U,V),集合 U 能被完全匹配的充要条件是,对于集合 U 的任意子集 S,与 S 相连的 V 中的节点个数大于等于 S 的大小。

那么在这个题中,考虑将 [0,ans) 范围内缺失的数字看作集合 U,将所有候补数看作集合 V。然后对于每个候补数 x,向范围 内的所有缺失的数连边。那么问题就变为,缺失的数能否被完全覆盖。

肯定不能枚举子集,考虑进行优化。举个例子可以发现,对于集合 {2,5},此时要考虑的候补数的能力就需要 。那么当集合变为 {2,3,4,5} 时,可以发现要考虑的候补数的能力还是 。那么也就是说,根本没有必要枚举元素不连续的子集,直接考虑所有后缀即可。所以定义候补数的能力 ,之后枚举每个后缀,要求候补数中 的个数大于等于 的空缺数个数。

考虑定义后缀数组 为能力 的候补数个数,减去 的空缺数个数的差值,那么能完全匹配就等价于对于任意后缀数组都有 。那么增加或删除一个能力为 L 的候补数,就等价于对区间 [0,L] 范围 ,添加或减少一个空缺数 x,就等价于对区间 [0,x] 范围 。由于还需要判断是否每个位置的值都大于等于 0,所以还需要范围查最小值,那么就可以用线段树维护了。

已经尽可能说的清楚了,感觉嘴好笨不会讲题呜呜......中间枚举后缀那块感觉很抽象啊,已经想不到什么更好的解释方法了qwq......

总结

这个 E 太变态了,或者说这一整场都很变态......

END

相关推荐
IT当时语_青山师__JAVA技术栈1 小时前
数组与链表深度解析:从内存布局到工业级实践
java·算法·面试
吃着火锅x唱着歌1 小时前
LeetCode 496.下一个更大元素I
算法·leetcode·职场和发展
咩咦1 小时前
C++学习笔记07:引用做返回值
c++·学习笔记·引用·static·引用返回
郭涤生1 小时前
C++ 20联合体(Union)
开发语言·c++
Fanfanaas1 小时前
Linux 系统编程 文件篇 (一)
linux·运维·服务器·c++·学习
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之字符串 --【回文字符串】:判断字符串是否为回文
c++·字符串·csp·高频考点·信奥赛·回文字符串·判断字符串是否为回文
Emberone1 小时前
C++ 模板进阶详解:从非类型参数到特化、偏特化与分离编译
开发语言·c++
不知名的忻1 小时前
关键路径(Java)
java·数据结构·算法·关键路径
凤凰院凶涛QAQ1 小时前
《C++转Java快速入手系列》实践篇:图书系统
java·开发语言·c++