数据结构与算法:莫队(一):普通莫队与带修莫队

前言

被学长逼着跳过来学莫队了 qwq......

一、普通莫队

直接在第一个题里介绍吧。

1.莫队 / 小 B 的询问

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 ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
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,m,k;
    cin>>n>>m>>k;
    vector<int>a(n+1);   
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<array<int,3>>query(m+1);
    for(int i=1;i<=m;i++)
    {
        cin>>query[i][0]>>query[i][1];
        query[i][2]=i;
    }

    int blen=sqrt(n);
    vector<int>bi(n+1);
    for(int i=1;i<=n;i++)
    {
        bi[i]=(i-1)/blen+1;
    }

    //classic
    auto cmp1=[&](auto &x,auto &y)->bool
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        return x[1]<y[1];
    };

    //parity
    auto cmp2=[&](auto &x,auto &y)->bool
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        return bi[x[0]]%2?x[1]<y[1]:x[1]>y[1];
    };

    sort(query.begin()+1,query.end(),cmp2);

    ll sum=0;
    vector<ll>cnts(k+1);

    auto erase=[&](int x)->void
    {
        sum-=cnts[x]*cnts[x];
        cnts[x]--;
        sum+=cnts[x]*cnts[x];
    };

    auto add=[&](int x)->void
    {
        sum-=cnts[x]*cnts[x];
        cnts[x]++;
        sum+=cnts[x]*cnts[x];
    };

    vector<ll>ans(m+1);
    for(int l=1,r=0,i=1;i<=m;i++)
    {
        auto [jobl,jobr,id]=query[i];
        while(jobl<l)
        {
            add(a[--l]);
        }
        while(r<jobr)
        {
            add(a[++r]);
        }
        while(l<jobl)
        {
            erase(a[l++]);
        }
        while(jobr<r)
        {
            erase(a[r--]);
        }
        
        ans[id]=sum;
    }

    for(int i=1;i<=m;i++)
    {
        cout<<ans[i]<<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;
}

因为每次要查范围内所有数词频的平方,那么如果不考虑多条查询,就可以直接通过一个 cnts 词频数组维护。之后遍历整个区间,每次先减去之前的贡献,更新词频,再加上当前的贡献即可。

此时可以发现,如果要让这个区间扩大或者缩小,是可以做到 完成信息的更新。那么对于就可以将其看作一个窗口,每次根据区间扩大或缩小即可,但若查询的区间长度是 n 和 1 交替出现,那么窗口就会频繁扩大缩小,复杂度就会降低为 ,需要考虑优化。

考虑 将所有查询离线下来 ,之后的操作是重点。首先,**对整个数组进行分块,**之后对于离线下来的所有查询,存在两种处理策略。虽然两种策略的时间复杂度是一样的,但第二种策略的常数时间会更好一点。

第一种策略,考虑 先根据左边界所在块号从小到大排序,再根据右边界数值从小到大排序 。此时,依次对每条查询暴力滑窗,复杂度就可以做到 。之后,考虑证明这个复杂度,分为块内和块间两部分进行分析。

对于块内,因为右边界是从小到大排序的,所以最多就是整个数组都滑一遍,所以复杂度 。又因为一共 个块,所以右边界整体复杂度就是 的。对于左边界,由于其是乱序的,所以最差情况是左边界频繁左右滑动。但由于每个块的长度是 的,所以每条查询左边界的代价就是 的。那么对于每个块,左边界移动的规模就可以看作 。

对于两个块间切换时的代价,就是让上一个块的最后一个窗口变成当前块的第一个窗口的代价。又因为左右边界滑动的代价最多就是 的,且一共就 块,所以总代价就是 。

对于第二种策略,注意到在块与块切换的时候,每次窗口的右边界是有可能滑过一个很大的范围的。所以可以考虑对于右边界,奇数块从小到大排序,偶数块从大到小排序。虽然这样看似能很大程度上优化复杂度,但若所有查询都在奇数块内,这样写其实和上一种策略是一样的。

实现方面,考虑用闭区间表示窗口,初始左边界为 1 右边界为 0。然后每次都判断左右是否需要扩以及左右是否需要缩,写好 add 和 erase 函数即可。

2.小 Z 的袜子

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 ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
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*b/gcd(a,b);
}

