SDUT 3513 皮卡丘的夢想 (二進制+線段樹) -- 解題報告

題面

皮卡丘的夢想
Time Limit: 1000ms Memory limit: 65536K

題目描述
一天,一隻住在 501 的皮卡丘決定發奮學習,成爲像 LeiQ 一樣的巨巨,於是他向鎮上的賢者金桔請教如何才能進化成一隻雷丘。
金桔告訴他需要進化石才能進化,並給了他一個地圖,地圖上有 n 個小鎮,並且標註了每個小鎮上可收集的進化石。
但是皮卡丘拿到地圖就蒙圈了,他可不知道自己到底需要哪種進化石,而且由於經費有限,他只能去某幾個相鄰的小鎮,所以他機智地找到了善於編程的你,詢問你這些鎮上可收集的進化石有哪些,然後再自己決定行程。

輸入
首先輸入一個整數 T (1 <= T <= 10),代表有 T 組數據。
每組數據的第一行輸入一個整數 n (1 <= n <= 100000),代表有 n 個小鎮。
接下來的 n 行表示第 1 個到第 n 個的小鎮的信息。每行先輸入一個整數 m (0 <= m <= 30),代表這個小鎮上進化石的種類數,緊接着輸入 m 個整數,代表進化石的種類編號(編號從 1 開始,不超過 30)。
之後的一行輸入一個整數 q (1 <= q <= 25000),代表皮卡丘有 q 次詢問。 接下來的 q 行每行輸入兩個整數 l, r (1 <= l <= r <= n),表示他想詢問從第 l 個到第 r 個小鎮上可收集的進化石有哪幾種。

輸出
對於每組輸入,首先輸出一行 “Case T:”,表示當前是第幾組數據。
接下來對於每一次詢問,按編號升序輸出所有可收集的進化石。如果沒有進化石可收集,則輸出一個小豪的百分號 “%”(不要問我爲什麼,出題就是這麼任性)。

示例輸入
1
3
2 3 10
3 1 2 4
0
3
1 2
2 3
3 3

示例輸出
Case 1:
1 2 3 4 10
1 2 4
%

提示

來源
bLue

解題思路

作爲大一小學弟一枚,第一次在學校 OJ 上出題,還連上了三道(3511、3512、3513,推薦大家都去做做),感覺好方啊,還好有 raincloud 老師和 LeiQ 學長幫忙。

言歸正傳,這道題一看就是很裸的區間查詢(畢竟弱弱一枚,只能出這種難度的題了QAQ~),很容易想到要用線段樹(不會的童鞋們抓緊去學吧,有一點樹的基礎就很容易入門)。當然肯定不能就這樣算了,爲了讓題目中的「皮卡丘」同學火一把,肯定要增加一些難度,於是便有了查詢區間內進化石有哪幾種的設定,所以如果只用線段樹,在每次查詢的時候照舊遍歷所有進化石的話還是會超時的(怪我咯)。這時我們就需要更高效的方法來存儲和查詢所有進化石的狀態,這種方法就是利用二進制表示存在狀態。

我們用二進制數的每一位來表示每種進化石的有無,也即該小鎮進化石的存在狀態。舉個栗子,對於有 1、4 號進化石的小鎮,我們可以用二進制數 1001 來表示。它的含義是:從右向左依次表示第 1 個到第 n 個進化石的有無,1 表示有,0 表示無。而且很容易想到,由於每一位只有 0 或 1 兩種可能,且每一位都對應固定的編號,所以對於任意一個二進制數,都能保證唯一對應一種存在狀態。

解決了如何表示存在狀態的問題,下一步就是如何存儲了。再舉個栗子,當前我們的進化石存在狀態爲:1、4,對應二進制 1001,如果我們加入一個 3 號進化石,則應變爲 1101,也就是讓倒數第三位變成 1。這裏需要用到位運算(沒學過的趕快去學):對於 1001,我們讓它與 0100(只含有 3 號石的狀態)進行或運算,即兩數對應的位有一個或兩個爲 1 時結果爲1,否則爲 0,運算結果爲 1101。這樣我們使用或運算就可以實現兩個狀態的合併。至於如何表示單個進化石的狀態,很簡單,使用左移運算就可以了,慄如:表示 3 號石存在,只需將 1(0001)左移 3-1=2 位即得到 0100。

這樣,我們只需要把二進制和線段樹結合一下就可以愉快地告別 TLE 了。在存儲時,每一個結點都表示它的左右子結點的合併狀態,即對左右子結點進行或運算後的結果,而葉結點直接存儲狀態。在查詢時,只需要遍歷結果對應二進制的每一位來輸出即可。

這樣我們就可以寫出本題的代碼了:

#include <cstdio>
#include <cstring>
using namespace std;

const int MAX = 100001;
int st[MAX<<2];

void Initialize();
void Update(int node, int l, int r, int index, int value);
int Query(int node, int l, int r, int il, int ir);

int main(int argc, char const *argv[]) {
    int t, n, m, q, l, r;
    scanf("%d", &t);
    for(int i=1; i<=t; ++i) {
        printf("Case %d:\n", i);
        scanf("%d", &n);
        Initialize();
        for(int j=1; j<=n; ++j) {
            scanf("%d", &m);
            int binary = 0, idx;
            while(m--) {
                scanf("%d", &idx);
                //每讀入一個編號,更新一次狀態
                binary |= 1<<(idx-1);
            }
            //在線段樹中更新此下標的狀態
            Update(1, 1, n, j, binary);
        }
        scanf("%d", &q);
        while(q--) {
            scanf("%d %d", &l, &r);
            int res = Query(1, 1, n, l, r);
            int digit = 1;
            bool is_first = true;
            //遍歷狀態來輸出存在的進化石編號
            while(res) {
                //如果當前狀態的最後一位爲1,則輸出
                if(res & 1) {
                    if(is_first) is_first = false;
                    else printf(" ");
                    printf("%d", digit);
                }
                //判斷完一位,右移刪掉最後一位
                res >>= 1;
                digit++;
            }
            if(digit == 1) printf("%%");
            printf("\n");
        }
    }

    return 0;
}

void Initialize() {
    memset(st, 0, sizeof(st));
}

void Update(int node, int l, int r, int index, int value) {
    int mid = (l + r) >> 1;
    if(l == r) {
        st[node] |= value;
        return;
    }
    if(index <= mid)
        Update(node<<1, l, mid, index, value);
    else
        Update(node<<1|1, mid+1, r, index, value);
    //更新爲左右子結點的合併狀態
    st[node] = st[node<<1] | st[node<<1|1];
}

int Query(int node, int l, int r, int il, int ir) {
    int mid = (l + r) >> 1;
    if(l == il && r == ir)
        return st[node];
    if(ir <= mid)
        return Query(node<<1, l, mid, il, ir);
    else if(il > mid)
        return Query(node<<1|1, mid+1, r, il, ir);
    else
        //跨區間查詢時,返回左右兩邊狀態的合併狀態
        return Query(node<<1, l, mid, il, mid)
            | Query(node<<1|1, mid+1, r, mid+1, ir);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章