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

2015年5月25日 星期一

Google Code Jam 2015 Round 1C

終於有心情打最後一篇今年的GCJ文

話說幾經辛苦把這場的solution都看完再把題目都AC之後

發現這場真的遺憾 還記得只要我做到 C-Small 就能Advance了

而C-Small (甚至C-Large) 在看完solution後驚覺是很易的...是應該能做到的

這兒學習到一件事, 要正視自己一直以來其中一個思考的弱點..下面再說

至於B-Large 是絕對不可惜, 甚至我也是在昨天才勉強明白solution的方法

只能說我的Probability / Expected Value學得太差了...

總結來說, 這場比1B學得更多, Ad-hoc度雖然也很高, 但仍然是有學習的地方

即場時我AC了 整題的A跟B-Small, 其它都是事後再AC的

玩了3場的GCJ Round 1, 我認為心理跟策略上也是有改善的空間

Google Code Jam 2015 Round 1C



A: (Ad-hoc, Greedy)

這題是我唯一能整題過的題目...
就結果來說, 其實Small的思路跟Large的是完全一樣
但即場由於策略問題, 完全沒多加思考Large 就跑去看B跟C了...
弄得同一樣的Code, 卻過了一小時才交A-Large...

題目本身很易嗎? 也不算, 也不是難...是很伏
很明顯很容易想少一些Case的類型

題目是這樣的:

給了一個R*C的Grid, 你跟你弟弟要玩一個遊戲.
你弟弟會放一隻 1*x  (打橫長x格) 的船在任意位置
你要做的就是猜船的位置, 並且把他打沉 (你看不到船的位置)
打沉的方法是: 把船的 x 格佔有位置都說出來

問題是你知道弟弟會出cheat, 他可以任意移動船的位置, 
只要跟你所知道的情報沒有矛盾就可以了
例如: 你說了某一格的位置, 弟弟可以先把船移動, 然後跟你說"沒有中"
當然, 當某一格他說了沒有中 (或中了) 之後, 那一個的狀態就不能再改變了
不然就跟你所知的情報有矛盾了

問題問, 你先走, 在知道弟弟會盡可能地出cheat的情況下, 你最少要用多少步才能勝出?

Small Case是Large的引導版, 是只有一個Row的 (R*C, R = 1)
我是先把想法大概猜到了, 才開始做個簡單的證明的

思路大約是: 由於最少都要用X做答案, 每次弟弟出Cheat會把答案的次數增大1
所以盡量減少他可以出Cheat的次數...對這個是可以控制到的, 因為他不能與我所得的情報相矛盾

怎樣可以限制弟弟出Cheat的次數呢? 不難聯想到跟Greedy有點關係
"盡可能"選擇某些位置使他不能出很多次Cheat這樣

答案就是: 由左至右, 每次選第 x 格
如果弟弟說了"中"的話, 基本最多再選 x 次就完結了 (為什麼不是x - 1次是伏位, 下面再說)
但弟弟這樣說沒好處, 如果他還能出Cheat的話
但他要再出Cheat, 就只能把船向右移了 (如果本身是選中了的話)
那麼我就再一次選向右的第x格...如此類推

那麼弟弟已經走投無路, 所有左邊的位置都比我"封鎖"了
他被迫要說"中了"
那麼我們就限定了那一格是船的某一部分
但伏位是...很自然會以為那格是船的 "最右點"
但這未必是真的,  要是 C 本身不能被X整除, 那麼其實右邊還有空間再移動
只是不夠整架船移動 (再出Cheat)

所以要是你用x - 2 步把船的 x - 2格都猜中了
第 x - 1 步, 弟弟永遠都可以說你錯了, 他可以出最後一次Cheat把船向相反方向移動一格
浪費你多一步 所以要是 C本身不能被 X 整除的話, 答案還要另外 + 1

這基本就是Small的答案了

而Large呢, 首先發現, 每一Row都是獨立的, 可以分開來看
Greedy地選也完全沒問題
所以就把這個策略重複 R-1 次, 最後一個Row就當是Small一樣的做法就好了...