void solve()
{
    int n,m;
    cin>>n>>m;
    vector<int>a(n+1);   
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<array<int,3>>query(m+1);
    for(int i=1;i<=m;i++)
    {
        cin>>query[i][0]>>query[i][1];
        query[i][2]=i;
    }

    int blen=sqrt(n);
    vector<int>bi(n+1);
    for(int i=1;i<=n;i++)
    {
        bi[i]=(i-1)/blen+1;
    }

    //classic
    auto cmp1=[&](auto &x,auto &y)->bool
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        return x[1]<y[1];
    };

    //parity
    auto cmp2=[&](auto &x,auto &y)->bool
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        return bi[x[0]]%2?x[1]<y[1]:x[1]>y[1];
    };

    sort(query.begin()+1,query.end(),cmp1);

    ll sum=0;
    vector<ll>cnts(n+1);

    auto erase=[&](int x)->void
    {
        sum-=cnts[x]*cnts[x];
        cnts[x]--;
        sum+=cnts[x]*cnts[x];
    };

    auto add=[&](int x)->void
    {
        sum-=cnts[x]*cnts[x];
        cnts[x]++;
        sum+=cnts[x]*cnts[x];
    };

    vector<array<ll,2>>ans(m+1);
    for(int l=1,r=0,i=1;i<=m;i++)
    {
        auto [jobl,jobr,id]=query[i];
        while(jobl<l)
        {
            add(a[--l]);
        }
        while(r<jobr)
        {
            add(a[++r]);
        }
        while(l<jobl)
        {
            erase(a[l++]);
        }
        while(jobr<r)
        {
            erase(a[r--]);
        }
        
        if(jobl==jobr)
        {
            ans[id][0]=0;
            ans[id][1]=1;
        }
        else
        {
            ll k=jobr-jobl+1;
            ll up=sum-k;
            ll down=k*(k-1);

            ll g=gcd(up,down);
            ans[id][0]=up/g;
            ans[id][1]=down/g;
        }
    }

    for(int i=1;i<=m;i++)
    {
        cout<<ans[i][0]<<"/"<<ans[i][1]<<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;
}

若区间长度为 k,a 这个数的词频为 a,那么选出 a 这个数的概率就是 。将所有数的概率累加起来,就有 ,拆开就有 。此时可以发现第一个式子的分子就是上个题的答案,第二个式子的分子就是 k 本身,所以答案就是 ,同除 gcd 化简即可。

3.大爷的字符串题

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 ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
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,q;
    cin>>n>>q;
    vector<ll>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<array<int,3>>query(q+1);
    for(int i=1;i<=q;i++)
    {
        cin>>query[i][0]>>query[i][1];
        query[i][2]=i;
    }

    vector<ll>sorted(n+1);
    for(int i=1;i<=n;i++)
    {
        sorted[i]=a[i];
    }

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

    int m=1;
    for(int i=2;i<=n;i++)
    {
        if(sorted[i]!=sorted[m])
        {
            sorted[++m]=sorted[i];
        }
    }

    auto bs=[&](int v)->int
    {
        int l=1;
        int r=m;
        int mid;
        int ans;
        while(l<=r)
        {
            mid=l+r>>1;
            if(sorted[mid]>=v)
            {
                ans=mid;
                r=mid-1;
            }
            else
            {
                l=mid+1;
            }
        }
        return ans;
    };

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

    int blen=sqrt(n);
    vector<int>bi(n+1);
    for(int i=1;i<=n;i++)
    {
        bi[i]=(i-1)/blen+1;
    }

    sort(query.begin()+1,query.end(),[&](auto &x,auto &y)
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        return x[1]<y[1];
    }); 

    int maxCnt=0;
    vector<int>cnts(n+1);
    vector<int>kinds(n+1);

    auto add=[&](int v)->void
    {
        kinds[cnts[v]]--;
        cnts[v]++;
        kinds[cnts[v]]++;
        maxCnt=max(maxCnt,cnts[v]);
    };

    auto erase=[&](int v)->void
    {
        if(cnts[v]==maxCnt&&kinds[cnts[v]]==1)
        {
            maxCnt--;
        }
        kinds[cnts[v]]--;
        cnts[v]--;
        kinds[cnts[v]]++;
    };

    vector<int>ans(q+1);
    for(int l=1,r=0,i=1;i<=q;i++)
    {
        auto [jobl,jobr,id]=query[i];
        while(jobl<l)
        {
            add(a[--l]);
        }
        while(r<jobr)
        {
            add(a[++r]);
        }
        while(l<jobl)
        {
            erase(a[l++]);
        }
        while(jobr<r)
        {
            erase(a[r--]);
        }

        ans[id]=-maxCnt;
    }

    for(int i=1;i<=q;i++)
    {
        cout<<ans[i]<<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;
}

