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年5月8日 星期五

T-414-ÁFLV Lecture 1 Homework 續: Kattis一題 + Operator Overload (Updated 2015-05-09)

(經GG提醒後有所Update, 全以紅字表示)
經上一篇由於要做功課的關係得知了Kattis這個OJ

經GG口中所說原來這個OJ水準也不錯, 題目很新很有趣

是由 KTH 的人研製出來的


有見及此, 我隨便找了一題difficulty 不錯的題目來試刀
上一篇所說的最難Bonus題目difficulty是4.4
這次選了一題difficulty 4.8的, 叫 Almost Union-Find

Kattis: Almost Union Find 

Almost Union Find (Data Structure Usage, STL, Disjoint Set)

題目: https://open.kattis.com/problems/almostunionfind

題目標題已經出現了很久沒接觸的字眼: Disjoint Set / Union-Find data structure!

題目給了 n 個不同的數字和 m 個query (n, m <= 10^5)

代表的是每個query 要在 O(lg n) 內完成了, 總時間最大 O(m lg n)

題目給了 3種不同的query, 有點像是Union-Find的:

  1. Union p 跟 q, 要是已經在同一Set 則無視
  2. 把 p 由現在的 Set 移動到包含了 q 的 Set, 要是已經在同一Set 則無視
  3. 報告包含 p 的 Set 現在的 Size 跟 Sum
入手立即從Union-Find能做到的事開始想...
發現不太記得怎樣Code, 順手 wiki 了一下, 借此重溫這個強大的東西:
  1. 這個Disjoint Forest的任何operation速度都depends on depth
  2. 存在2種方法可以盡力把depth 變小
    1. Union by Rank: 在Union()時, 永遠把depth少的tree 加到 depth 大的tree
    2. Path Compression: 在Find()時, 把所有經過的node的root 直接設定到最高的root
    3. 第一種方法已經令所有operation 變成O(lg N); 第二種方法更加使複雜度變成O(alpha(N)), alpha(N) 就是 inverse of Ackermann function, 直接就其實就是變成O(1)了,  2種加速手法複雜度證明我就沒深究了

分析這題題目的話, Disjoint Set本身已經能處理 第1種和第3種query, 但是第2種query : Move()

則有點難度了...原因在於要是用Disjoint Set的的結構處理這種operation

明顯要知道所有點的children, 而且在移動時要把它的children移動到它的parent / root...

這樣做法最差已經是O(N)了, 不可取

由於限制了要O(lg N)內完成這種operation, 我最能直接想到的就是STL中的Set

只要一個erase(), 一個insert() 就完了, 但是STL:Set 又不能好好的處理另外2種operations...


運氣又到了, 我忽發奇想, 要是我把Disjoint Set 跟 STL:Set一起用呢?

Simulate了一下小的Test Case好像可行的樣子:

圖像化來說, 我把資料分成三層: 
  1. 真正的data: 1,2,3,...n; 設一個array叫 curSet[] 記錄數字 i 現在所在的Set index
  2. STL: Set: 真正的Set, 真正的儲存了數字, 可能某些Set在某些時間是空的
  3. Disjoint Set: 一個array sp[], 第 i 格代表 第 i 個Set 的 root
簡單地說, 我把Disjoint Set apply在 STL:Set 之上
記錄的是那一個Set跟那一個Set已經Union了, 還有這些Set的 Size 跟Sum

而實際上的移動 (operation 2) 則在 STL:Set那一層進行, 同時用O(1) 可以Update到
curSet[]

沒有太多的證明, 過了sample case就交了, 一棍AC

現在可以試試說服自己為何這樣做OK

首先複雜度是肯定沒問題的: operation 1跟3 只是普通的Disjoint Set, 而operation 2 則是 STL:Set
所以最多每個operation也只是O(lg n)

正確性來說呢
先來看Operation 2 (跟Disjoint Set最不同的operation...)
由於過程是直接從Set中移動,  每次移動時同時Update curSet[], 也update 2個相關的Set 所屬的Disjoint Set 的Sum[] 跟Size[]....這樣就維護了所有以後operation所需的狀態正確性

再來看Operation 1, Union() 是把Set 的index 作一個關係, 
相對的Find(), 也可以從某個Set index 找到它所屬的 Disjoint Set的root, 
這樣Sum[] 跟Size[] 可以直接改動 root 那個Set的, curSet[]則不用改