策略上...我應該先花幾花鐘想想Small是否能改一下就能交Large的...
這題根本不用花一小時再交...

#include<bits/stdc++.h>
#define LL long long
#define PI acos(-1)
#define x first
#define y second
#define PII pair<int,int>
#define F(x,y,z) for(int (x)=(y);(x)<(z);(x)++)
#define pb push_back
#define mp make_pair
#define eps 1e-7
#define feq(x,y) (fabs((x)-(y)) <= eps)
#define flt(x,y) ((x)+eps < (y))
#define fle(x,y) ((x)+eps <= (y))
#define fgt(x,y) ((x) > (y)+eps)
#define fge(x,y) ((x) >= (y)+eps)
using namespace std;

int T,n,r,c,w,ans;
int main(){
    scanf("%d", &T);
    for(int qwe=1; qwe<=T;qwe++){
        scanf("%d%d%d", &r,&c,&w);
        ans = (r-1)*(c/w);
        ans += (c - w)/w + w + (c%w != 0);

        printf("Case #%d: %d\n", qwe, ans);
    }
    return 0;
}

B: (String, Probability, Expected Value)

這題是這一Round最難的了 (如果考慮Large的話)
這題有我最苦手的Probability / Expected Value的問題...
幸好結果來說我有學到東西了...

題目有點複雜就不說了 直接說分析

Small是很簡單的Brute Force, 隨便寫Backtrack都可以

Large就需要數學技巧了...
首先題目有點賤
題目要找的是: Expected Number Of Banana Left 

首先我來說一下我對Expected Number 的理解:
很白痴, 我只懂最簡單的definition: Weighted Average of Possible Result
也就是 E(X) = P(a)*v(a) + P(b) * v(v) + ...
where P() 跟 v() 是 probability 跟 Value of 所有Possible outcomes

然後因為之前某場CF, 我也得知了一個經常會用到的Property叫
Linearity of Expectation
這個是這條題目的重點, 是很方便很強大的Property, 可惜我對他的理解等於零

在CF上面厚顏地開了Post求問, 有人給了一些很好的Reference, 看了之後才開始有點理解
http://codeforces.com/blog/entry/18025#comment-228851

這個Property, 引用Wiki的說法: 
\operatorname{E}[a X + b Y + c] = a \operatorname{E}[X] + b \operatorname{E}[Y] + c\,
即使 X , Y 是 dependent 的Random Variable!!

先不詳細說這個東西, 回到題目上面
設 C為你會準備的Maximum # of bananas, 
那麼題目就是求 E(X) = E(C - X)  = C - E(X)  where X  = expected # of bananas give out

那麼題目就分兩部分了: 找出C跟找出E(X)

先說找 C
首先跟KMP一樣, 要先找出Pattern 的"最長前後綴"
由於string很短, 不用failure function找也可以...直接substring()找就行了...
有了這個之後..就可以直接用String的長度 S
去算出所有可能性裡面包括最多Pattern的String究竟有多少個Pattern
是為C

重點還是要在算E(X)上面吧
首先說一下不懂答案思路之前的想法:
跟上面說的一樣, 我只會最基本的Definition
所以很自然就會去想 
E(X) = P(Exact 1 banana)*1 + P(Exact 2 bananas)*2 + ...
(詳情我CF那個Post有說, 可以自己去看)

但當然這樣算沒有好處, exact X banana這些event 明顯就不是independent的
無限double count, 也不知道怎用 (知道也不會用, 想也知難code) inclusion-exclusion principle
總之直感也知道不是這樣直接去數的, 用Counting之類的數法是不行的了

然後...答案竟然是很直接的...
設P 為 Pattern在某一個位置出現的機率, 這個易算, 直接用Product Rule乘起來就是了
然後重點是:
By Linearity Of Expectation, E(X) = P*(S-L+1) As there are (S-L+1) possible starting point for Pattern

這句是如何來的...!?
看了CF那個Post後...我終於有了以下的理解 (還不知道對不對, 最少我信了)

先說一下Random Variable
它們只是一些有不同值的Variable, 每個值都associate 一個probability, 當然加起來就是 1

