顯示具有 Kattis 標籤的文章。 顯示所有文章
顯示具有 Kattis 標籤的文章。 顯示所有文章

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{}