例如 數字 1 是在 Set 3內面,  而Set 3現在跟Set 5 Union, 我們會改的是:
Set 3 的root,  Set 5 的Sum 跟Size,  數字1 仍然是在Set 3 內面不用改curSet[]

最後是Operation 3, 我們可以由curSet[] 知道某數字存在的Set index, 然後可以由Find()找出這個Set 所在的 Disjoint Set 的root (另一個Set的index) 
而由上面所改動的Sum[], Size[]也是在root 這個Set上維護的, 所以直接output root的Sum跟Size就可以了...

大約就是這樣吧...沒太嚴謹但還算是logically correct吧?
AC Code: https://open.kattis.com/submissions/717636
好吧昨晚太累了也沒想清楚, 其它人是看不到我的Submission的...
所以直接把code貼出來

#include<bits/stdc++.h>
#define LL long long
using namespace std;
 
int n,m,t,p,q;
LL sp[100010], size[100010], sum[100010], curSet[100010];
set<int> data[100010];
 
void init(){
    for(int i=0; i<100010;i++){
        sp[i] = i; size[i] = 1; sum[i] = i; curSet[i] = i;
        data[i].insert(i);
    }
}
 
int find(int x){ // pass curset of x
    return sp[x] == x? x : sp[x] = find(sp[x]);
}
 
void Union(int x, int y){
    int sx = find(curSet[x]), sy = find(curSet[y]);
    if(sx == sy) return;
    sp[sx] = sy; sum[sy] += sum[sx]; size[sy] += size[sx];
}
 
void move(int x, int y){
    int sx = curSet[x], sy = curSet[y];
    if(sx == sy) return;
    data[sx].erase(x); sum[find(sx)]-=x; size[find(sx)]--;
    data[sy].insert(x); sum[find(sy)]+=x; size[find(sy)]++;
    curSet[x] = sy;
}
 
int main(){
    while(scanf("%d%d", &n, &m) != EOF){
        init();
        for(int i=0; i<m;i++){
            scanf("%d%d", &t, &p);
            if(t != 3) scanf("%d", &q);
            if(t == 1) Union(p,q);
            else if(t == 2) move(p,q);
            else printf("%lld %lld\n", size[find(curSet[p])], sum[find(curSet[p])]);
        }
    }
    return 0;
}

而經GG的提醒下, 這題確實可以以O(alpha(N)), 純Disjoint Set去解的...
Operation 2 等於要delete 一個node, 我原本的想法是physically真的要delete...
導出上面我說的結論: 要移動它所有children...etc

但其實根本可以不用真的delete它...留它在原本的地方就好了...
反正我們也可以照樣update size[] 跟sum[]....另外再allocate 一個新的node 去新的root之下就好了....下面補上紅字User GG的 O(alpha(N)) 的Code
#include <cstdio>
#include <algorithm>
#include <cstring>
#define FOR(i,s,e) for (int i=(s); i<(e); i++)
#define FOE(i,s,e) for (int i=(s); i<=(e); i++)
#define FOD(i,s,e) for (int i=(s)-1; i>=(e); i--)
#define CLR(a,x) memset(a, x, sizeof(a))
#define EXP(i,l) for (int i=(l); i; i=qn[i])
#define LLD long long
#define N 200005
using namespace std;
 
int n, m, x, y, rx, ry, op;
int p[N], c[N], a[N];
LLD s[N];
 
int find(int x){
    if (x == p[x]) return x;
        return p[x] = find(p[x]);
}
 
int main(){
        while (scanf("%d%d", &n, &m) != EOF){
                FOR(i,0,n){
                        a[i+1] = i;
                        s[i] = i + 1;
                        c[i] = 1;
                        p[i] = i;
                }
                while (m--){
                        scanf("%d", &op);
                        if (op == 1){
                                scanf("%d%d", &x, &y);
                                if (find(a[x]) == find(a[y])) continue;
                                x = find(a[x]), y = find(a[y]);
                                s[y] += s[x];
                                c[y] += c[x];
                                p[x] = y;
                        }
                        if (op == 2){
                                scanf("%d%d", &x, &y);
                                rx = find(a[x]), ry = find(a[y]);
                                if (rx == ry) continue;
                                c[rx]--, s[rx] -= x;
                                c[ry]++, s[ry] += x;
                                p[n] = ry;
                                a[x] = n++;
                        }
                        if (op == 3){
                                scanf("%d", &x);
                                x = find(a[x]);
                                printf("%d %lld\n", c[x], s[x]);
                        }
                }
        }
        return 0;
}
運行速度比我的做法快了一倍, 由0.28s --> 0.14s, 也簡單易code也簡單易明 (correctness self-explained) 