視乎情況, 這些值可以自己define出來的, 最常見的就是 1 跟 0 (某些desire result就設1, with some probability P, otherwise 0 with (1-P))

由於他們是variable, 可以照樣自己設立一些equation

例如設 Z 為 # of bananas giving out, 我不知道Z 的所有Value跟它們的probability 
但照樣可以寫一個equation

Z = A_1 + A_2 + A_3...
A_i 是 # of banana when pattern start at position n

A_i 我們倒是知道的, 沒錯就是最常用的 {1 with some probability p, 0 otherwise}
這個 some probability p 就是solution中算的 P, 所有A_i 的 p 都是一樣的

現在不難發現A_i 互相也不是independent 的!
在某一個位置假設出現了pattern, 某些位置就不可能出現了...

這就是Linearity Of Expectation 發光的地方了!
\operatorname{E}[a X + b Y + c] = a \operatorname{E}[X] + b \operatorname{E}[Y] + c\,
即使 X , Y 是 dependent 的Random Variable!!

Z = A_1 + A_2 + A_3...
==>
E(Z) = E(A_1 + A_2 + ...) = E(A_1) + E(A_2) + ...
= P*1 + P*1 + ...
= P*(S-L+1)

嗚啊...即使不是Independent也還是可以直接這樣做, So 變態...
這個技巧不用真的很熟悉Probability...也可以照學起來吧, 思路而已...

這題做不到真的不可惜, 因為回頭看根本不是能做到的東東
還有要知道: 不恥下問真的很重要, CF真的是很好的平台 :)

PS: 最後糾結的是...比較 P 跟 0的時候 不知道為什麼不能用eps, 不是應該要用才對嗎? 用了反而會錯, sample case都過不了...eps這個東東也是要認真面對一下了

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

int T, L,S,K;
int alphabet[300];
string kb, pat;
int main(){
    scanf("%d", &T);
    for(int qwe=1; qwe <= T; qwe++){
        scanf("%d%d%d", &K,&L,&S);
        cin>>kb>>pat;
        memset(alphabet,0,sizeof(alphabet));
        for(int i=0; i<K; i++) alphabet[kb[i]]++;

        // O = maximum overlap
        int O = 0;
        double maxBanana = 0, expectedBanana = 0, P_fixedPoint = 1;

        for(int i=0; i<L;i++) P_fixedPoint *= (alphabet[pat[i]]*1.0/K);

        if(P_fixedPoint > 0){
            for(int i=1; i<L; i++) if(pat.substr(0,i) == pat.substr(L-i)) O = i;

            maxBanana = 1.0 + (S-L)/(L-O);
            expectedBanana = P_fixedPoint * (S-L+1);
        }
        printf("Case #%d: %.8f\n", qwe, maxBanana - expectedBanana);
    }
    return 0;
}

C: (Ad-hoc, Greedy)

這題真的太太太可惜了...回頭看真的應該要做到的..

題目是說:
給了 D 種錢幣, 每種錢幣可以用C個, 現在要你合出 [1, V] 所有的value
當然有機會合不了, 所以你可以額外增加某一些種類的錢幣, 當然它們也只能用C個
問最少要另外增加多少種類的錢幣?

這兒我的思路, 一下子就飛去 Coin Change的 Classic DP去了
這個就是我開首說的壞習慣...
由Day 1 開始玩比賽到現在, 我每次看到題目有一點點像我聽過的Classic Problem
就會往那方向去想, 以為是一些簡單變種還是什麼
但其實by experience, 很多時候都是錯的...
有時候, 根本不用想那麼複雜(像這題)
有時候, 有一點點像, 但其實差很多很多 (特別是Graph類的題目)

總之這樣的"聯想式"的思考好像不太有效呢...很多時把自己帶進死胡同

這題結果也是這樣的, 其實跟Coin Change沒有關係...(當然如果會的話會有幫助)
整道題目只考你一個發現:

能合到的value像一條條segment, 是有range的

例如你不增加種類, 可能合到 [1,5], [10,12]....
這樣的話 其實 "6" 是一定要增加的了, 因為用現在所有種類都合不出來, 
也不可能用比6 大的種類合出來, 對吧?

