顯示具有 Segment Tree 標籤的文章。 顯示所有文章
顯示具有 Segment Tree 標籤的文章。 顯示所有文章

2015年5月11日 星期一

Segment Tree + BIT (Fenwick Tree) In Deep

今天GCJ Round 1C也GG了...要是早點交A-large 或者不做C-small...應該就過了...
看來要拿T-Shirt還有段距離, 明年請早 :"(

回到正常的步伐, 繼續原本計劃好的學習旅程
(大學時期每次打完比賽都會頹廢很久, 現在能直視自己的失敗真的感恩)

再來一篇記Concept / 技巧的文...
(因為這陣子除了GCJ都沒怎樣打完整的比賽, 較少比賽題目的紀錄)

這篇是針對Segment Tree 和 BIT 的一些深一步的了解而寫的

說是In Deep其實也是一些對Div.1 玩家來說很common的東東

其中有些concept我也不是100%肯定 但暫時理解是這樣

Segment Tree + BIT (Fenwick Tree) In Deep

首先故事是這樣的, 萬惡也是從Stack Overflow開始

我閑逛Stack Overflow的時候, 看到了一題有關SPOJ的題目

是關於Segment Tree的, 由於該問發問者的名稱很不錯 (叫Wen_dy)

我決定看一下, 發現是很赤裸的Segment Tree題目
  1. Range Add Value
  2. Range Query Sum

BIT (Fenwick Tree) Part

本想借此重溫一下Segment Tree...沒想到一下子就卡住了

才發現根本一直都沒搞清楚Segment Tree的結構 (所謂沒搞清楚下面會再講)

然後更嚴重的是, 看了SPOJ下面的Comment, 竟然有人說可以用BIT做!

怎樣想也不懂...結果就跑去查了....

幸好找到了一個很好很好的BIT 說明 Reference:

https://kartikkukreja.wordpress.com/2013/12/02/range-updates-with-bit-fenwick-tree/
裡面還有一個更好的Reference, 是來自Quora的, 有信心保證:
http://programmingcontests.quora.com/Tutorial-Range-Updates-in-Fenwick-Tree

讓我引用一下裡面說的東西:
基本上, BIT出現的形式有3種: 
Range Update Point Query . Point Update Range Query, 和最麻煩的Range Update Range Query
然後文中給出了頭2種基本的例子...大同小異的

Range Update Point Query 
Point Update Range Query


然後就是重中之中, 這題目要用到的技巧: Range Update Range Query!

長話短說, 這個技巧是用上 兩個BIT Array去完成的
update(ft, p, v):
for (; p <= N; p += p&(-p))
ft[p] += v

# Add v to A[a...b]
update(a, b, v):
update(B1, a, v)
update(B1, b + 1, -v)
update(B2, a, v * (a-1))
update(B2, b + 1, -v * b)

query(ft, b):
sum = 0
for(; b > 0; b -= b&(-b))
sum += ft[b]
return sum

# Return sum A[1...b]
query(b):
return query(B1, b) * b - query(B2, b)

# Return sum A[a...b]
query(a, b):
return query(b) - query(a-1)
上面的Code中的B1 是普通的Range Update Point Query, 可以用它來Range Add和Query某一個位置的value a[p]

然後考慮一下 index p 的range, 來想一下Range Update [a,b] Add v 後
S[p] = Sum [1..p] 的值會怎樣
  1. 1 <= p < a : 0
  2. a <= p <= b : v * (p – (a – 1))
  3. b < p <= N : v * (b – (a – 1))
留意假設只有這個Update的話, 只有上面第2點的range內會有value, 其它的value 都是0
所以如果我們Query B1(p) 的話 (就如上面說的, 就是普通的Point Query, 所以是a[p])
再乘以p自己, 然後得出的值如下:
  1. 0
  2. v*p
  3. 0
對比上面我們想要得到的算式 (就是Sum[1..p]的算式)
不難發現對於每個range, 其實也是 Query B1(p) * p 減去一個constant X:
  1. 0 - 0  = 0   --> X = 0
  2. v*p - v*(a-1) =  v*(p-(a-1)) --> X =  v*(a-1)
  3. 0 - (-v*(b-(a-1))) = v*(b-(a-1)) --> X =  -v*b + v(a-1)
