初級莫隊算法的詳解(附加一個例題)

例題鏈接:D-query

題目大意:給你n(1 ≤ n ≤ 30000)個數(1 ≤ ai ≤ 10^6),q(1 ≤ q ≤ 200000)個詢問,每個詢問有l,r兩個數,問這個區間內有多少個不同的數。

Input
5
1 1 2 1 3
3
1 5
2 4
3 5

Output
3
2
3 

最暴力的方法就是,先從【1,5】區間一個一個查詢,在從【2,4】區間一個一個查詢……
但是這樣可能會出現n^2的時間複雜度,所以這樣是肯定不行的。

莫隊核心思想(包括帶修莫隊也是這樣,只不過是加入和修改的部分而已)
我們可以先查找【1,5】區間,記錄每一個元素的個數,再拓展到【2,5】區間,這樣是隻是少了1號元素,再拓展到【2,4】區間,少了5號元素,所以【2,4】的答案就出來了,以此類推可以求到所有的答案。

不過這樣是肯定不行的,因爲區間的順序不同所消耗的時間是不同的,也有可能會超時。
此時我們要對所有的區間進行排序,使得所需要查詢的次數降低,以此來降低時間複雜度。

不知道我們該如何排序呢?
我們要利用分塊的思想(只是不清楚爲啥要這樣,可以降低時間複雜度),可以將時間複雜度降到O(n√n)。
我們的排序是按照左段點所在的塊爲第一關鍵字,以右端點爲第二關鍵字(這點很重要)。

排完序後從左往右處理詢問(離線),過程就是上述的核心思想部分。

基本上莫隊的做法就是上述部分,只不過不同的題目略有不同而已。

下面是每一個分部:

  block=sqrt(n*1.0);

1.塊的大小:一般是sqrt(n),n爲數組長度。

bool cmp(node f1,node f2)
{
    if(f1.blk==f2.blk)//左段點所在的塊
        return f1.t<f2.t;
    return f1.s<f2.s;
}

2.對查詢的排序
3.如何通過加1或-1,從上一個區間變化到本區間。

下面是詳細代碼:

#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
using namespace std;

#define mem(a,b) memset(a,b,sizeof(a))
const int maxn=30000+10;
const int max1=1e6+9;
typedef long long ll;

int a[maxn];
int inq[max1],sum[maxn*10];
int block,ans;

struct node
{
    int s,t;//區間範圍
    int id;//查詢的編號
    int blk;//左端點所在的塊
} edge[maxn*10];

bool cmp(node f1,node f2)//排序
{
    if(f1.blk==f2.blk)
        return f1.t<f2.t;
    return f1.s<f2.s;
}

void init()
{
    mem(inq,0);
    ans=0;
}
//每道不同的題目思想不同,下面兩個函數的內容也各不相同
void up(int x)//擴充區間
{
    if(inq[a[x]]==0)
        ans++;
    inq[a[x]]++;
}
void down(int x)//縮小區間
{
    inq[a[x]]--;
    if(inq[a[x]]==0)
        ans--;
}

int main()
{
    int n,m;
    while(~scanf("%d",&n))
    {
        init();
        block=sqrt(n*1.0);//塊的大小
        for(int i=1; i<=n; i++)
            scanf("%d",&a[i]);
        scanf("%d",&m);

        for(int i=0; i<m; i++)//離線記錄
        {
            scanf("%d%d",&edge[i].s,&edge[i].t);
            edge[i].id=i;
            edge[i].blk=edge[i].s/block;
        }
        sort(edge,edge+m,cmp);

        int l=1;//初始左右區間
        int r=0;
        ans=0;//答案
        for(int i=0; i<m; i++)
        {
            int s=edge[i].s,t=edge[i].t;
            while(r<t)//區間擴充
            {
                r++;
                up(r);
            }
            while(l>s)//區間擴充
            {
                l--;
                up(l);
            }
            while(r>t)//區間縮小
            {
                down(r);
                r--;
            }
            while(l<s)//區間縮小
            {
                down(l);
                l++;
            }
            sum[edge[i].id]=ans;//記錄答案
        }
        for(int i=0; i<m; i++)
            printf("%d\n",sum[i]);
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章