這樣的話就是Greedy的想法了
我們想 maintain X, where X 是 最少的value such that [1,X] 都是你能合出來的
那麼X+1怎樣合出來呢?
可以試用 現在的種類(如果有還沒用的話)試試合不合到
不能的話只能增加合成種類
當X >= V 的時候 算法就完成了

When we add a new denomination X to S, the new set of values we could produce include each of the values we could produce with the existing set S plus between 0 and C of the new denomination X. If X is at most N+1, then this new set of values will be the set of all values from 0 to N+X*C, so we can update N to N+X*C.

這段借用solution的解說, 說明了增加/ 使用某一種類的錢幣時, 可以很簡單直接地update X (solution的notation跟我用的有不用, 注意一下)

由於只有D種現有錢幣, 而增加種類即使 X 變為 X+C*(X+1) = X(C+1) +C
C最少是 1 , 所以 X 最少是2倍2倍地增長, 直到 V, 最多就是O(lg V) 次

TOTAL = O(D + lg V) 的LOOP

這樣的分析..跟Facebook Hacker Cup 某題有點像, 也是不用嚴格地分析, 只要證明了
最少是2倍2倍地增加/減少, 就可以得出 O(lg X)的bound, 用來說服自己很有用

說起來, 這題的難度全部在於發現如何update X, 分析複雜度
至於Greedy地增加種類..是很直接的, 當時我是這樣猜, 但不能很系統地說服自己Greedy是正確的, 結果根本就是很明顯嘛...

int T,n,c,d,v,ans;
LL up, used, D[105];
int main(){
    scanf("%d", &T);
    for(int qwe=1; qwe<=T;qwe++){
        used = up = ans = 0;
        scanf("%d%d%d", &c,&d,&v);
        F(i,0,d) scanf("%lld", &D[i]);
        sort(D,D+d);
        while(up < v){
            LL X = up+1;
            if(used < d && D[used] <= X){
                up+= c*D[used++];
            }
            else{
                up += c*X; ans++;
            }
        }

        printf("Case #%d: %d\n", qwe, ans);
    }
    return 0;
}


終於終於 都把GCJ Round 1 的東東都搞完了...
剩下的就是String的文章, 還有Kattis那邊的"Lecture"吧?
String也是我拖太久 看到題B要用到的時候我已經打了自己兩巴 ...
(雖然根本重點在Expected Value)

GCJ, 下年再見吧 :/

2015年5月14日 星期四

Codeforces Round #301 (Div. 2)

經過GCJ的失敗後, 慢慢調整腳步回復正常

前幾天在公司看到了這場只有Div. 2 的Round

就跑去打了

話說原本的計劃是半休閒地打的

但這場的性質, 我認為是較陷阱型, 就是那些讓你不斷HACK人和被HACK的題目

本身題目能學習到的也不少, 但沒有前幾場那麼多

可能這場要真正參加才行吧...

我其實也是Virtual Participate的

但無奈在題B 的Implementation上卡了很久, 也被人發現我在做冗員就開始給我工作了

結果還是完了Virtual Participation之後當練習地慢慢打

題目方面除了是陷阱型, 題C我認為是題目1999...我理解完全錯誤 導至一直想不通

也不明白為何會這麼多人AC...下面再詳說吧~ 重點是題C-E (Div,1 的頹題) 學習到的東東

Codeforces Round #301 (Div. 2)


A: (Math, Greedy)

這題在老細的壓力下完成, 直接地說, 我是看Sample猜題目直接做直接交一棍AC...
題目就是給了兩個平時單車用的密碼鎖的密碼, 問你從第一個轉到第二個最少要多少下
就是每個digit greedy地看順時針轉還是逆時針轉較好了

B: (Math, Greedy)

沒想到這題卡我那麼久 害我WA了很多棍...
其實題目沒什麼好想的, 題目說明 n 個數字裡面給你 k 個 (n肯定是odd number)
然後問你能否把剩下的 n-k 個數字填上, such that SUM <= X && Median >= Y