B2 說白了, 就是儲存這個X的另一個 BIT (也是Range Update Point Query的形式)了
For 一個Update Query [a,b] + v: 
B2 先 Update B2 (a, v*(a-1)) <---所有 >= a 的 index都有影響
再Update B2 (b, -v*b) <-- v*(a-1) 的部分上面包括了

那麼: Query(L,R) = Sum[1..R] - Sum[1..L-1]  
= (Query(B1, R)*R - Query(B2, R)) - (Query(B1, L-1)*(L-1) - Query(B2, (L-1)))
(這兒順帶學習了C++一個很簡單的東西: 只要signature不同, function同名都可以, 於這兒不同種類的Update()/ Query() 利用會很方便)

再想個複雜一點的例子來說明Concept
假設現在有2個Range Query [a,b,v1] 和 [c,d,v2]
再假設現在想找 Sum[1..p] where p 只在 [c,d]內而不在[a,b]內
那麼Query(B1, p)*p 是多少? Query(B2, p) 又是多少?

Query(B1,p) =  (0+v2)  <--0是因為不受第一個Query影響
Query(B2,p) = -v1*b + v1*(a-1)   + v2*(c-1)

所以
Query(B1, p)*p - Query(B2,p) =   (0+v2)*p - (-v1*b + v1*(a-1)   + v2*(c-1))
= 0 - (-v1*b+v1*(a-1))    +    v2*p - v2*(c-1)
-->Sum[1..p] from Query 1 + Sum[1..p] from Query 2

其它Case也差不多可以這樣分解, 代表 n 個update 之後,  仍然可以用這個方法去Range Query的
道理上明白了, 最要緊是記得如何找出X, 自然就可以很簡單Code出來了

可惜BIT只能處理Add / Sum 之類的function...
下面是用這個變態技巧過 SPOJ Horrible的Code:
#include<bits/stdc++.h>
#define LL long long
using namespace std;

LL T, n, c, t,p,q, v;
LL bit1[100005], bit2[100005];

void add(LL* bit,int x, LL v){
    for(; x <= n; x+= x&(-x)) bit[x] += v;
}
void add(int x, int y, LL v){
    add(bit1,x, v); add(bit1,y+1, -v);
    add(bit2,x, v*(x-1)); add(bit2, y+1, -v*y);
}

LL query(LL *bit, int x){
    LL ret = 0;
    for(; x ; x-= x&(-x)) ret += bit[x];
    return ret;
}
LL query(int x){
    return query(bit1, x)*x - query(bit2, x);
}

LL query(int x, int y){
    return query(y) - query(x-1);
}

int main(){
    scanf("%I64d", &T);
    while(T--){
        memset(bit1,0,sizeof(bit1));
        memset(bit2,0,sizeof(bit2));
        scanf("%I64d%I64d", &n,&c);
        for(int i=0; i<c;i++){
            scanf("%I64d%I64d%I64d", &t, &p, &q);
            if(!t) scanf("%I64d", &v);
            
            if(!t) add(p,q,v);
            else printf("%I64d\n", query(p,q));
        }
    }
    return 0;   
}

Segment Tree Part

學完新技巧, 回歸原點, 我決定以Segment Tree再過這一題
(人家在Stack Overflow原文是求Segment Tree的答案嘛...)

才發現自己對Segment Tree的理解是如此低
當年開了頭學習了基本implementation, complexity等等的東東
但仍是不到位

這題 (Range Add, Range Query Sum) 我印象中算是很Standard了
也肯定在POJ Code過類似的題目, 但這時又忘了..
才更覺得要認真看待這個Data Structure, 問了GG後, 又再看了看一些Reference後
又有一些(好像)深一點的理解

我看的是這個Reference:
(裡面附送人家寫的Segment Tree STL, 但它的方式實在不合口味, 雖然是很強大, 還是學Concept算了...)

首先是這個神奇的, 似懂非懂的Term: Lazy Propagation
原來一直聽的這個詞, 是等價於另一些人可能會說的Terms, 例如: Split, Push Down
但其意思/ 意義是一樣的, 指向同一個"動作"