牛魔的,这题自己读了半天没读明白,直接看左神的形式化题意了......

这道题其实就是每次求区间内众数的次数的相反数,那么因为只考虑数字的词频,所以就可以先对其离散化。之后,由于需要维护窗口中众数的词频 maxcnt,肯定需要维护每个数的词频 cnts,每次对入窗口的数看其词频能否更新 maxcnt。但在出窗口时,若当前数不是 maxcnt,那么是不用管的。但如果当前数是 maxcnt,此时就需要判断是否还有别的数词频同样是 maxcnt,所以考虑再维护一个 kinds 表示词频为 i 的数有几种。入窗口减去之前的加上当前的,出窗口时判断此时词频为 maxcnt 的数的个数是否为 1 即可。

4.异或序列

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 ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
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,m,k;
    cin>>n>>m>>k;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }    
    vector<array<int,3>>query(m+1);
    for(int i=1;i<=m;i++)
    {
        cin>>query[i][0]>>query[i][1];
        query[i][2]=i;
    }

    vector<int>pre(n+1);
    for(int i=1;i<=n;i++)
    {
        pre[i]=pre[i-1]^a[i];
    }

    int blen=sqrt(n);
    vector<int>bi(n+1);
    for(int i=1;i<=n;i++)
    {
        bi[i]=(i-1)/blen+1;
    }

    sort(query.begin()+1,query.end(),[&](auto &x,auto &y)
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        return x[1]<y[1];
    });

    const int MAXP=1<<17;
    vector<ll>cnts(MAXP);
    ll sum=0;
    
    auto add=[&](int v)->void
    {
        if(k)
        {
            sum-=cnts[v]*cnts[v^k];
        }
        else
        {
            sum-=(cnts[v]*(cnts[v]-1))/2;
        }

        cnts[v]++;

        if(k)
        {
            sum+=cnts[v]*cnts[v^k];
        }
        else
        {
            sum+=(cnts[v]*(cnts[v]-1))/2;
        }
    };

    auto erase=[&](int v)->void
    {
        if(k)
        {
            sum-=cnts[v]*cnts[v^k];
        }
        else
        {
            sum-=(cnts[v]*(cnts[v]-1))/2;
        }

        cnts[v]--;

        if(k)
        {
            sum+=cnts[v]*cnts[v^k];
        }
        else
        {
            sum+=(cnts[v]*(cnts[v]-1))/2;
        }
    };

    vector<ll>ans(m+1);
    for(int l=1,r=0,i=1;i<=m;i++)
    {
        auto [jobl,jobr,id]=query[i];
        jobl--;

        while(jobl<l)
        {
            add(pre[--l]);
        }
        while(r<jobr)
        {
            add(pre[++r]);
        }
        while(l<jobl)
        {
            erase(pre[l++]);
        }
        while(jobr<r)
        {
            erase(pre[r--]);
        }

        ans[id]=sum;
    }

    for(int i=1;i<=m;i++)
    {
        cout<<ans[i]<<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;
}

对于子数组异或问题,还是考虑转化成前缀异或。那么对于一个区间内的所有子数组,其必然可以从 l-1 到 r 的前缀和中得到。所以在扩窗口时,对于即将加入窗口的位置的前缀异或 x,其必然可以和当前窗口内所有前缀异或为 x^k 的位置组合产生贡献。注意,若 k=0,因为 x 就不能和自己一起产生贡献了,此时的贡献就变成从所有前缀异或为 x 的位置中选出 2 个的方案数。所以实现时,维护好每个前缀异或值的词频 cnts 以及当前窗口的贡献 sum。那么扩窗口和缩窗口时都按照这个逻辑考察前缀异或,先减去之前的贡献,再加上更新后的贡献即可。

二、带修改的莫队

还是直接在题里说吧。

1.数颜色 / 维护队列