思路大約就是Greedy地填, 由於想SUM<=X, 我們能填最少的都填最少 (i,e, 1)
但又想Median >= Y, 所以把n 個數字排序後,(假設已填好)  頭 n/2  個數字可以是1
中間那個數字及之後的直接填Y

這樣肯定SUM是最少的同時, MEDIAN 又 >= Y, 最後查一查SUM是否真的 <= X就好了

問題來了, 這題好多Case需要分析考慮...
例如可能給你的 k 個數字全部都 < Y, 而 k > n/2, 則答案直接是 -1
還有很多其它可能性...
簡單來說, 思路是這樣做的了, 但很多Case如果沒考慮就很易WA / 被Hack...

這題做Div. 2 的B 也算是有點難了 我個人認為 即場玩的話應該很多人都是Hack這一題來得分吧 以下是很難看的AC Code:
#include<bits/stdc++.h>
using namespace std;

int n,k,p,x,y,p1=0,p2=0, sum = 0;
vector<int> ans;
int main(){
    scanf("%d%d%d%d%d", &n, &k, &p, &x, &y);
    for(int i=0,a; i<k; i++){
        scanf("%d", &a); 
        sum += a;
        if(a < y) p1++; else p2++;
    }
    if(p1 > n/2) printf("-1\n");
    else{
        for(int i=0; i<min(n/2 - p1, n-k); i++) { ans.push_back(1); sum ++; }
        for(int i=0; i<n/2 - p2 + 1; i++) { ans.push_back(y); sum += y;}
        
        if(sum <= x) for(int i=0; i<ans.size(); i++) printf("%d ", ans[i]);
        else printf("-1\n");
    }
    return 0;
}

C: (Graph)
題目自己看, 看完再來看是我腦殘還是它語癌

題目給了一個 N*M 的Grid, 是典型用BFS 問由 S 能不能到 T 的問題
題目中說明每一格如果經過了一次, 會有裂痕, 第二次再經過就會掉下去了
本身格子有些已經裂了, 而S 是肯定裂的 (這個對Implement上有些影響)
問能否由S 到 T?

好了 我一直到不久前的理解是....我看到是 side view, 也就是說如果一格裂了
再到那一格, 玩家會直接掉去下面一格,  要是下面一格也裂了,就再掉...這樣
這樣的話問題就很複雜了, DFS/ BFS那種沒什麼次序的Transverse好像不行, 因為先走那一格都會影響到往後所有path...不能隨便走走走一直走到終點
反正就是沒什麼Idea

搞了很久, 甚至看了Editorial也不明白....到最後..真相是
題目的Grid 是Top View...所謂掉下去是...該格不能再走而已...

這樣就很簡單了..變回普通的BFS問題
但也沒這麼直接

首先由於題目要求的是走到T 那一格然後掉下去
所以如果T 本身是裂的, 那只要有Path到達就好了
如果T 本身沒裂, 則它旁邊最少要有 2個 "沒裂的格子"
(數"沒裂的格子"時要數上起點 S, 即使 S 已經裂了, 但如果它是T 的鄰居, 可以直接走去T使T變裂)
另外 S = T 也是Special Case, 處理方法是看 "沒裂的格子(不算S)" 是否 >= 1


基本上剩下的就是用BFS 看有沒有PATH能從S 到T 
當中注意的是每格不是用 isVisit[] 來看能不能走, 是直接看是不是裂的 (或者終點)
要是裂的直接當不能走, 要是能走的, 走完 (把點PUSH入QUEUE) 後 把該格變裂

#include<bits/stdc++.h>
using namespace std;
int n,m,sx,sy,tx,ty;
char w[505][505];
int dx[4] = {1,0,-1,0};
int dy[4] = {0,1,0,-1};
queue< pair<int,int> > q;
bool bfs(int cx, int cy){
    q.push({cx, cy});
    while(!q.empty()){
        int x = q.front().first, y = q.front().second;
        if(x == tx && y == ty) return true;
        q.pop();
        for(int i=0; i<4; i++){
            int nx = x+dx[i], ny = y+dy[i];
            if(w[nx][ny] == '.' || (nx==tx && ny==ty)) {
                q.push({nx,ny}); w[nx][ny] = 'X';
            }
        }
    }
    return false;
}

