『回滚莫队及其简单运用』

<更新提示>

<第一次更新> 基础莫队和带修莫队能够看这个 课件c++


<正文>

回滚莫队

基础的莫队算法相信你们都已经熟悉了,而咱们知道,莫队算法的关键就在于如何进行区间的转移,这就可能涉及到不少的细节。有一类普通莫队不可解的问题就是在转移区间过程当中,可能出现删点或加点操做其中之一没法实现的问题。那么咱们就来探讨如何利用特殊的莫队算法来解决这类问题,而这种莫队算法就称之为回滚莫队算法算法

只加不减的回滚莫队

咱们考虑一个区间问题,若这个问题在区间转移中,加点操做得以实现,可是删点操做没法有效的实现时,就可使用以下的莫队算法:数组

\(1.\) 对原序列进行分块,并对询问按照以下的方式排序:以左端点所在的块升序为第一关键字,以右端点升序为第二关键字spa

\(2.\) 对于处理全部左端点在块\(T\)内的询问,咱们先将莫队区间左端点初始化为\(R[T]+1\),右端点初始化为\(R[T]\),这是一个空区间code

\(3.\) 对于左右端点在同一个块中的询问,咱们直接暴力扫描回答便可。orm

\(4.\) 对于左右端点不在同一个块中的全部询问,因为其右端点升序,咱们对右端点只作加点操做,总共最多加点\(n\)排序

\(5.\) 对于左右端点不在同一个块中的全部询问,其左端点是可能乱序的,咱们每一次从\(R[T]+1\)的位置出发,只作加点操做,到达询问位置便可,每个询问最多加\(\sqrt n\)次。回答完询问后,咱们撤销本次移动左端点的全部改动,使左端点回到\(R[T]+1\)的位置事件

\(6.\) 按照相同的方式处理下一块ip

根据其操做的过程可知,回滚莫队的时间复杂度仍然为\(O(n\sqrt n)\),而且,在回答询问的过程当中咱们只进行了加点操做,没有涉及删点操做,这样就完成了咱们须要的操做。rem

只减不加的回滚莫队

和上一种典型的回滚莫队相似,咱们还能够实现只有删点操做没有加点操做的回滚莫队,固然,这样的前提是咱们能够正确的先将整个序列加入莫队中,那么算法流程以下:

\(1.\) 对原序列进行分块,并对询问按照以下的方式排序:以左端点所在的块升序为第一关键字,以右端点降序序为第二关键字

\(2.\) 对于处理全部左端点在块\(T\)内的询问,咱们先将莫队区间左端点初始化为\(L[T]\),右端点初始化为\(n\),这是一个大区间

\(3.\) 对于左右端点在同一个块中的询问,咱们直接暴力扫描回答便可。

\(4.\) 对于左右端点不在同一个块中的全部询问,因为其右端点降序,从\(n\)的位置开始,咱们对右端点只作删点操做,总共最多删点\(n\)

\(5.\) 对于左右端点不在同一个块中的全部询问,其左端点是可能乱序的,咱们每一次从\(L[T]\)的位置出发,只作删点操做,到达询问位置便可,每个询问最多加\(\sqrt n\)次。回答完询问后,咱们撤销本次移动左端点的全部改动,使左端点回到\(L[T]\)的位置

\(6.\) 按照相同的方式处理下一块

一样地,回滚莫队的时间复杂度仍是\(O(n\sqrt n)\),而且咱们只使用了删点操做,只有在一开始时将整个序列加入到莫队中,这样就完成了咱们须要的操做。

那么咱们将经过两道例题来详细地了解整两种回滚莫队。

歴史の研究

Description

IOI国历史研究的第一人——JOI教授,最近得到了一份被认为是古代IOI国的住民写下的日记。JOI教授为了经过这份日记来研究古代IOI国的生活,开始着手调查日记中记载的事件。

日记中记录了连续N天发生的时间,大约天天发生一件。

事件有种类之分。第i天(1<=i<=N)发生的事件的种类用一个整数X_i表示,X_i越大,事件的规模就越大。

JOI教授决定用以下的方法分析这些日记:

  1. 选择日记中连续的一些天做为分析的时间段
  2. 事件种类t的重要度为t*(这段时间内重要度为t的事件数)
  3. 计算出全部事件种类的重要度,输出其中的最大值 如今你被要求制做一个帮助教授分析的程序,每次给出分析的区间,你须要输出重要度的最大值。