另外由於這篇不想太短, 我也歸納了上一篇最後帶出的一個問題:
C++ Struct的 operator是怎樣寫overload的呢? 上一篇我說是不知道
巧合之下我發現了Stack Overflow的一個 Post, 解釋得還算很清楚:




Finally, in answering your question, what is the difference between a member function or operator declared as const and one that is not?

const member declares that invoking that member will not modifying the underlying object (mutable declarations not withstanding). Only const member functions can be invoked again const objects references and pointers. For example, your operator +() does not modify your local object and thus should be declared as const. Your operator =() clearly modifies the local object, and therefore the operator should not be const.
所以說:重點是在於該operator的implementation內會否改動到你object內的東東
決定了要不要 bool operator() const {} 中的const

至於parameter的 const是速度的考慮:
All of your input parameters to all of your members are currently making copies of whatever is being passed at invoke. While it may be trivial for code like this, it can be very expensive for larger object types. An exampleis given here:

所以合起來就是:


// assignment operator modifies object, therefore non-const
    pos& operator=(const pos& a)
    {
        x=a.x;
        y=a.y;
        return *this;
    }

    // addop. doesn't modify object. therefore const.
    pos operator+(const pos& a) const
    {
        return pos(a.x+x, a.y+y);
    }
這樣理解還算很易記, 也解開了心中一個疑問...


PS: 今天還看了之前Codeforces #300 的Problem G Editorial...我認為要進步就要盡力看跟學..即使這題的AC人數還是只有雙位數....肯好還算明白, 等有時間Implement完就再Edit那一篇補充好了...至於之前的GCJ Round 1B, 我也做了B-Large, 看了Solution後, 明白了B跟C...C還沒做, 至於A是看了也不明白, 好像有點事情它沒有Proof的樣子...只能說B-Large其實絕對是可以做得出來的...也是一樣等做得夠完整才一篇長篇說吧...



PS2: Segment Tree再深究了一下, 做了一題SPOJ的題目, 那一題可以下篇長篇說一下, 同時學習了:
  1. 如何用BIT 做 Range Update (Add)
  2. Segment Tree in general的結構, 還有關於push down (split / lazy propagation) 跟merge的一點理解
還是同一句 真的忙死了...
再多一句: 希望GCJ 1C順順利利 (反正只能打一個小時多一點就要出門了=.=)

2015年5月6日 星期三

T-414-ÁFLV Lecture 1 Homework

再來一篇短篇, 今天在公司趁有空, 把上一篇介紹的那個Course 的第一課所有功課都完成了

這些功課都是在一個叫 Kattis 的Online Judge上提交的, 這個OJ還算可以吧, 算漂亮的

由於是第一課, 都是一些比較基本的題目...

有好些題還有印象曾做過了

曾經會為了做到這些題目高興呢

但現在看來也只是Div. 2 A-C 的難度吧....

這個Course 每一課都有一些相關題目, 這第一課 "Introduction" 的功課大多是Ad-hoc

除了 Bonus 那2題還算有趣 , Bonus 那2題其中有一題太麻煩了所以沒做 就做了另一題

(這個Course每一課都有Bonus的高難度題目)

Ad-hoc的Bonus題, 難度大多在於I/O Format...超麻煩的

我選了一題非Bonus的題目, 和一題Bonus的題目來記下

下一篇文章會返回現在的程度的東西...這篇就當是熱身 / 遊戲的題目吧...

絕對不能因為會做這些沒什麼思路的題目而開心呢

T-414-ÁFLV Lecture 1 Homework

普通題目: Chess (Ad-hoc, STL)

https://open.kattis.com/problems/chess