傻逼卡常题......

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
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
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,m;
    scanf("%d%d",&n,&m);
    int a[n+1];
    for(int i=1;i<=n;i++)
    {
        scanf("%d",a+i);
    }
    array<int,4> query[m+1];
    array<int,2> update[m+1];
    int lenq=0,lenu=0;
    for(int i=1;i<=m;i++)
    {
        char op[5];
        int x,y;
        scanf("%s%d%d",op,&x,&y);
        if(op[0]=='Q')
        {
            lenq++;
            query[lenq]={x,y,lenu,lenq};
        }
        else
        {
            update[++lenu]={x,y};
        }
    }

    int blen=pow(n,0.666);
    vector<int>bi(n+1);
    for(int i=1;i<=n;i++)
    {
        bi[i]=(i-1)/blen+1;
    }

    sort(query+1,query+lenq+1,[&](auto &x,auto &y)
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        if(bi[x[1]]!=bi[y[1]])
        {
            return bi[x[1]]<bi[y[1]];
        }
        return x[2]<y[2];
    });

    const int MAXV=1e6;

    vector<int>cnts(MAXV+1);
    int kind=0;

    auto erase=[&](int v)->void
    {
        if(--cnts[v]==0)
        {
            kind--;
        }
    };

    auto add=[&](int v)->void
    {
        if(++cnts[v]==1)
        {
            kind++;
        }
    };

    auto moveTime=[&](int jobl,int jobr,int time)->void
    {
        auto &[pos,val]=update[time];
        if(jobl<=pos&&pos<=jobr)
        {
            erase(a[pos]);
            add(val);
        }
        
        swap(a[pos],val);
    };

    vector<int>ans(lenq+1);
    for(int l=1,r=0,t=0,i=1;i<=lenq;i++)
    {
        auto [jobl,jobr,jobt,id]=query[i];

        while(jobl<l)
        {
            add(a[--l]);
        }
        while(r<jobr)
        {
            add(a[++r]);
        }
        while(l<jobl)
        {
            erase(a[l++]);
        }
        while(jobr<r)
        {
            erase(a[r--]);
        }
        while(t<jobt)
        {
            moveTime(jobl,jobr,++t);
        }
        while(jobt<t)
        {
            moveTime(jobl,jobr,t--);
        }

        ans[id]=kind;
    }

    for(int i=1;i<=lenq;i++)
    {
        printf("%d\n",ans[i]);
    }
}

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

考虑对单点修改操作打上修改时间点的信息,之后考虑对每个查询操作,统计其前面进行过多少次修改事件 jobt。在按一定策略排好序后,还是一条一条考察每个查询滑窗,每次都暴力修改到当前的 jobt 即可。唯一不同的是,在修改时执行 swap 操作将要改的数和原始的数交换。这样后续回退的时候 swap 回来即可。

对于排序策略,考虑先按 jobl 所在块号从小到大排序,再按 jobr 的块号从小到大排序,之后按 jobt 的数值从小到大排序。那么若块长为 x,则块数为 。此时对于左右边界所在块号,将其看作一个二元组。又因为块数为 ,那么可能的组数就是 。那么对于同一组里的所有任务,其最多滑过所有时间,所以复杂度就是 的。又因为左右边界都在块内,所以左右边界的滑动规模就是 。所以对于一共的 组,整体复杂度就是 。对于组间代价,因为切换时左右边界和时间的变化最多就是 的,所以整体代价还是 的。所以整体代价 ,将这个函数画出来可以发现其是一个单谷函数。所以对其求导计算导数为零的位置,可以得到在 取到最优复杂度 。

2.F. Machine Learning