Input Format

第一行两个空格分隔的整数N和Q,表示日记一共记录了N天,询问有Q次。

接下来一行N个空格分隔的整数X_1...X_N,X_i表示第i天发生的事件的种类

接下来Q行,第i行(1<=i<=Q)有两个空格分隔整数A_i和B_i,表示第i次询问的区间为[A_i,B_i]。

Output Format

输出Q行,第i行(1<=i<=Q)一个整数,表示第i次询问的最大重要度

Sample Input

5 5
9 8 7 8 9
1 2
3 4
4 4
1 4
2 4

Sample Output

9
8
8
16
16

解析

大体题意:给定一个长度为\(n\)的序列,离线询问\(m\)个问题,每次回答区间内元素权值乘以元素出现次数的最大值。

咱们考虑用莫队来解决这个问题,显然,为了统计每一个元素的出现次数,咱们要用到桶。而加点操做就很好实现了,在桶中给元素的出现次数加一,并查看是否可以更新答案便可。可是删点操做就难以实现,当咱们删去一个点时,咱们难以得知新的最大值是多少,因此咱们用只加不减的回滚莫队。

那么回滚莫队中提到的撤销操做具体就是指在桶中减去出现次数,而无论答案是否改变。在下一次加点的过程当中,答案就得以统计了。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+20 , SIZE = 1020;
int n,m,size,T,raw[N],val[N],t,cnt[N],cnt_[N];
int belo[N],L[SIZE],R[SIZE];
long long ans[N],Max,a[N];
struct query{int l,r,id;}q[N];
inline void input(void)
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
        scanf("%lld",&a[i]) , raw[++t] = a[i];
    for (int i=1;i<=m;i++)
        scanf("%d%d",&q[i].l,&q[i].r) , q[i].id = i;
}
inline void discrete(void)
{
    sort( raw+1 , raw+t+1 );
    t = unique( raw+1 , raw+t+1 ) - (raw+1);
    for (int i=1;i<=n;i++)
        val[i] = lower_bound( raw+1 , raw+t+1 , a[i] ) - raw;
}
inline void setblocks(void)
{
    size = sqrt(n) , T = n/size;
    for (int i=1;i<=T;i++)
    {
        if ( i * size > n ) break;
        L[i] = (i-1) * size + 1;
        R[i] = i * size;
    }
    if ( R[T] < n ) T++ , L[T] = R[T-1] + 1 , R[T] = n;
    for (int i=1;i<=T;i++)
        for (int j=L[i];j<=R[i];j++)
            belo[j] = i;
}
inline bool compare(query p1,query p2)
{
    if ( belo[p1.l] ^ belo[p2.l] )
        return belo[p1.l] < belo[p2.l];
    else return p1.r < p2.r;
}
// 加点
inline void insert(int p,long long &Maxval)
{
    cnt[val[p]]++;
    Maxval = max( Maxval , 1LL * cnt[val[p]] * a[p] );
}
// 撤销
inline void resume(int p)
{
    cnt[val[p]]--;
}
inline void CaptainMo(void)
{
    sort( q+1 , q+m+1 , compare );
    int l = 1 , r = 0 , lastblock = 0;
    for (int i=1;i<=m;i++)
    {
        // 处理同一块中的询问
        if ( belo[q[i].l] == belo[q[i].r] )
        {
            for (int j=q[i].l;j<=q[i].r;j++) cnt_[val[j]]++;
            long long temp = 0;
            for (int j=q[i].l;j<=q[i].r;j++) 
                temp = max( temp , 1LL * cnt_[val[j]] * a[j] );
            for (int j=q[i].l;j<=q[i].r;j++) cnt_[val[j]]--;
            ans[ q[i].id ] = temp; 
            continue; 
        }
        // 若是移动到了一个新的块,就先把左右端点初始化
        if ( lastblock ^ belo[q[i].l] )
        {
            while ( r > R[belo[q[i].l]] ) resume(r--);
            while ( l < R[belo[q[i].l]]+1 ) resume(l++);
            Max = 0 , lastblock = belo[q[i].l];
        }
        // 单调地移动右端点
        while ( r < q[i].r ) insert(++r,Max);
        // 移动左端点回答询问
        long long temp = Max; int  l_ = l;
        while ( l_ > q[i].l ) insert(--l_,temp);
        // 回滚
        while ( l_ < l ) resume(l_++);
        ans[ q[i].id ] = temp;
    }
}
int main(void)
{
    input();
    discrete();
    setblocks();
    CaptainMo();
    for (int i=1;i<=m;i++)
        printf("%lld\n",ans[i]);
    return 0;
}