很肯定在比賽生涯很早期 (早到只懂scanf printf 那些) 有做過
現在覺得沒什麼好想的 倒是怎樣code 有聰明有笨的方法吧...

思路是查parity是否能到 (題目連parity都告訴你了...)
能去到的話, 最多只要2步, 證明很簡單, 任何 2個格子, 打個交叉
不難想像這2個交叉肯定會在某一格相遇,  所以最多可以從起點到那一格再到終點

實作上來也是直接這樣做了...code起上來才發現這樣code也沒多短....
先用2個SET 儲起2個交叉的點  再查一查那一格是相遇的一格
特別處理0步, 1步能到的情況

好像沒多聰明, 應該怎樣都比當時的我要好吧...能過就好了...

Bonus題目: Booking (Ad-hoc, Graph, Greedy)

https://open.kattis.com/problems/booking

這題絕對有資格做Bonus題目...
我是第一次做的, 除了Input Format麻煩之外...
背後的思路 對於一個初學者來說也是麻煩的...

肯好我正重溫CSC2110不久
這題正是赤裸裸的 Interval Graph (Minimum) Coloring!

正好這題也清了我一些很久以前的concept 特意記下
(連同上一篇SPOJ 那一題 我曾說不知是Sort by 什麼...也是有關係的)

這兒先說一下
首先Graph Colouring 的問題,  就是要找Minimum Number to Color a Graph (Chromatic Number)

In general, 這問題是 NP-Complete的!!

但還是有些特例, 有些 Positive Result 很好利用的


例如某些graph 的 chromatic number X(G)是肯定的:

    1. X(Tree) = X(Bipartite Graph) = 2
    2. X(K-complete graph) = K
    3. Simple cycle = 2 if even,  3 if odd
    4. Wheel = 3 if even, 4 if odd
當中第 2 個property 會導出另一個結果:
任何Graph 的 X(G) >= K where K is maximum complete sub-graph size
這個很直觀就不特別解釋了...



然後對於一種很特別的Graph:  Interval Graph (有頭有尾的segment作為vertex , overlap / conflict的話則有edge), Chromatic Number是有P解的!

這種Graph 有一個有趣的property, X(G) = K where K is maximum complete sub-graph size
是Strict 的 Bound

證明是用Maximum Degree Ordering的結果, 再證明任何 Interval Graph 如果存在K complete sub-graph的話必定可以把點以這個Order 排序, results follow (詳情看2110 notes, 思路很有用)

由證明的過程直致導出P的解法: (Greedy的想法)

  1. 把 Interval 按 Ending Time 排序
  2. 從最後的Interval開始, 看之後有多少Interval 的Start Time < 該Interval的End Time
    這個數字就是一個 K complete subgraph 的K
  3. Loop 所有Interval 找出第2步的 K, 取最大值 就是 X(G)了
1棍AC, 複雜度是O(n^2)  直覺上絕對有更快的做法, 但這題 n 是5000, 就沒多想了...

再來我做了個實驗的說, Sort by Start time 再做可以嗎?
算法也差不多, 也是取某 Interval, 然後看所有之前 (之後) 的 Interval 有沒有conflict, 求K
果斷WA!!  想了良久終於發現重點了....也解決了心中良久的疑問 


假設這是sort by start time後的排序...在處理第 i 個interval時
i 之後有多少interval 跟 i conflict, 這兒是 3
但明顯 實際答案是 2...問題在那?

答案是: 當第 j 個interval 跟第 i 個interval 有conflict, (j > i) 不代表所有其中的interval 可以組成complete sub-graph!!!

以上面的例子, i+1跟i+2 沒有conflict,  i, i+1, i+2 根本組不成 3-complete sub-graph

上面的P解法源至那個Proof, 為什麼上面AC了的做法可以?
因為那個做法確保了 所有跟第 i 個interval 有conflict 的 interval 肯定是complete sub-graph

證明是: 我們已經sort by end time, 所以所有 j 的interval 肯定比 i interval 遲完結

有conflict 代表第 j 個interval 的start time 比 i interval的end time 早...

這樣已經肯定所有跟第 i 個interval 有conflict 的 intervals 能組成complete graph了...

因為在第 i 個interval 的end time 打直劃一筆, 肯定能穿過所有跟它有conflict的intervals..!


