1. 程式人生 > 實用技巧 >數列分塊入門9

數列分塊入門9

首先上題目

題目描述
給出一個長為n的數列,以及n個操作,操作涉及詢問區間的最小眾數。

第一行輸入一個數字n。

第二行輸入n個數字,第i個數字為a[i],以空格隔開。

接下來輸入n行詢問,每行輸入兩個數字l,r以空格隔開。

表示查詢位於[l,r]的數字的眾數。

這道題遇到了之前沒有遇到過的眾數問題,那麼我們怎麼解決呢?

眾所周知,直接暴力可以騙分

暴力顯然是不行的,因此我們要考慮其他方法。

根據陳立傑的區間眾數解題報告可知,a和b集合的眾數,一定屬於a集合的眾數與b集合的交集,即一定在這兩個集合的某一個集合中出現(似乎是廢話)

解題思路:

如果邊界是[l,r],l在第a塊,r在第b塊,可以分成三個部分:

1.l到a最後一塊

2.a+1~b-1塊

3.第b塊到r

根據上面的性質,如果我們預先處理a+1~b-1塊的眾數,再去遍歷判斷第一部分和第三部分是否有更合適的眾數,這道題就能做出來了。

具體細節在下面程式碼中說:

1.建立分塊:

#include<bits/stdc++.h>
using namespace std;
/*
    1.將陣列v[i]離散化,不記錄其值,只記錄其位數
    2.分塊查詢
*/ 
const int N=100005;
int n,blo,id,bl[N],v[N],val[N],cnt[N];
int f[1005][1005];
vector<int> ve[N];//儲存編號相同,就是數相同時原來的第幾個值,用來二分求眾數 
map<int,int> mp;

其中,bl儲存每個塊的邊界,val儲存原來的v陣列,cnt用來計算每個數出現的個數。

進入主函式

在主函式中,由於輸入進去的v陣列太大,我們並不需要那麼大的數

因此可以進行離散化,相同的數具有相同的標號,再用val來儲存原來的v陣列

當需要查詢其值大小的時候,只需要用val[v[i]]就行了

ve函式其實是查編號為v[i]的數在第幾位出現過,為後來的二分找下標做處理

int main()
{
    cin>>n;
    blo=200;//每一塊分成200,在後期尋找中好計算
    for(int i=1;i<=n;i++)
    {
        cin>>v[i];
        if(!mp[v[i]])//之前沒有出現過
        {
            mp[v[i]]=++id;//第幾個出現的數 
            val[id]=v[i];//記錄編號為i的原值 
        }
        v[i]=mp[v[i]];//記錄其是第幾個出現的數(離散化) 
        ve[v[i]].push_back(i);//第v[i]個出現的數在哪裡出現,此句話為記錄個數所用, 
    }

    for(int i=1;i<=n;i++) bl[i]=(i-1)/blo+1;
    for(int i=1;i<=bl[n];i++) pre(i);//將每一段進行眾數排序 
    for(int i=1;i<=n;i++)
    {
        int a,b;
        cin>>a>>b;
        if(a>b) swap(a,b);
        cout<<val[query(a,b)]<<endl;//輸出眾數 
    }
    return 0;
}

for(int i=1;i<=bl[n];i++) pre(i);

這個語句是為了預處理在[i,n]中每個區間的眾數,接下來上程式碼:

預處理眾數

我們將整個處理眾數的過程看成是一個表

那麼我們需要算出來每一塊到每一塊區間的眾數,包括其本身

假如說有四個區間a,b,c,d;

那麼我們就需要算a-b,a-c,a-d,b-c,b-d,c-d,a,b,c,d這一共10個區間的眾數。

我們可以將這個看成是一個表格

那麼就可以開一個二維陣列f[1000][1000]用來記錄每一塊的眾數是什麼

void pre(int x)//預處理每一段的眾數 
{
    memset(cnt,0,sizeof(cnt));//一開始進行初始化
    int mx=0,ans=0;
    for(int i=(x-1)*blo+1;i<=n;i++)//第x個塊左邊界到n中的眾數 
    {
        cnt[v[i]]++;//記錄每一段每個數出現的個數 
        int t=bl[i];//記錄這是第幾段 
        if(cnt[v[i]]>mx||(cnt[v[i]]==mx&&val[v[i]]<val[ans]))//找最小眾數 
            ans=v[i],mx=cnt[v[i]];
            //x是初始段,t是末尾段
        f[x][t]=ans;//這一段眾數是ans 
    }
}

至此,我們所需要的就已經準備好了。

查詢,解題

因為我們已經預先處理好每一塊的眾數,因此只需要判斷左右兩邊多出來的那一小部分中,有沒有數字可以當成是新的眾數。

其中第一個query函式是照應了在建函式中ve[v[i]].push_back(i)這一步,搜尋從l到r中標號為v[i]的數出現了多少次

int query(int l,int r,int x)//二分查詢這一塊內編號為x的數量(ve[v[i]].push_back(i);) 
{
    int t=upper_bound(ve[x].begin(),ve[x].end(),r)-lower_bound(ve[x].begin(),ve[x].end(),l);//大下標減去小下標
    //注意前面是upper_bound,後面是lower_bound
    return t; 
}
int query(int a,int b)
{
    int ans,mx;
    ans=f[bl[a]+1][bl[b]-1];//意思是在(bl[a]+1~bl[b]-1)這一序列中的眾數
    mx=query(a,b,ans);//查詢這個眾數在此區間裡出現的個數 
    for(int i=a;i<=min(bl[a]*blo,b);i++)
    {
        int t=query(a,b,v[i]);//暴力查詢左邊多餘部分每個數的數量 
        if(t>mx||(t==mx&&val[v[i]]<val[ans])) 
            ans=v[i],mx=t; 
    } 
    if(bl[a]!=bl[b])
        for(int i=(bl[b]-1)*blo+1;i<=b;i++)//暴力查詢右邊多餘部分每個數的數量 
        {
            int t=query(a,b,v[i]);
            if(t>mx||(t==mx&&val[v[i]]<val[ans]))
                ans=v[i],mx=t;
        }
    return ans;
}

快樂的結束掉這一題!