mex

Description

有一个长度为n的数组{a1,a2,…,an}。m次询问,每次询问一个区间内最小没有出现过的天然数。

Input Format

第一行n,m。

第二行为n个数。

从第三行开始,每行一个询问l,r。

Output Format

一行一个数,表示每一个询问的答案。

Sample Input

5 5
2 1 0 2 1
3 3
2 3
2 4
1 2
3 5

Sample Output

1
2
3
0
3

解析

这道题咱们莫队是思路就是用桶维护出现过的数字,那么\(mex\)值就是第一个不在桶中出现的数字。

咱们发现删点操做很容易实现,能够顺带的更新答案,可是加点操做难以实现,咱们原来的最小值在加点过程当中出现了,咱们就无从得知新的答案。显然,一开始将整个序列加入到桶里并统计答案是可行的,那么咱们就是用只删不加的回滚莫队。

撤销操做仍是在桶中更新,但无论答案的变化就能够了。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 200020 , SIZE = 1020;
int n,m,a[N],cnt[N],Min,size,T,ans[N];
int cnt_[N],ans_,belo[N],L[N],R[N];
struct query{int l,r,id;}q[N];
inline void input(void)
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for (int i=1;i<=m;i++)
        scanf("%d%d",&q[i].l,&q[i].r) , q[i].id = i;
}
inline void init(void)
{
    for (int i=1;i<=n;i++)
        if ( a[i] <= n+1 )
            cnt[ a[i] ]++;
    while ( cnt[ ans_ ] ) ans_++;
    // 先把整个序列加入桶,同时获得总体的答案
}
inline void setblocks(void)
{
    size = sqrt(n) , T = n/size;
    for (int i=1;i<=T;i++)
    {
        if ( i * size > n ) break;
        L[i] = (i-1)*size + 1;
        R[i] = i * size;
    }
    if ( R[T] < n ) T++ , L[T] = R[T-1] + 1 , R[T] = n;
    for (int i=1;i<=T;i++)
        for (int j=L[i];j<=R[i];j++)
            belo[j] = i;
}  
inline bool compare(query p1,query p2)
{
    if ( belo[p1.l] ^ belo[p2.l] )
        return belo[p1.l] < belo[p2.l];
    else return p1.r > p2.r;
}
// 删点
inline void remove(int p,int &Minval) 
{
    if ( a[p] > n+1 ) return;
    cnt[a[p]]--;
    if ( cnt[a[p]] == 0 ) Minval = min( Minval , a[p] );
}
// 撤销
inline void resume(int p)
{
    if ( a[p] > n+1 ) return;
    cnt[a[p]]++;
}
inline void CaptainMo(void)
{
    sort( q+1 , q+m+1 , compare );
    int l = 1 , r = n , lastblock = 0;
    for (int i=1;i<=m;i++)
    {
        // 处理同一块中的询问
        if ( belo[q[i].l] == belo[q[i].r] )
        {
            for (int j=q[i].l;j<=q[i].r;j++) 
                if ( a[j] <= n+1 ) cnt_[a[j]]++;
            int temp = 0;
            while ( cnt_[temp] ) temp++;
            ans[ q[i].id ] = temp;
            for (int j=q[i].l;j<=q[i].r;j++) 
                if ( a[j] <= n+1 ) cnt_[a[j]]--;
            continue;
        }
        // 若是移动到了一个新的块,就先把左右端点初始化
        if ( belo[q[i].l] ^ lastblock )
        {
            while ( r < n ) resume(++r);
            while ( l < L[belo[q[i].l]] ) remove(l++,ans_);
            Min = ans_ , lastblock = belo[q[i].l];
        }
        // 单调地移动右端点
        while ( r > q[i].r ) remove(r--,Min);
        // 移动左端点回答询问
        int temp = Min , l_ = l;
        while ( l_ < q[i].l ) remove(l_++,temp);
        // 回滚
        while ( l_ > l ) resume(--l_);
        ans[ q[i].id ] = temp;
    }
}
int main(void)
{
    input();
    init();
    setblocks();
    CaptainMo();
    for (int i=1;i<=m;i++)
        printf("%d\n",ans[i]);
    return 0;
}

<后记>

相关文章
相关标签/搜索