理解完這些後, 我就想了, 所以重點在於 判定 i-th 跟 j-th interval有conflict時, 要肯定所有這樣的 j-intervals 跟 i-th interval 可以組成 complete graph吧...?

那麼sort by start time, 由第一個interval 開始loop 也是可以的吧?

只要我看的是第 i 個interval的start time, 要是 j (j < i)  的end time 比 i-th interval 的start time
後的話, 

這些intervals 肯定能各自相互在 i-th interval的start time conflict, 組成complete graph!

想完後立刻做做看...結論是....AC!!

所以我立即想通了...sort by start time / end time其實不是重點, Loop的方向也不是重點, 重點是sort 完之後, 要確保一個property: 就是在判定conflict時, 要肯定所有跟 第 i 個interval 有conflict的 interval 能組成complete sub-graph!

這題就是這樣了...很好的重溫

實作方面也很不錯 (麻煩), 由於是Date Time的format, 用C++很不好處理
肯好前天理解Segment Tree時已經有在寫Struct, 這題也很直接地用2個Struct 加一些operation overload 解決

這兒是第一種解決方法的Code (Sort by End Time)
實際上由於還要把那個datetime 加minute, 有潤年(題目有提醒...算很好了)
所以有點長

順帶重溫了Struct中怎樣overload operator...很多時都有用呢 (尢其要用有comp的STL時)
為什麼要const, 要 &, 我不知道

bool operator<(const datetime& x)const{}
bool operator==(const datetime &x)const{}

2015年5月3日 星期日

New Platform & New Tutorial Material!! SPOJ + T-414-ÁFLV + 短期方向

這篇之後就剩下Google Code Jam 1B的記錄了..那真是慘痛的經驗...經一事長一智吧...
下篇再詳說了

這篇短篇只是把一些散亂的東東集中起來記一下~

SPOJ

首先是一個新發現的Programming Contest 平台~
很漂亮很boostrap的, 我真的覺得很不錯, 叫 Sphere Online Judge (SPOJ)
平台漂亮, 提交容易, 題目也很新很多
不過還是近於POJ 那種題目庫向的, 不太多contest向
contest還是CF / TC 那面較好吧~ :)
這個平台最強大的地方, 是有一個(不知道是否官方) 的 sample case tester
可以自己打input 看看官方答案的output是什麼這樣子
然後還有一個random input generator, 可以按要求random給出數據!
這2樣東西加起來要找counter example / test case就易很多了...
我也是用這兩件事才能AC下面所說的一題...

得知這個平台是從在公司用Ideone.com 打C++時右邊一些廣告發現的,
開頭以為是一些"野平台", 沒太大注意

後來偶然下, 為了解決一條在Stack Overflow的題目 (那題目就是問SPOJ的一條題目)
我就跑去玩了~ 話說在Stack Overflow答題目真的是一個學習/試試自己的好方法
快點有1000分就好了

What is wrong with this solution while solving JUICE on SPOJ?

在我威迫利誘之下, 他還是要Accept我的答案...哈哈! (誰叫他想要看AC的Code, 就要付出啊...)

話說這一題也很有學習的價值, 是一條 O(N^2) 的DP...
題目跟答案直接去看Stack Overflow 的Post就可以了

我再次覺得DP真的很彈性, 而且只要concept正確的話, 在題目的context 下
可以special handle很多東西 (甚至需要你這樣做) 才能AC...

這題來說就是要把同一個Starting Time 卻不是最後的segment 的state強制設定到負無限
其實只要有方法不使用它們update 更大的state就好了, 這只是其中一個方法
要找出這個問題, 我就是利用了 SPOJ Toolkit 這個強大的工具...這也令我更喜歡這個平台

然後針對這類interval graph的題目...其實我也很混亂的說
sort by start time還是end time, 還是sort by segment length...我印象中好像每一題都不同...
看來沒有什麼一定的rule 吧?

還有就是由於之前 (in progress)在重溫 CSC2110的notes
記得interval graph是 graph coloring problem 的其中一種特例, 是有解的
開頭以為是問那個方法, 還跑去重溫了...結果卻不是那回事
但Interval Graph還是一種很特別的東東
立志的notes也提出了一種叫 "Maximum Degree Ordering" 的theory
說了一種greedy找 minimum coloring on interval graph的方法
這種思路是絕對要學習的 (由於是特例, 直接記下也可以, 其實我還是更偏向這種學習方法)