這個動作的原意是想避免每次Update / Query 都會去到葉子
經POJ某份PDF所說, 這樣好像可以去到O(N), 不懂Proof, 感覺也不對, 但總之就是很慢, 慢到會TLE的樣子 (for eg: Range Update [a,b], 如果每次都到葉子, implementation很簡單, 也肯定正確, 但就是太慢了, 要是[a,b] 每次都是 [1,n] 怎辦?)

所以就出現了這個方法: Lazy Propagation, 故名思意, 就是要用到 (Tranverse到那一個Segment Node) 時候才Update Node的Information

而Lazy Propagation 之外, 又有另一種常聽 / 見的字眼: Merge, Pull Up...
這又是Segment Tree內另一種動作, 跟Lazy Propagation相對的

究竟這2種動作是什麼來的, 如何運作, 何時使用?
有沒有一種Standard的Rule / Structure決定, 何時要用上這些動作呢...

按GG 和該Reference所說, 是有的
Segment Tree的 Update / Query 是有一定結構的, 在不斷試驗理解後, 應該是這樣的:

Range Update():
    if (Current Segment covers the range){
        update this node's info; return;
    }
    Split();
    Recur Left / Right Son Part;
    Merge();
}

Range Query():
    if (Current Segment covers the range){
        return this node's info
    }
    Split();
    Recur Left / Right Son Part;
    Merge();
}

當中Split() 跟Merge() 是把Info 推向下層兩個兒子, 和從下層兩個兒子算出current segment的Info用的
就是這樣了! 原來是很Standard / General 的結構, 但為何以前很多時候都可以沒有Split / Merge呢?

原因是這樣的, 視情況而定, Node 的 Info, Split 和Merge的Implementation 也是視題目而定的...這就是Segment Tree難搞的地方, 因為沒有萬能藥, 可以萬年通用的Rule / Implmentation

但是還是有多少事情可以深入理解下:
首先, 不難發現到, 會執行Split 的 Segment, 也會執行Merge
那麼該Segement的Info 已經 1. 推向下層 2. 最後會由下層算出正確的Info
所以在Split的時候 基本上可以 把Info Reset了

例如在Horrible中, 一個Segment Node的Info 是 Sum 跟 Add
代表該Segment的總和, 和Add在該Segment的 value
在Split的時候, 先把Add 加去下一層, 又把下一層的Sum 加上 Add的value
然後需要把Add reset做 0 !

這時, 該Segment的下面2個node 的Sum已經update了, 要是有query的range 剛好包括它們2個其中1個, 那麼就如上面所說的, 直接return sum 就可以了, 要是還要再向下一層, 那麼會在query時再Split下去, recur下去

然後所有曾經Split下去的Segment的Sum 又會在Merge的時候從下面2個兒子算出 (下面2個兒子的Sum肯定正確的, 因為Split到盡頭時的Sum一定正確, 然後沿路一直Merge上來)

把Add reset做 0 是因為可能下次會再Update 該Segment
然後又可能會再需要Split下去, 記住我們Split的時候是把當時該Segment的Add Split下去再算Sum的 (用+=) 所以如果不reset 做0,  會加多很多value去下一層...
(這些Node的Split / Update, Merge等等的東西, 我極度懷疑真的是每題題目都要度身訂做, 慢慢試一些test case去試出來)

簡單地說, 這種Split 跟Merge的結構, 在我們從根到某個node的path, 經過path上的node時才一直push down 和merge (要是需要的話), 沒經過的path (上的node) 就不管了

這樣當然速度會相當快, 也不會TLE

最後Base on上面對Split 跟Merge的理解, 我有以下的猜測:
要是Update和Query 有其中一個不是Range的話, Split 跟Merge可能不用Implement:

如果是Point Update, 可能不用Split了...但需要Merge
如果是Point Query, 可能不用Merge了, 但需要Split

80%肯定, 但沒有Proof

反正最大的得著就是: Segment Tree的Update跟Query有很好很Stable的結構
但Node design, Split()和Merge()的 Implementation就真的要看題目了...

最後是用Segment Tree (Lazy Propagation) 過SPOJ Horrible的Code:
可以特別留意Split(), Merge() 的Implementation

#include<bits/stdc++.h>
#define LL long long
using namespace std;