很重要的一个 trick!!

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,q;
    cin>>n>>q;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<array<int,4>>query(q+1);
    vector<array<int,2>>update(q+1);
    int lenq=0,lenu=0;
    for(int i=1,op,l,r,p,x;i<=q;i++)
    {
        cin>>op;
        if(op==1)
        {
            cin>>l>>r;
            lenq++;
            query[lenq]={l,r,lenu,lenq};
        }
        else
        {
            cin>>p>>x;
            lenu++;
            update[lenu]={p,x};
        }
    }

    vector<int>sorted(n+lenu+1);
    for(int i=1;i<=n;i++)
    {
        sorted[i]=a[i];
    }
    for(int i=1;i<=lenu;i++)
    {
        sorted[i+n]=update[i][1];
    }

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

    int len=1;
    for(int i=1;i<=n+lenu;i++)
    {
        if(sorted[len]!=sorted[i])
        {
            sorted[++len]=sorted[i];
        }
    }

    auto bs=[&](int v)->int
    {
        int l=1;
        int r=len;
        int m;
        int ans;
        while(l<=r)
        {
            m=l+r>>1;
            if(sorted[m]>=v)
            {
                ans=m;
                r=m-1;
            }
            else
            {
                l=m+1;
            }
        }
        return ans;
    };

    for(int i=1;i<=n;i++)
    {
        a[i]=bs(a[i]);
    }
    for(int i=1;i<=lenu;i++)
    {
        update[i][1]=bs(update[i][1]);
    }

    int blen=pow(n,0.6666);
    vector<int>bi(n+1);
    for(int i=1;i<=n;i++)
    {
        bi[i]=(i-1)/blen+1;
    }

    sort(query.begin()+1,query.begin()+lenq+1,[&](auto &x,auto &y)
    {
        if(bi[x[0]]!=bi[y[0]])
        {
            return bi[x[0]]<bi[y[0]];
        }
        if(bi[x[1]]!=bi[y[1]])
        {
            return bi[x[1]]<bi[y[1]];
        }
        return x[2]<y[2];
    });

    vector<int>cnts(len+1);
    vector<int>kind(len+1);

    auto erase=[&](int v)->void
    {
        kind[cnts[v]]--;
        cnts[v]--;
        kind[cnts[v]]++;
    };

    auto add=[&](int v)->void
    {
        kind[cnts[v]]--;
        cnts[v]++;
        kind[cnts[v]]++;
    };

    auto move=[&](int jobl,int jobr,int time)->void
    {
        auto &[p,v]=update[time];
        if(jobl<=p&&p<=jobr)
        {
            erase(a[p]);
            add(v);
        }
        swap(a[p],v);
    };

    vector<int>ans(lenq+1);
    for(int l=1,r=0,t=0,i=1;i<=lenq;i++)
    {
        auto [jobl,jobr,jobt,id]=query[i];

        while(jobl<l)
        {
            add(a[--l]);
        }
        while(r<jobr)
        {
            add(a[++r]);
        }
        while(l<jobl)
        {
            erase(a[l++]);
        }
        while(jobr<r)
        {
            erase(a[r--]);
        }
        while(t<jobt)
        {
            move(jobl,jobr,++t);
        }
        while(jobt<t)
        {
            move(jobl,jobr,t--);
        }

        int mex=1;
        while(mex<=n&&kind[mex]>0)
        {
            mex++;
        }
        ans[id]=mex;
    }

    for(int i=1;i<=lenq;i++)
    {
        cout<<ans[i]<<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;
}

还是考虑定义 cntsi 为 i 这个数出现的词频,再定义 kindi 为词频为 i 的数出现了几种。那么在增加和删除数的时候,可以发现这两个数组的更新是很容易的。而对于查询操作,可以直接从 1 开始暴力枚举 kindi。因为若前 i 个词频的数都出现过,即使各出现了一次,那么数的个数也是呈等差数列增长的,所以枚举的代价是 的。又因为数字的范围很大,所以需要离散化。

第 inf 次怀念以前的 cf......

总结

能发明出这玩意儿的绝对是个人才,这个排序策略也太无敌了......

END

相关推荐
jimy122 分钟前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
hhb_61835 分钟前
AIRAGDebug:一键定位RAG链路异常
人工智能·python·算法
朝朝辞暮i1 小时前
C++ 第 6 课:switch —— 多状态选择
开发语言·c++·算法
朝朝辞暮i1 小时前
C++ 第 4 课:if / else —— 让程序自己做决定
开发语言·c++
苏打豆1 小时前
Pinocchio源码阅读——刚体运动物理量空间变换及求导
c++·线性代数·算法·矩阵·机器人
m0_380743872 小时前
Qt中导航栏实现的详细指南
开发语言·c++
FluxArt2 小时前
GPT Image 2.5 中文提示词教程:Flux Art 可复用模板
前端·gpt·算法
估值探索者2 小时前
【Python量化策略实战 #02】多因子合成mom和vol两因子打分合成
开发语言·c++·python·数据挖掘·c#
思麟呀2 小时前
位运算:从一个整数到一个集合
c语言·数据结构·c++·算法
阜阳山君GEO2 小时前
豆包 SEED 新版本 GEO 优化:信源金字塔、打分体系与避坑落地指南
人工智能·算法