所以結語是:  SPOJ, 好平台, 不用嗎?


T-414-ÁFLV Programming Contest Course

剛剛在某CF comment中看到有人介紹的一個好物...
不知道是什麼人寫的Course, 裡面還很正式的有評分, 功課等等
很好很強大.. 

暫時看了一下outline 和introduction, 好像很可靠的樣子
盡快學習一下好了~

這時也想了一下放在心中很久的疑慮:
其實material真的不少, 要多少有多少
演算法筆記, 這個programming course, TC 上的Tutorial....等等
問題是各家有各家的心法, 大家不論code還是學習的方法也很不同
即使是同一個技巧 / algorithm也差很遠

以前的我是想找"最好"的一個去學習的, 但慢慢發現根本沒有"最好"這回事
只有"最適合"自己的一套, 例如我很快就會說的 (我記得的...) KMP
那個複雜度的解釋,演算法筆記那個解釋看了明白卻不太能記下
Google隨便找了另一份notes的解釋是另一個合理的思路, 明白易記

所以其實根本不用強求, 也不用硬要學習那一份material
反正就都看看, 不明白就再找同一個topic的其它material, 找到自己最能明白理解的就好
Code的方面則最好在明白了之後有自己一套習慣的寫法
這是現在自己最習慣易學習的模式了...怎麼以前都不懂這些道理



最後的最後, 由於近來真的忙死了...還是一樣一樣整理下事件的先後次序吧...短期的計劃是這樣吧:

  1. 先集中準備下星期的Google Code Jam 1C...誰叫我1B差幾名才能Advance...
  2. 1C完了之後, 把1B, 1C的題目能學能做的都先搞定
  3. 繼續重溫CSC2110的Notes, 還差最後幾課了不能放棄
  4. 學習+記下說了很久的String Searching Algorithms + Trie!
  5. 開始跟住 T-414-ÁFLV 的課程學習 (重中之重)
中間的其它比賽能不打的都先不打了, 不然學習不了也沒好處
把這些都做得七七八八再繼續看演算法筆記尋尋寶吧

還有之前一直玩的Maya 2015也要找時間繼續...真的忙死了 = =

Codeforces Round #300


先來兩張圖:















久別的參賽...這是一場 Div 2 + Div 1一起玩的比賽....
誘因是前300名有機會有T-Shirt...才厚臉皮參加的

運氣使然下, 結果比想像中好太多了...
雖然只做了最簡單, Div 2水平的4題 (共8題題目)
Room中竟然也排第6 
Rating也+158..差一點就變黃色User了=.=

興奮開心過後, 還是要面對一些問題:
首先頭4題一棍AC是高興的, 但除了D之外其實是很簡單的Div 2題目
D也不算難..但我認為之前的我未必能做到就是了...

而E 跟 F 才是真正的考驗, AC人數很合理的Div 1水平...
我認為要在比賽中能AC 這2題 其中1題才能算是Div 1吧...
這2題賽後幾經辛苦下也AC了...雖然還有些疑問
這場的Editorial 這2題寫得很亂..基本還是要自己想, 外加Peter神秒殺後也給了一點hints之類
過了之後在Editorial也試著把自己的想法留了comment, 題 E也有幾個正評, 太好了

至於G跟H 果斷放棄, 賽中不過100人能提交...賽後看Editorial也看不懂
暫時先放棄

結論: 先集中記下 題A-F好了

Codeforces Round #300



A: (Ad-hoc, String)

很基本的String題目, 就是問把一個substring移除後, 可以把string 變成 "CODEFORCES"嗎?

B: (Greedy)

這題可以秒殺是因為跟我曾經答過的Stack Overflow題目很像...
題目給了一個數字(n <= 10^6), 問把它拆成sum of Quasi Binary, 最少需要拆多少個?
Quasi Binary的定義是數字只由0或1組成: 10001, 10101, 10000...etc

先看Stack Overflow的題目: (shole是我在SO的名字)
Logic: Applying gravity to a vector