int T, n, c, p, q, v, t;

struct node
{
    int l,r,m;
    LL numleaves, add, sum;
    node(int l,int r, int m): l(l), r(r), m(m){
        add = sum = 0;
        numleaves = r-l+1;
    }
    node(){}
}tree[400010];

void build(int c, int u, int v){
    int m = (u+v)>>1;
    tree[c] = node(u,v, m);
    if(u == v) { tree[c].numleaves = 1; return;}
    build(2*c, u, m);
    build(2*c+1, m+1, v);
}

void push_down(int x){
    tree[2*x].add += tree[x].add;
    tree[2*x+1].add += tree[x].add;
    tree[2*x].sum += tree[x].add * tree[2*x].numleaves;
    tree[2*x+1].sum += tree[x].add * tree[2*x+1].numleaves;
    tree[x].add = 0;
}

void merge(int x){
    tree[x].sum = tree[2*x].sum + tree[2*x+1].sum;
}

void update(int c, int l, int r, int v){
    if(tree[c].l == l && tree[c].r == r){
        tree[c].add += v;
        tree[c].sum += v*tree[c].numleaves;
        return;
    }
    push_down(c);
    if(r <= tree[c].m) update(2*c, l,r, v);
    else if(l > tree[c].m) update(2*c+1, l,r, v);
    else{
        update(2*c, l, tree[c].m, v);
        update(2*c+1, tree[c].m+1, r, v);
    }
    merge(c);
}

LL query(int c, int l, int r){
    if(tree[c].l == l && tree[c].r == r){
        return tree[c].sum;
    }
    push_down(c);
    LL L,R; L = R = 0;
    if(r <= tree[c].m) L = query(2*c, l, r);
    else if(l > tree[c].m) R = query(2*c+1,l,r);
    else{
        L = query(2*c, l, tree[c].m);
        R = query(2*c+1, tree[c].m+1, r);
    }
    merge(c);
    return L+R;
}

int main(){
    scanf("%d", &T);
    while(T--){
        scanf("%d%d", &n, &c);
        build(1,1, n);
        for(int i=0; i<c;i++){
            scanf("%d%d%d", &t,&p,&q);

            if(!t) scanf("%d", &v);
            if(!t) update(1,p,q,v);
            else printf("%I64d\n", query(1,p,q));
        }

    }
    return 0;
}




2015年3月15日 星期日

Codeforces Round #291 (Div. 2)

這場可真是我近來最想最想記下的一場, 記完這一場後真的要寫說好的Trie跟String Matching的Algorithm了...但在此之前  這場5題都做了
而且學到更多以前聽過卻沒學過的實用技巧!

Codeforces Round #291 (Div. 2)


A: (頹題, Greedy)
又一題改digit 令數字變成什麼什麼的題目...我又再重覆上一場的句子: 通常都是Greedy就行了..

B: (Math, Geometry)
略有一點難度的數學題, 問最少要多少條線才能把所有目標都擊破
也不用特別的技巧或用上大家比賽常用的Geom的Class, 只要計Slope, 一樣Slope 的就Lie on 同一條線了...

C: (String, Hashing)
第一題要詳細記下的題目!
這題跟之後會寫的String Searching不無關係!
先來說題意吧:  Given n 條pattern, 和m 條 string (每一條是一個獨立query),  所有string 只會由[a-c] 組成
問是否有任意一個pattern跟 string 只差exactly 一個字母相異?

在學了Trie的基本之後的我, 直接想到的也是用Trie 去解, 事實上看comment也有人說能用
prefix tree (即是Trie)去解...但當時我沒很直接想到怎樣做

String的處理我一向都很弱 (說真的也沒什麼特別強=.=)
我只想到 每個query 可以試住做以下的事:
  1. 每個位試試改成其它的2個字母   eg: 原本是'a'的話試試改成'b'或者'c', 每個位都這樣做
  2. 跟每個pattern比較一下看是否一樣
這題的時間我也很難分析, 因為它是直接說"所有input 的總長度 L<= 6*10^5"  
所以這樣說吧, 上面的第一步最壞的情況已經是O(L), 第2步要跟每個pattern比較一下這步卻很痛苦, 這是multi-pattern matching, 用KMP好像也不一定夠快? (我也忘了KMP怎寫, 只記得concept...) 實際上我也真的這樣implement, 也真的超時了