int main(){
    scanf("%d%d", &n,&m);
    int cnt = 0;
    for(int i=1; i<=n;i++){
        getchar();
        for(int j=1; j<=m ;j++) w[i][j] = getchar();
    }
    scanf("%d%d%d%d", &sx,&sy,&tx,&ty);
    for(int i=0; i<4; i++) if(w[tx+dx[i]][ty+dy[i]] == '.' || (tx+dx[i] == sx && ty+dy[i]==sy)) cnt++;

    if(sx == tx && sy == ty){
        puts(cnt >= 1? "YES":"NO");
        return 0;
    }
    if(w[tx][ty] == '.' && cnt <= 1) { puts("NO\n"); return 0;}

    if(bfs(sx,sy)) puts("YES");
    else puts("NO");
    return 0;
}

這題聽完也會覺得思路也是直接但又是很多Case要Handle要考慮, 又一題Hacking 爆分的題目...
還好這場沒參加...


D: (Math, DP, Probability)

這題學到東西了

首先題目很直接

是一個猜包剪dup的遊戲

由於題目數據, 基本直接就想到是 dp(x,y,z) 的形式了

formula也很快寫出來了

但反而是每一個Event的Probability有點難數

打和的情況也在想怎樣Handle的時候

最後看Editorial發現我一直理解 "equiprobably" 都有點錯誤

題目中: At some moments of time two random individuals meet (all pairs of individuals can meet equiprobably)

開始我的想法是每一種出現的機會是 1/3...

但它用的字眼是 "all pairs of individuals" 總覺得意思不一樣  也好像不用搞得很複雜
(如果是原本的想法, 數 species A 的數目 再算 n 隻入面抽中它們其中一隻的probability 就是 pairs of individual中 會抽到A 的機率....總之很複雜...這也令我更肯定我一定要重溫 (重新學習) Probability了...)

總之原來不用簡單複雜化, 又用nCr 又什麼的
題目說什麼是equiprobably, 就直接數那個東西的數目
在這題中是 "all pairs of individuals"
那就直接數啊... 所有可能性 就是 N =  r*s + s*p * p*r   <---s,r,p 是剪, dup, 石頭的數目
然後假設那一pair是 rock 跟scissor, 就是 r*s / N 啊...
剪刀死了 就把數目減一再DP啊...
注意這兒是直接沒有數打和的情況, 不論分子分母都沒有數!
我不知有沒有term, 但我形容的是, 在它考慮 "probability space"時已經直接排除打和了..很聰明正確的做法

所以就是這樣了 
設 DP(R,S,P) := probability to make the island have R rocks, S scissors, P papers left
這題用Top-Down絕對更易做
 int rp = (r+1)*s + s*p + (r+1)*p;
 int rs = r*(s+1) + (s+1)*p + r*p;
 int sp = r*s + s*(p+1) + r*(p+1);

 if(rp)
     dp[r][s][p] = (r+1)*p*1.0 / rp * DP(r+1,s,p) ;
 if(rs)
     dp[r][s][p] += 1.0*r*(s+1)/rs * DP(r,s+1,p); 
 if(sp)
     dp[r][s][p] += 1.0*s*(p+1)/sp * DP(r,s,p+1);
 return dp[r][s][p];

可能會有一種物種已經全死光了, 所以要查一下 if(rp), if(rs)等等 看是不是有可能出現這個組合

那麼石頭會贏的Probability
就是 Sum DP(i, 0, 0)  

另外2個會贏的Probability也是這樣做, 下面是一棍AC的Code
(這題沒什麼陷阱, 是想到就做到的題目, 我認為C是更難的...)

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

int R,S,P;
double dp[105][105][105] = {0};

double DP(int r, int s, int p){
    if(r > R || s > S || p > P) return 0;
    if(dp[r][s][p]) return dp[r][s][p];
    
    int rp = (r+1)*s + s*p + (r+1)*p;
    int rs = r*(s+1) + (s+1)*p + r*p;
    int sp = r*s + s*(p+1) + r*(p+1);

    if(rp)
        dp[r][s][p] = (r+1)*p*1.0 / rp * DP(r+1,s,p) ;
    if(rs)
        dp[r][s][p] += 1.0*r*(s+1)/rs * DP(r,s+1,p); 
    if(sp)
        dp[r][s][p] += 1.0*s*(p+1)/sp * DP(r,s,p+1);
    return dp[r][s][p];
}