I think it is as simple as counting the total '1' bit of each position...
我這句也完全能apply在這題呢! 基本上就是每個digit 要不是0的話就是 >= 1, 還可以 (需要) 拆多一個Quasi Binary而在該位置也是1...然後原數字的該digit 就減1, 如此類推直到原數字變成0就好了

這題好像有人用很強的DP做...我是完全不知道怎樣用DP做的

C: (Math, Greedy)

題目給了n pair 數字 <a_i, b_i>
a_i 代表日子, b_i 代表高度
每日與隔日(前後一日)的高度最多能相差1
在給定了的n pair 數字和要符合這個條件下
最高的高度可以是多少? 要是本身 n pair 的input 不合理則說"IMPOSSIBLE"

n pair數字把日子分隔了n+1段
每段各自求最高的高度就可以了...
由於要符合條件, 最高的高度是能直接算出的
在紙上算了算...我好像寫了一條formula 出來
直接把 第 x日跟第y日之間的最大高度算出 (第x日的高度與第y日的高度沒有限制, 可以是h_x > h_y 或 h_x <= h_y)
   LL k = (h[i+1] + d[i+1] + d[i] - h[i])/2;
h 跟 d 是高度跟 日子

每一段都算一下取maximum的高度, 頭尾兩段特別處理就好了
O(n)的Greedy算法完成

算高度那一段有人用binary search做的 (好似係)

D: (Brute Force)

這題是賽中最後做到的一題了...話說本來連這題也苦無對策的
在AC之後才覺得這題其實很直接啊...證明以前水平太低了-.-

題目給了一個棋盤 棋盤上有棋數隻 (同一種類的)
然後mark了 那些位置是能被某些棋"吃掉"的
棋子自己的位置 可以是被"吃掉" 可以沒被"吃掉"

然後問: 這類棋子的移動方式怎樣? (可吃掉的位置)
要是有多種答案隨便輸出即可
(詳細要看輸入輸出例子, 自己按link看吧)

這類題目....除了暴試我也沒其它想法了
題目數據最大也是O(n^4) , 絕對是叫你暴試的

但開頭的想法怎樣也要O(n^5), 所以也想放棄了...
然後逆轉思維的時間到了..其實是在看例題輸出的時候想到的

題目input是 n*n 的棋盤 (n <= 50)
而output 是 2*n-1 * 2*n-1的棋盤...而棋子是在這兒的中間

如果我不是 "for 每隻在input的棋, for 每一格看看是不是由這隻棋吃掉.. for..." 這樣傻的想法
而是從output來想, "output的每一格, 與正中央(棋子)的 delta x, delta y 是已知的, for 每一格 O(100^2) , 看看input 的每一隻棋 O(50^2) 的delta x, delta y 位置是不是能吃掉 (或者另一隻棋或者out of bound)"

這樣的話是 O(2*2*50^4) = O(n^4)! 很像樣了, 正確性我認為也是self-explain! 也很好code!
太好了....回頭再想為什麼這樣暴試會快一點, 是因為output 限制了一隻棋能吃掉的range...
這樣其實不用每隻棋把全範圍試了...

Editorial好像不是這樣試的, 但comment中有一個很多正評的做法是這樣
Editorial也有一個挑戰: 用O(n^2 lg n) 解決同一問題...很變態的挑戰=.=


E: (DP on Tree, Graph)

終於到這一題了...賽後檢討的一題
很難很難, 我認為絕對是 Div 1的題目
而且下次再出現這樣的題目我也應該不懂=.=

題目很直接是DP on Tree的了, 由於每個state只會做一次, 基本上也可以用DFS代替

由於我花了一段篇幅在Editorial的comment寫了這題的思路, 就不詳細說了
http://codeforces.com/blog/entry/17612#comment-225396