結果還是跑去看Editorial, 又掘到金了! 兩大keywords:

Rolling Hash,  Rabin-Karp Algorithm
 前者是一個運用Polynomial Hashing的一個技巧, 後者是活用這個技巧的 Multi-Pattern Matching Algorithm!!
這個Wiki 的解釋也真的很好懂, 沒複雜的分析, 但很有說服力, 而且還說明了它跟其它String Matching算法的分別, 例如它說了 "Rabin-Karp始終是Hashing, 可以有很差的效率, Single-Pattern Matching上還是KMP佔優, 但它的強處是在Multi-Pattern Matching (其它的還有AC自動機)"

所以...基本上這題就是Rabin-Karp Algorithm的變種囉
詳細的自己再看上面的wiki-page吧

Rolling Hash 是一個技巧, 不一定要 "減頭加尾", 像這題, "換一個字母" 這類的事當然也做到
只要你是用Polynomial Hashing就行了

我想這題最難的是要說服自己時間複雜度
官方說是 O(L lg n)  我感到有點奇, 因為我自己AC的版本, 最低限度也是O(L lg n) 但這沒算上Hashing Collision後要做String Comparison 然後fail 這件事
理論上Hashing Function柒的話, 這件事可以發生很多次吧? 那就遠超於 O(L lg n)了, 因為要做很多次String Comparison, 這也是為什麼它用在Single-Pattern Matching時worst case可以跟naive algorithm一樣是O(NM)的原因...不過算了AC了就好了, 也可以這樣想吧: 一直以來所有人都是用Polynomial Hashing去比賽的, 這已經是定式了, 自然不會是一個"柒"的hash function...

以某黃字User的comment做一個結語:
Most hashes are certainly not unique or why do we use hash? If C is in ACM/ICPC, you can simply choose a prime other than 3 because the problem setter does not know which prime you used so he can not construct data against you. He can only guess some contestants will choose 3 and build some data to make them failed. However, in Codeforces, hackers can construct data against a solution. So you have to compare two strings themselves when the hash of them are the same. However the probability of two different strings have same hash is low so only a few strings will be compared. So, the hash helped you to evade TLE.

強烈建議看一下Code, 也是很有機的寫了一些comment
http://codeforces.com/contest/514/submission/10158032

D: (Binary Search, Two-Pointer Algorithm, Segment Tree)
這題有點難搞
原因在於 題意其實很直觀, 就是不斷query 一個range maximum
所以我直接就用Segment Tree上了! 結果也是AC的
(原本有想過能否用BIT可以更易寫出來, 但maximum之類的好像不能用BIT做...只能做統計的工作啊)
http://codeforces.com/contest/514/submission/10172455

正如我的comment上所說,  這是overkill啊,
官方的答案是用一種叫 "Two-Pointer Algorithm" 的方法, 那個方法應用在這條題目上很高明
但實作上我自己是想不出來了, 紅字User們說是用兩個Stack的樣子
總之正解是易寫也很有效率

我反而學到了兩件事: 是否overkill也不要緊, 但起碼concept對, 寫得出來, 就可以AC的, 也不用這麼介意...
然後再來Google了一下什麼是Two-Pointer Algorithm...這東東很多題目都有Tag, 從來都沒認真想過是什麼的一類Algorithm

看了高手們的解說, 就比較清楚了:
這類Algorithm的運作通常是這樣的:
一個pointer指住第一格, 一個pointer 指住最後一格, 然後開始夾三文治
做某些東西或者checking, 然後移動其中一支pointer (不知會否有2支一起動的情況?)
一個pointer只能向單一方向移動, 而每次最少一定要移動其中一支pointer
這樣的話 時間性一定是O(N)了
這類Algorithm好像很強大的樣子, 形象化來想有點像什麼Sliding Window還是 Sweeping Line的? 傻傻搞不清, 總之這種思路還是記一下好

E: (DP, Matrix Repeat Squaring)
另一題很想記下的題目!
因為這道題的重點在於當年聽過的Matrix Repeat Squaring應用
話說當年完全不知 "為何, 何時, 怎樣" 用這個技巧
還是CF的高手們好, 一個Reference就解得清清楚楚的說

