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

前言

被学长逼着跳过来学莫队了 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;
}

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

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

总结

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

END

相关推荐
风筝在晴天搁浅2 小时前
n个六面的骰子,扔一次之后和为k的概率是多少?
算法
KuaCpp2 小时前
C++面向对象(速过复习版)
开发语言·c++
MATLAB代码顾问3 小时前
Python实现蜂群算法优化TSP问题
开发语言·python·算法
代码飞天3 小时前
机器学习算法和函数整理——助力快速查阅
人工智能·算法·机器学习
jiushiapwojdap3 小时前
LU分解法求解线性方程组Matlab实现
数据结构·其他·算法·matlab
笨笨饿4 小时前
69_如何给自己手搓一个串口
linux·c语言·网络·单片机·嵌入式硬件·算法·个人开发
纽扣6674 小时前
【算法进阶之路】链表进阶:删除、合并、回文与排序全解析
数据结构·算法·链表
消失的旧时光-19435 小时前
统一并发模型:线程、Reactor、协程本质是一件事(从线程到协程 · 第6篇·终章)
java·python·算法
智者知已应修善业5 小时前
【51单片机不用数组动态数码管显示字符和LED流水灯】2023-10-3
c++·经验分享·笔记·算法·51单片机