顯示具有 Data Structure Usage 標籤的文章。 顯示所有文章
顯示具有 Data Structure Usage 標籤的文章。 顯示所有文章

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月3日 星期日

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 也是正分, 可以上黃色吧...

2015年4月15日 星期三

Codeforces Round #293 (Div. 2)

一場只有Div. 2 的比賽
題目有6題, A-E基本上都應該能做得出來吧 , D跟E要小心一點...
其實A-E已經做完很多日, 只有F一直沒做所以就沒記下...
終於昨晚也過了F

今天也順路經過了 Google Code Jam 2014 Round 1A, 還有很順路地revisit了 hamming problem一次, 我也知道很雜食, 證明在公司開始忙了, 不能很有系統地學/看我計劃好的東西
.這2件事我看看如果歸納以後再分開寫一篇吧...(還有說好的String Searching沒寫...)

Codeforces Round #293 (Div. 2)


A: (Greedy)

題目給了2條string s跟t,  長度都 <= 100, 求任意一條 比s 大又比t 小的string

就Greedy地construct就好了...

B: (Ad-hoc, Greedy)

也是給了2條string s跟t, 然後如果s的某一個字母跟t 的完全一樣, 那麼把counter1 加1, 要是他們只有大小楷不同則把counter2 加1, 求最大的counter1, tie break是求最大的counter2

有點煩膠的一題, 但也只是Implementation的問題, 沒什麼特別的算法~ 
就是Greedy地先把counter1 加到最大, 然後對counter2同樣處理~

C: (Ad-hoc,  Data Structure Usage)

我認為是最煩膠的一題, 也很難分類
題目本身是很簡單的...我想主要是靠他的煩膠度使玩家們WA吧

由於題目太煩膠了, 我就不詳寫了
但Solution是 利用 2個array,  array A 儲存的是 array B 的 index, array B相對的也儲存了array A相對應的index, 然後在For loop 中每次用O(1) manipulate 這2個array 的其中一個element
所以總共是O(N)

D: (Probability, DP)

開始有難度的一題, 主要是因為出現了我很怕的probability.
題目本身倒是很簡單的:
現在有一條queue有n 個人, 每一秒第一個人有p 的機會離開隊伍(不再回來), 有1-p的機會不動
求 t 秒後已離開人數的expected value

E(X) =  p(0)*0 + p(1)*1 + .... + p(n)*n,  p(i) 是在 t 秒後有 i 個人已經離開的機會率
這個(些)機會率我們可以先用DP求出, 然後就能計到E(X)

設DP(i, x) 為第 i 秒, 有x 個人已經離開的機會率
那麼DP(i ,x ) = DP(i-1, x) * C +  DP(i-1, x-1)*p ,  C = { 1  if  x >= n , 1-p otherwise}
注意上面粗體的部分, 是當 x > 0的時候才有的, 不然不用加
(physical meaning是: 如果一個人都還沒有離開, 根本就沒有這個transition)

就這樣 O(N^2) 算完機率 就基本做完了

E: (Ad-hoc, Greedy)

可以說是這場最難想的一題, 也很難code,  總結是比F更難, F是難code而已
題目說給了一個SEQUENCE 和一個長度L
然後這個sequence 每個連續長L的subsequence sum會形式strictly increasing sequence
例子: {1,2,3,4,5,} L=3,   那麼1+2+3 = 6,  2+3+4  = 9, 3+4+5 = 12 --> {6,9,12}是strictly increasing

現在這個SEQUENCE有某些位置(可以無) 是 ?  
求一個填法such that SEQUENCE符合上面所說的條件
如果有數個Solution, 則取 sum( absolute value of a_i) 最小的 <--這句是令題目變很難的東東


這條真的是很麻煩很麻煩, 一步一步來看
首先連續長度L的sum, 其實有很多overlap的位置對吧, 那些位置反正都一樣
eg:  {1,2,3} 跟 {2,3,4}  那麼這些位置根本是什麼都沒關係 反正他們的sum一樣
所以其實連續L長度的Sum是increasing, 等於每相隔 L 的數字形成的sequence本身就要strictly increasing

那麼我們可以把原本的SEQUENCE拆成 L 條List,  它們形成原Sequence的Partition
由於這L 條List 是disjoint, 各自擊破就好了

集中看一條List吧:  {-2,?,5,7,?,?,10,?} 可能是這個樣子  我們的問題變成要把 ? (如果有的話)填上數字, 使這條List 
  1. Strictly Increasing
  2. Sum of absolute a_i  最小
當然可以的話當然很想全都填上0吧...
理想跟現實, 而且由於有負數的出現令問題更難

需要多一些觀察:
首先每堆連續的 ? 其實也各自不相關, 他們的value已經被前後非 ? 的value bound住了
所以也是可以分開分析
會發現有3個Cases
  1. 前後都是non-negative (>= 0)
  2. 前後都是non-positive (<= 0)
  3. 前面negative, 後面positive
Case 1跟2是對稱的也很易明白: 如果是正數的話 我們就直接由前面+1, +1的填起就好了
因為這樣的absolute sum是最小的, 也肯定是strictly increasing; 如果負數的話則由後面-1,-1的填起, 總之我們想盡量填靠近 0 的數字