先說一下題意:
一棵無限的Tree, 每個node 都一樣有同一set的兒子 (最多有10^5個兒子)
eg:  root 有 3個兒子, 它的每個兒子也一樣有3個兒子....直到無限深度
每個兒子跟它的parent距離 (edge的長度) 最多是100
現在問有多少個node距離root 是 x ( x < 10^9)

這題很直接就能寫下DP的狀態:




dp(i) 為 有多少個node 距離現在的node EXACTLY = i

然後答案就是





但難度在於 dp(x) 的 x 可以有 10^9 那麼大! 即使是 O(N)也超時 (也超MEMORY!)
怎麼辦呢??? 原來....原來....
原來這種情況就是要用Matrix Multiplication的Trick啊
在Editorial 挖金挖到一篇超上乘的tutorial 給我這種新手的
Matrix Exponentiation

But, what will you do if the problem says, given 0 < n < 1000000000, find f(n) % 999983 ? No doubt dynamic programming will fail!
這是它的前言, 說了最簡單的DP例子Fibonacci Sequence, 在 n 好X大的時候也是會吃屎的
而這就是 "Matrix Repeat Squaring" 出場的時候了!
這種加速的技巧只能用於符合某種"形式"的 Recursion Formula, 也就是DP Transition
| f(n) | | f(n+1) |
| f(n-1) | | f(n) |
M x | f(n-2) | = | f(n-1) |
| ...... | | ...... |
| f(n-k) | |f(n-k+1)|
直接引用它的解說, 其實就是先寫下 類似以上的算式, 以他的notation
第一格就是我們想要的答案 而其它的是計算答案時要用上的東東
然後我們逆算出 M 這個 n*n 的matrix, 之後的就簡單了, 把repeat squaring的技巧用在
matrix multiplication 上算出 M^x, 乘上我們的"base case" (最開頭的 n*1 vector)
就能得出我們想要的答案 (右邊的n*1 vector, 的第一格)

例如 Fibonacci Sequence, F(n) 要用上F(n-1) 和F(n-2) 對吧
右邊先寫上 B
| F(N) |
| F(N-1)|

左邊是比它"前一步"的States, 我們叫它A: | F(N-1)|
| F(N-2)|

然後要做的就是人手逆推 M, 使 MA = B
| ? ? | |F(N-1)| | F(N) |
| ? ? | * |F(N-2)| = | F(N-1)|

不難推出 M 是
| 1 1 |
| 1 0 |

那麼當我們要算 F(10^9) 的時候 只要算 M^(10^9) * A , 然後得出的B 的第一格就是我們的 F(10^9)了!

其它詳細的看那個優美的Reference吧, 都說得很清楚了, 通常M 都會很有Pattern 不需要hard code就能generate出來的
由於 是做 lg(x) 次 matrix multiplication, 假設matrix 是 N*N
那麼就是 O(N^3 * lg(x))
很好很強大的DP輔助技巧!

回到原題目上, 基本就是用這個想法, 當然作為Problem E也沒這麼直接
最不同的是我們要求的答案不是單一的 DP(X) , 而是 summation DP(i)
但也是大同小異的

首先注意到我們的A (也就Base Case的vector) 要用上全部DP(100) 個State
所以 X <= 100的時候, 直接用普通的DP就好了
當X > 100的話, 我們就要用上這個技巧了
注意先Update X = X-100, 因為現在我們的"Base"不是 0, 而是100
然後由於我們想要算的是 summation DP(i) 那麼乾脆把它放進我們的A (還有B) 內 加上本來的 DP(1) to DP(100) 總共是一個
101 * 1 的vector


官方的notation是 1* 101的, 思路一樣, 我想我以後還是會跟Reference那個notation

接下來的就是逆推 M了, 這一步是這一題的唯一難度 M 是什麼可以自己看Editorial:
(注意它跟我 / Reference的notation不同, 應該是Transpose了還是怎的)

就是這樣了! 
最後把AC的Code貼一下以作記念: comment應該也算清楚的 日後重溫應該也明白的吧...
http://codeforces.com/contest/514/submission/10218534