HDU4638 Group (樹狀數組+離線處理)

在網上看了好多大神的博客,反正自己是絕對想不到怎麼做的,好不容易感覺自己迷迷糊糊懂了個大概,就先記下來,免得以後忘記。有的地方思路很勉強甚至是錯的,歡迎大家指教糾正。

題意:給你一個1~n的排列序列,m次查詢,每次給出一個區間,求這個區間內使每個分組裏面的數字都是連續的最小的分組方法。(題目鏈接:點擊打開原題鏈接

題解:因爲求的是區間內的連續數字的分組,那麼對於當前要查詢的區間來說,它前面的數字就是無效的,所以我們可以將所有要查詢的區間儲存起來然後排序,從第一個區間開始把左邊的不會用到的數“刪除”,這裏說的“刪除”不是將這個數從數列裏面去掉,而是指把它自身對最終答案的貢獻變爲0 。怎麼做到這一點呢?

首先我們要初始化每個位置的數對當前整個序列來說的分組貢獻(在樹狀數組上操作,方便單點更新和區間查詢):

(1)當一個數單獨出現而它左右的數字並沒有出現的時候,這個數只能單獨被分爲一個區間,那它對分組數量的貢獻就爲1;

(2)當一個數出現時,它的前面或者後面的數字已經出現了,那麼這個數就能和它組成一個區間,所以不需要新增分組,它對分組數量的貢獻爲0;

(3)當一個數出現前它的前後的數字都已經出現了,那麼這個數就能將前後兩個數連起來使兩個區間變成一個區間(一個數的前後兩個數原本是不能連在一起的),那麼區間數就會減少,所以它對分組數的貢獻爲-1;

初始化以後,我們再根據要查詢的區間對區間左邊的數進行“刪除”,對於區間左邊的每一個數,我們分幾種情況討論:

(1)這個數比它前後的數都要後出現,那麼它前後的數已經被刪掉了,所以這個點對區間的貢獻爲-1;

(2)這個數比它前後的數都要先出現,原本我們初始化的時候這個點要算一個區間的,現在更新-1,,而此時他前後的數都還在,刪掉了它以後它前後的數就分別表示一個區間了,所以前後的數對區間的貢獻都更新爲1;

(3)這個數只比它前面的數先出現,將他自己的貢獻更新爲-1,它前面的數在他被刪掉以後只能自己做一個分組所以更新貢獻爲1;

(4)這個數只比它後面的數先出現,將他自己的貢獻更新爲-1,它後面的數在他被刪掉以後只能自己做一個分組所以更新貢獻爲1;

每次進行完區間刪除操作以後就查詢樹狀數組上從序列開始到當前查詢區間右端點的和保存下來,最後輸出就行了。

附上代碼(當然是舶來品,不過加了一些自己的註釋):

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxn=100000+10;
int v[maxn],c[maxn];
int ans[maxn],a[maxn],n;
struct node
{
    int s;
    int t;
    int id;
} q[maxn];
bool cmp(node n1,node n2)
{
    return n1.s<n2.s;
}
int lowbit(int x)
{
    return x&(-x);
}
void add(int i,int d)//樹狀數組單點更新
{
    while(i<=n)
    {
        c[i]+=d;
        i+=lowbit(i);
    }
}
int sum(int i)//樹狀數組區間求和
{
    int ret=0;
    while(i>0)
    {
        ret+=c[i];
        i-=lowbit(i);
    }
    return ret;
}
int main()
{
    int T,i,j,m;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);
        memset(v,0,sizeof(v));
        memset(c,0,sizeof(c));
        memset(ans,0,sizeof(ans));
        for(i=1; i<=n; i++)
        {
            scanf("%d",&a[i]);
            v[a[i]]=i;//記錄每個數的位置
        }
        v[0]=n+n;
        v[n+1]=n+n;
        for(i=1; i<=n; i++) //從1~n進行更新
        {
            if(i>v[a[i]-1]&&i>v[a[i]+1])//i表示位置,大於就是在他前後的數都出來了以後纔出來
                add(i,-1);
            else if(i<v[a[i]-1]&&i<v[a[i]+1])//小於就是表示他出現了他前後的數還沒有出現
                add(i,1);
        }
        for(i=1; i<=m; i++)
        {
            scanf("%d%d",&q[i].s,&q[i].t);
            q[i].id=i;
        }
        sort(q+1,q+m+1,cmp);
        i=1;
        j=1;
        while(j<=m)
        {
            while(i<=n&&q[j].s>i)//將左邊的刪除
            {
                if(i>v[a[i]-1]&&i>v[a[i]+1])//落後它前後的數,那原本是應該減少線段的
                    //既然後已經落後了那麼肯定他前面的數也要刪掉
                    add(i,-1);
                else if(i<v[a[i]-1]&&i<v[a[i]+1])//如果該位置本來是超前它前後的數
                {
                    add(i,-1);//原本這個點要算一個新的區間因爲他出現的時候它相鄰的數沒有出現,現在不要這個點了那就要刪除
                    int mi=v[a[i]-1]<v[a[i]+1]? v[a[i]-1]:v[a[i]+1];
                    int ma=v[a[i]-1]>v[a[i]+1]? v[a[i]-1]:v[a[i]+1];
                //add(v[a[i]-1],1);add(v[a[i]+1],1);
                    add(mi,1);//這個點被刪除了那它相鄰的兩個數就只能是單獨的區間不能連續了
                    add(ma,1);//所以都更新爲1
                }
                else if(i<v[a[i]-1])//如果它比它前面的數先出現
                {
                    add(i,-1);
                    add(v[a[i]-1],1);
                }
                else
                {
                    add(i,-1);
                    add(v[a[i]+1],1);//總結一下,總之就是要刪除這個點的話它前後的點只要比它後出現
                    //所以在它被刪除以後不能有一個連續的區間所以它必須自己單獨表示1
                }
                i++;
            }
            while(j<=m&&q[j].s<=i)//統計每個查詢區間的和
            {
                ans[q[j].id]=sum(q[j].t);
                j++;
            }
        }
        for(i=1; i<=m; i++)
            printf("%d\n",ans[i]);
    }
    return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章