Case 3就很麻煩了, 不難發現Case 3 肯定有一個位置是0了
然後就很直觀地把0 放在區間的中間, 前後 +1 -1 的填就好了對嗎

錯!! 因為前後非? value的問題, 這樣填可能會變成no solution, 但其實是有solution的, 只是 0 不能放中間

這步令我很想哭, 寫得很辛苦也很難看, 但也想不到什麼更方便的方法
就是計算由左面 (負數那面) 開始算起, 最遠可以放0的位置, 對稱的, 也計算由右面(正數那面)最遠可以放0的位置,  要是有solution的話他們一定會overlap, 放在overlap同時最近區間中間的位置就好了, 然後就是左右開始填起 (一面+1 一面-1的)

用文字說就已經很煩了, 寫起上來更是辛酸...老實說如果真的在比賽, 很有可能根本放棄這題了, 又或者根本不能很肯定地在時間內提交...


F: (Combinatorics, Binary Indexed Tree, Data Structure Usage)

最後最後的一題了, 老實說最後能過這題還是很有成功感的
其中一個原因是自前陣子再了解BIT的用法和寫法後 
在這題可以用上並且AC了 !

其實這題所有難度在於Coding...算法(如果是我想的那個寫法)
是很易很易想到的

先來說說題目
給了一個2D grid (1000*1000), 當然也是有些格子不能走的,  現在要開始畫一條線

           ....#            ....#            .*..#
           *****            ****.            .***.
           ..#..            ..#*.            ..#*.
           #...#            #..*#            #..*#
           .....            ...*.            ...*.

借用了題目的例子: 上面 . 是空格, # 是不能走的, * 是你要畫的線

這條線有些條件:
  1. 一定要在邊開始和完結, 不能在角
  2. 只有頭尾兩點可以在邊上, 其它都要在更中間的位置
  3. 邊上不能有連續2點或以上的線
  4. 線可以90度轉, 最多轉2次
  5. 線,當然是連續的
就這樣吧 (題目還說了很多, 歸納起上來也只有這些)

題目很簡單, 給了一個grid, 問有多少種不同合法的線?

我再強調算法很簡單的, 就是直接做=.=

首先把答案分成 3 種去數會更易數: 沒有轉向(直線), 轉了一次向(L型), 轉了2次向(U型或閃電型)

基本上前兩種是很直接的吧?
我們用Partial Sum precompute了從4條邊各自可以最長向中間伸展的長度
例如partialLR[i] 就是 第 i row,  由左至右, 可以最遠的距離
同理partialRL[i], partialTB[i], partialBT[i]也是這樣

那麼直線的就直接O(N^2)數好了,  L型的就是O(N^2), 每一row 看看vertial 那個partial sum能否達到這個row 這樣

當然, 最難的一定是轉向2次的了
但還是很直觀的: 我們再precompute另一些array, 我叫他們做lengthLR[i][j] (還有另外3個)
代表 (i,j) 向某個方向最遠的長度 (其實可以用上面的partial sum array同時間做這件事)

eg:  .....#...#   -->  12345#123#  左面的數字就是lengthLR[i][j] 在該格的value, 代表由左至右, (i,j)距離上一格牆的距離

好了 考慮以下U型的情況(閃電型同理, 還有另外3個方向也同理=.=)

........
...#...
......#.
.....#..
.........    
.....#..

我知道很不明顯, 但上面有一點紅色的, 假設現在我在這一點 (我會O(N^2)暴試所有點
現在我想數以左邊那條BORDER為依歸的U型數目 (就是反轉的C型), 而紅點是這個倒"C"型的下面那條橫線, 這樣我們數的時候不會重覆

我要數的很簡單: 是這一點向上直到有牆 (用我們的length array可以直接算出),  在這一個Range入面有多少個左面開始partial sum 比紅點遠的 (或者一樣)?

上面的例子來說, 有2個, 這樣理論上就有2個 倒C型成了, 但留意其中一個是會在左BORDER連續佔了2格, 不合法的, 簡單說, 不能選紅點鄰住那些格, 因為這些格做了U TURN, 那條邊永遠都佔了邊上連續2格
 
話雖如此, 其實也只是RANGE的選擇而已
閃電型也類似, 簡單地說,  用我們precompute的partial sum + length array, 我們可以O(1)的就選好了我們的Range, 問題是我們要query, query的是這個range來,  # of 大於某個數的partial sum
這是赤裸裸的Range統計問題, 這時就是BIT出場的機會了!
log (1000) ~ 10, 所以其實O(N^2 lg(N) 也是很快的, 這使我更有信心是用BIT了

設bitLR(i, v) 代表由第0 row 至第 i row,  (左至右的partial sum where >= v)的數目
其它方向也類似, 這樣, 我們在precompute partial sum時也可以順路update 這些 BIT

剩下的只是按RULES, 暴試所有點O(N^2),  找出Range, 執行Query  O(lg N)

這樣就能數出轉向2次的合法線總數

把3個答案加起來就好了


Code很長, 超過200行=.=
其實有某些array 不用開的, 根本用不上
但看到有人幾十行做完, 那根本不是這個算法吧?
但我看Editorial也是這樣做的說 ...