int main(){
    scanf("%d%d%d", &R, &S, &P);
    dp[R][S][P] = 1;
    double a1,a2,a3;
    a1=a2=a3 = 0;
    for(int i=1; i<=R; i++) a1 += DP(i,0,0);
    for(int i=1; i<=S; i++) a2 += DP(0,i,0);
    for(int i=1; i<=P; i++) a3 += DP(0,0,i);
    printf("%.12f %.12f %.12f\n", a1,a2,a3);
    return 0;   
}

E: (Binary Indexed Tree, STL, Ad-hoc)

昨天終於最後AC了這一題....雖然也是有一半是靠Editorial啦...
這題新學到的東西不多  倒是應用了不久前學的東西!
就是 用BIT 來Count 一個 Array的 # of Inversions!

這題當然沒這麼簡單, 單單是把位置 map去一個 O10^5)大的array再用這個技巧
已經是又要用STL的Map又要STL的Set (當然可以不用...但反正這些都不是Complexity的Bottleneck就算了)

題目要求的答案可以分開兩部分算:

第一部分就是用這個技巧
我自己卡是卡在第二部分沒想通...現在想就很合道理了:

For 每一對連續受影響(有swap過)的 index, 它們本身的inversion已經用BIT (第一部分算了)
但它們之間 沒Swap過的數字與它們的Inversion(如果有)則沒有算, 這就是第二部分
方法其實很簡單, for 每一個swap過的數字 x ,  找回它原本的index, 數它們之間有多少個數字也是swap過的, 減走它們! 因為這些swap過的數字不論有否跟 x 組成inversion, 它們也已經在BIT那部分受考慮了,  減走它們正正是 "沒被SWAP過的數字" 數量, 這些數字肯定跟 x 會形成inversion.... 找 x 原本的位置可以用Binary Search做

這樣的話總結來說是 O(n lg n)的算法

這題最開心的是, 之前新學的Trick可以派上用場...也算是有真正apply這個技巧的經驗!!
但要是我能改改那些不小心的習慣, 例如 array開小了導至Runtime error 等等.... :o)

這也是這題為什麼多人AC的原因
因為第二部分(我卡的部分) 其實不難想, 主要是要懂用Merge Sort / BIT 去找 # of inversions

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

int n, bit[200010] = {0};
map<int,int> mp;
set<int> st;
vector<int> swapList, value;

void update(LL x){
    for(; x <200010; x+=(x&-x)) bit[x]++;
}

LL query(LL x){
    LL ret = 0;
    for(; x ; x-= (x&-x)) ret += bit[x];
    return ret;
}

int main(){
    scanf("%d", &n);
    for(int i=0,l,r; i<n; i++){
        scanf("%d%d", &l, &r);
        if(mp[l] == 0) mp[l] = l;
        if(mp[r] == 0) mp[r] = r;
        swap(mp[l], mp[r]);
        st.insert(l); st.insert(r);
    }

    int tmp = 1;
    for(set<int>::iterator it = st.begin(); it!= st.end(); it++){
        swapList.pb(*it); value.pb(mp[*it]); mp[*it] = tmp++;
    }

    LL ans1 = 0, ans2 = 0;
    for(int i=value.size()-1; i>=0; i--){
        int id = mp[value[i]], tt = i;
        ans1 += query(id);
        update(id);
        int pos = lower_bound(swapList.begin(), swapList.end(), value[tt]) - swapList.begin();
        if(pos < tt) swap(pos, tt);
        ans2 += swapList[pos]-swapList[tt] - (pos-tt);

    }
    printf("%I64d\n", ans1+ans2);


    return 0;
}

看看能不能在下星期內也學回GCJ 1B / 1C 的題目吧...要是清掉了就沒什麼累積的東西了~

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也是這樣做的說 ...