這題印象最深的是...DP min 跟DP max 這2個state的轉移方式可以完全不同...
以前直覺上是相差不遠的
而這題完全經過一些很變態的分析之後, 發現一個的轉移是 summation(), 另一個是maximum()
太變態了...
把DP的答案強硬地控制在 [1, # of leaf under this node] 這個狀態定義也很變態

終歸一句: 這題學習到的是 DP的彈性真的很大...不聰明地分析根本不可能做到的吧...

F: (Data Structure Usage, BIT, Segment Tree, Math)

另一題變態的題目...
這題也有點故事的...話說有2個approach
一個是O(n lg^2 n)的, 另一個是 O(n sqrt(n))
我本身的想法是第一個approach which is 正確的想法

但那一個approach 以我的想法來說
是要用某些data structure(我認為BIT/ Segment Tree都可以) 在O(lg n) 內query 到 range 內的inversion總數, Editorial也是這樣說的, 但沒詳細解說怎樣做, 我在comment問了一下吃白果了
為此我還在Stack Overflow問了, 但是沒答案, 有人知道的煩請答一下

由於Peter神用approach 2 已經秒殺了這題, 我就認輸了
看了一下某些紅字User的code, 發現90%人都是用approach 2的
而另外10%人, 用了一種叫"Wavelet Matrix"的template, 我認為就是approach 1需要用到的data structure吧....話說也看到有人真的用很簡單的Segment Tree過了...沒深究下去了

果斷用approach 2試試做一下 (比賽中90%用的方法, 理論上應該要學習下思路的)
approach 2 是有 sqrt(n) 在內的...

關於這一點, 我早有感受: 我對 SQRT(N)相關的複雜度完全不敏感!
什麼RMQ, LCA好像也有SQRT(N)的存在...所以其實SQRT(N)也不是冷門的
也是要學習開始敏感了

暫時我的感覺是, 如果有關於 FACTOR / 除數 之類的題目, SQRT(N) 也可能有關係
(想想Prime Sieve的原理)

我comment中的2個case 是由於那個除數直接導出的結果, 令到總segment的數量只有
2*SQRT(N) = O(SQRT(N))
感覺上這樣分2個case歸納出SQRT(N)的手段可能很common, 記下比較好 (我是第一次見)

另外這題也是很有得著的!

首先就是一個可能頗常用的技巧
"Delta Encoding + Partial Sum"!! O(N)
Delta_encoding是第一次聽, 但做法不是第一次用了...這次更"學術"地看看這是什麼東東

簡單地說, 他是一個array, array中的每一格記下與上一格的difference
然後把這array 做partial sum, partial sum後每一格是原來的data array

例如 現在range [1,10] = {0}, 我做了幾個operations
1. [3,5] +1
2. [7,8] +1
3. [4,6] - 1
問現在range[1,10]的value?
這樣每個operation可以用Delta Encoding的手法以O(1) update一下:
[3,5]+1 --->  range[3]++; range[6]--;
[7,8]+1 --->  range[7]++; range[9]--;
[4,6]-1  ---> range[4]--; range[7]++;

現在range的array是 {0,0,1,-1,0,-1,2,0,-1,0}
然後以O(N)在這上面做 partial sum, 得出
{0,0,1,0,0,-1,1,1,0,0}
完全正確!

這樣代表什麼? 是代表其實可以用這方法取代BIT / Segment Tree嗎?
不!!
這個方法只能處理Static的operation, 不能online dynamice地update segment和query segment
是有點相像但卻完全不同的處理手法...

另外這個方法感覺上好像一定要配合partial sum才有用武之地?
所以"Delta Encoding + Partial Sum"是一個以O(N) Offline 處理range update operations的手法


另外學到的是 Floor() / Ceil() Properties!
這題用approach 2 的核心思路, 是在於由一個Floor() / Ceil() 的Property推算出一個連續的 k  range
這個推算如果不懂 Floor() / Ceil() Properties (如我一樣) 是很難的...
幸好賽後找到了一個不錯的 Reference! 


除了在這次Editorial 推算中出現的Property外, 還出現了很多年前忘了是Chin爺說的還是說的
"沒有precision lost的ceil 方法"! 就是:mn=mn+m1


在coding中, 直接 (n+m-1)/m 就做到 ceil (n/m) 的效果了
Reference也很好的解釋了如何得出這條式的...!

所以這題就這樣了...
學習到的是
  1. 對SQRT(N)要更敏感一點
  2. Delta Encoding + Partial Sum的技巧
  3. Floor() / Ceil() 的Properties
話說我對Approach 1 (就是我原先的想法) 還是念念不忘, 要是有神人能用簡單的 BIT / Segment 做到 而不是用什麼 "Wavelet Matrix" 的話, 煩請告知一下!

PS: 希望下場CF 也是正分, 可以上黃色吧...