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

2013年4月1日 星期一

最小圓覆蓋

好久沒有學習新東西了,今天因為剛好要用到 (poi的plot)

所以就順便學一下了,如果有敘述不清,歡迎糾正>_____<

=====================================


Lv 1. Naiwny Algorytm(樸素算法) O(n^4)


枚舉三個點,做出一個圓圈圈,看有沒有覆蓋所有的點點點點


Lv 2. Zhang Jiao(張角法) O(n^3)



       想法其實也很簡單,就是枚舉兩個點(如圖中的A, B)當圓周上的點,接下來,因為離這個圓越遠的角一定越小,所以找出在AB線段上面跟下面兩種的最小角點(也就是像圖中的C與D),如果這兩個角alpha + beta >= pi 的話,你就得到一個包含圓了(太小就變成一個怪怪的橢圓了),雖然要特別判一下,就是如果max(alpha, beta) >= pi,就可以把AB當成直徑,如果 < pi,就拿這三個點做圓(必定唯一)。

接下來就要進行大進化!

Lv 9999999. Venusaur(廟哇花) O(n)

       其實叫隨機增量法 A___A ,利用一個神定理和Random的特性,以最樸素作法少三個冪次的超神演算法。(雖然常數頗大)

神定理:假設{p1, p2 ... pi-1},形成一個最小包含圓,若pi不在這個圓內,那麼{p1, p2 ... pi}所產生的最小包含圓一定有pi在圓周上。

{p1 ~ pi-1} 產生一個最小包含圓:
加入 pi 之後(不是M_PI,只是p的第i項XDDD)
--------------------------------------------------------------------
1. pi 在圓內or圓上:爽!什麼毛都不用做
2. pi 在圓外:對於{p1 ~ pi}的最小包含圓,pi一定在圓上(依據神定理)

=====================================

至於他發生2.的這件事的機率是多少呢!因為1~i ,普通情況只會有三個點在圓上,所以機率是 3 / i !!!!!!!!!!! 接下來你輕鬆亂推廣神定理。

亂推神定理:假設{p1, p2 ... pi-1},且點 x 在圓周上,形成一個最小包含圓,若pi不在這個圓內,那麼{p1, p2 ... pi}且點 x 在圓周上,所產生的最小包含圓一定有pi在圓周上。

{p1 ~ pi-1} 且圓周上有點x,產生一個最小包含圓:
加入 pi 之後
--------------------------------------------------------------------
1. pi 在圓內or圓上:爽!什麼毛都不用做
2. pi 在圓外:對於{p1 ~ pi}且x在圓周上的最小包含圓,pi一定在圓上(依據亂推神定理)


=====================================


相同的,發生2.的機率還是3 / i 或更低。這時你已經確定有兩個點在圓周上了。
接下來,反正就再做一次亂推神定理,就有三個確定的點啦~~~~~~~~~

三個點形成一個圓~所以就結束了。

至於為什麼是O(n),請自行做點小小的計算吧。

2012年12月9日 星期日

中國郵遞員問題(參考poj 2404)

題目敘述:

有一張連通圖,找出經過所有的邊且回到原處的最小權重和

============================

經過所有的邊且要回到原處,很顯然就是一個歐拉回路

但是跟歐拉回路不一樣的地方在於他沒有限制每個邊只能走一次

也就是說,只要是一張連通圖,就一定會有解

但其實你可以把它想成是歐拉回路的進階版本

歐拉迴路中,絕對不能有奇度的點(因為進出的數量相同)

只要有奇度的點,想要回到啟始點,就必定會重複

用重複的概念,可以把原本是奇度的點,修改成全部都是偶度的

至於要怎麼修呢?你可以想成我們對這張圖增加一些邊

原本偶度的點要增加偶數條,奇度的點要增加奇數條

換一種方式想,其實就是要在奇度點之間兩兩建立一條路徑,並使權重和最小

因為奇度點一定是偶數的(整張圖的度數必定是偶數),假設為2N

那就要建立N條路徑,使得這些奇度點兩兩配對,也就是所謂的一般配對

普通的題目N應該很小,所以可以直接用狀態壓縮DP(如本題)

=============================

算法描述:

1. 建出兩兩奇度點之間的距離(用Floyd或點太多用Dijkstra)

2. 將奇度點兩兩配對,並使權重和最小

3. 新增的邊權+原本圖上的邊權和 = 答案

//By momo
#include<cstdio>
#include<vector>
#include<algorithm>

#define N 17
#define INF 999999999
#define SZ(x) (int)((x).size())

using namespace std;

int n, dis[N][N], deg[N], dp[1<<N];
vector<int> odd;

void floyd()
{
    for(int k = 0; k < n; k++)
        for(int i = 0; i < n; i++) if(dis[i][k] != INF)
            for(int j = 0; j < n; j++) if(dis[k][j] != INF)
                dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
}

int dfs(int cb)
{
    if(cb == 0) return 0;
    if(dp[cb] != INF) return dp[cb];

    for(int i = 0; i < n; i++)
    {
        if(!(cb & (1<<i))) continue;
        for(int j = 0; j < n; j++)
        {
            if(i == j || !(cb & (1<<j))) continue;
            int v = dfs(cb^(1<<i)^(1<<j));
            dp[cb] = min(dp[cb], v+dis[odd[i]][odd[j]]);
        }
    }
    return dp[cb];
}

int main()
{
    while(scanf("%d", &n), n != 0)
    {
        int ans = 0, m;
        fill(deg, deg+n, 0);
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++)
                dis[i][j] = INF;

        scanf("%d", &m);
        for(int i = 0; i < m; i++)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c); a--, b--;
            dis[a][b] = dis[b][a] = min(c, dis[a][b]);
            deg[a]++, deg[b]++, ans += c;
        }
       
        floyd();
       
        odd.clear();
        for(int i = 0; i < n; i++)
            if(deg[i]&1) odd.push_back(i);
        n = SZ(odd);

        for(int i = 0; i < (1<<n); i++) dp[i] = INF;
        printf("%d\n", ans + dfs((1<<n)-1));
    }
}

2012年8月11日 星期六

That's talk about Network Flow

其實我一直以為我對網路流很瞭解了,

但其實有很多東西我還不會QQ

===========================從最簡單的開始

首先,有一個作法,就是不斷的流,流到不能流就停止的FF算法

要注意的是你在邊的某方向流了多少,就可以在另一個方向多流一些!!!!(就是剩餘的概念)

他的證明很神奇,但也不是什麼想不到的事情,只要你注意到一個KEY:『最小割』,『流』一定會小於等於『割』,當兩者相等時,流必為最大的,割必為最小的,也就是所謂的『最大流最小割定理』

每次選一條可以流的路 -> O(E)*最慘的情況下每次只增加一,而最大流是F->O(F) = O(FE)

這樣實在是太悲劇了,完全無法預知,只要流量都大到暴,時限內根本跑不完QAQ

Dinic在好久好久以前,就做了一個很棒的改進,他發現只要每次都走最短路(是指距離最短,跟邊上的流量無任何關係),就可以有很精確的複雜度了(但通常會比較小)


希望我寫的不會太爛,如果不想去了解的話,可以直接記結果,也就是每次都走最短路的話,最多只會擴充O(VE)次。所以重要的就是怎麼去找最短路。如果你感到很疑惑,走不走最短路真的有差嘛?還是那只是理論值而已,說說罷了。這裡可以附上一個非常誇張的圖:

走最短路的話,就可以避免這種愚蠢的情形發生。想要找最短路的話主要有三種方法:

1. 用BFS找:俗稱Edmonds- Karp Algorithm
這個概念很簡單,對於每次擴展都BFS一次,複雜度O(VE*E)

2. 用BFS建分層圖找:又稱Dinic Algorithm
他是先建出一個分層圖,不斷的dfs找路走,一直到沒有辦法走為止,就更新一次分層圖,並重複上述步驟。因為最長的distance是V,所以只會更新分層圖V次,複雜度O(VE+VE*V)

3. 用Relabel建分層圖找:又稱Improved Shortest Path Algorithm
先假定每個人距離T的dis都是0,接下來就開始用DFS找最短增廣路,如果你發現,某個點的距離已經不是你所想的那樣了,就把它設成他應該的樣子(這裡會用到一個概念,也就是點的距離只會增不會減,正確性在先前已證過),每次增廣O(V)*最大距離O(V)*某距離要查找幾次O(E)=複雜度O(VE*V)

ISAP code:

struct edge{int t,c,r;edge(){} edge(int _t,int _c,int _r){t=_t;c=_c;r=_r;}};
vector<edge> G[N];
int s, t, n;
int iter[N], d[N], gap[N];
int dfs( int p, int flow ){
    if( p == t ) return flow;
    for( int &i=iter[p]; i<SZ(G[p]); i++ ){
        edge &e=G[p][i];
        if( e.c > 0 && d[e.t] == d[p]-1 ){
            int f = dfs( e.t, min( e.c, flow ) );
            if( f ){ e.c -= f, G[e.t][e.r].c += f; return f; }
        }
    }
    if( (--gap[d[p]]) == 0 ) d[s]=n;
    else{
        d[p]=n;
        for( int i=0; i<SZ(G[p]); i++ ) if( G[p][i].c && d[p]>d[G[p][i].t]+1 ) d[p]=d[G[p][i].t]+1;
        gap[d[p]]++; iter[p]=0;
    }
    return 0;
}
int isap(){
    for( int ret=0; d[s]<n; ret+=dfs(s,INF) );
    return ret;
}

2012年8月2日 星期四

斜率優化

今天多虧了J睿的福,我很清楚的了解了斜率優化的基礎,

並自己透過概念,想出一套錯誤的演算法,再經J睿的修正,得出這個算法的正確形象

感謝最愛J!NN的J1SS睿~~~>//////<

他並不是很難,看到下面這個基本的斜率優化code就知道了,精神不過五行。

//By momo
#include<cstdio>
int s[100010],dq[100010],f,e,r,L,R,x,y,t,n,k,i;
bool crs( int a,int b,int c ){
    return 1ll*(s[b]-s[a])*(c-b)>1ll*(s[c]-s[b])*(b-a);
}
int main(){
    scanf("%d",&t);
    while( t-- ){
        scanf("%d%d\n",&n,&k);
        for( s[0]=f=e=y=0,i=x=1; i<=n;i++) r=getchar(),s[i]=s[i-1]+r-'0';
        for( i=k; i<=n; i++ ){
            while( f<e-1 && crs(dq[e-2],dq[e-1],i-k) ) e--; dq[e++]=i-k;
            while( f<e-1 && !crs(dq[f],dq[f+1],i) ) f++;
            if( 1ll*y*(i-dq[f])<1ll*x*(s[i]-s[dq[f]]) 
             || 1ll*y*(i-dq[f])==1ll*x*(s[i]-s[dq[f]]) && i-dq[f]<R-L )
                x=i-dq[f], y=s[i]-s[dq[f]], L=dq[f], R=i;
        }
        printf("%d %d\n",L+1,R);
    }
}

=============================今天就快速進入正題,code已附上
他基本上用來解的題目是這樣子的-Q:給一個序列(有N個值),問長度大於等於K的最大平均和?

預先操作為將序列轉成sum[i]序列(就是由a0+...+ai),題目也轉成在座標平面上(x為i值,y為sum[i]值),選出兩點使(sum[i]-sum[j])/(i-j)的值最大(其中i-j+1>=K),也就是斜率最大(故稱為斜率優化)



naive作法:枚舉i並枚舉他的j,O(n^2),爛作法-3-



這時,你重新的看一下你想要做的事情:



你其實想要做的是對於所有的i,要從K限制以下的點中,找出一點,使i節點與那一個點的斜率最大。

而從幾何上看來,即為一個圍起來的凸多邊形,而在 此多邊形 外的點,要與其中的所有點有最大斜率or最小斜率時,其必出現在端點上(線性規劃的精隨)。



(pf: 一個不是端點的點,代表在一個距離為e的圓(或半圓)內必存在另一個也在此多邊形中的點,若此點有最大斜率,則在此點指向i的向量順時針轉180度的範圍內移動e,皆可使斜率變大(反之則斜率變小),故唯有某一端點才會有最大斜率)



這時你就得到一個較快速的方法,求出前面的凸包,一個一個枚舉,而你又可以知道上半部的凸包並不是最大的(反而是最小的),原因如同上面的證明,所以你只需要記錄下凸包即可,但複雜度仍為O(n^2)。(下凸包定義:斜率遞增,且不會有點在此函數下方)


再多想一下你想要找的是什麼:



你發現了其實你要找的是一個切點-定義為與i形成的直線『斜率最大』的點-,經過一些直覺的猜想與證明,你知道這個切點與i形成的直線斜率要介於這個切點與兩個邊所形成的直線斜率之間,把它做切塊后,你可以很清楚的看到這樣的點只有一個,除非是在邊上(如圖所示),而且每個點與i形成的斜率會呈現遞增再遞減的趨勢。



{性質:斜率會是遞增再遞減,而切點則是使與i形成的斜率介於兩個邊所形成的直線斜率之間,且唯一}

pf:先證遞減,同理可證遞增








pf:再證當斜率介於這個切點與兩個邊所形成的直線斜率之間,則斜率比兩邊都還要大

這時你就可以用一個二分搜的方法求出切點,演算法複雜度:O(nlgn),但這並不是最好的,因為再多看一下我放的第二張圖,你會發現當你做到某一切點時,前面的點都可以刪掉了,因為若之後的點會把前方的點當做切點時,他的斜率必介於兩個邊的斜率之間,而且邊的斜率遞增,所以這個斜率一定會小於現在做到的斜率,故你也不需要去看前面的點了。加上這個單調性優化,複雜度變成了O(n)。






















2012年7月29日 星期日

AC自動機(為什麼講義裡沒有="=)

Aho Corasick Automaton
(還是覺得很煩,為什麼講義裡沒有,害我一直以為不用用這個==)-> tioj 1723

其實他的想法跟KMP是一樣的,徹底了解KMP的人,對AC自動機一定可以很快的有體會。

搞不好,在想這題的時候,就會自然而然的想到這個作法,只是因為一些地方會覺得這個作法不可行,但其實稍做優化,就可以在時限內解決。


=================即將進入正文=================


他聽起來很厲害,一年前聽到的時候,覺得他是某種很高尚的東西,但他其實不過就是一個用來做多字串匹配的演算法。(而且他真的很像KMP


還記得KMP嘛?概念就是做到某個前綴時發現不行,那就找他的某個後綴去試(failure function)AC自動機也是類似的概念。


AC自動機:


1. 先對多字串建出一棵Trie(我這個人不喜歡pointer,所以我都用array實作,開一個[N][26]的陣列。但因為N很大,算一下其實會發現他會MLE,我試了好久,發現只要把它改成一維的,他好像就會把沒有存取過的點視為未使用,但開二維的他卻會因為你把[1][2]存了,而認為你使用了所有[1][0]~[1][25]的所有空間而認定你MLE了,這只是個偷吃布的怪作法,但tioj可以騙到,搞不好連自己的電腦都可以這樣騙?!


2. 用類似的方法建出failure function(因為你要做的是先求深度淺的再慢慢往較深的地方走,就跟KMP一樣,所以通常會使用BFS


3. 要多記錄output function(他其實跟failure function很像,但是因為這裡面會有很多個匹配字串,所以當你發現某一個匹配字串被匹配時,你要把它所有也是某個匹配字串的后綴都印出來,或做一些操作,製作方式可以直接拿failure function過來,只要確定他是某個匹配字串即可)


4. 開始匹配!(基本上跟KMP一樣,除了最後找到的時候,要遞迴一下output function)


時間複雜度:

1. O(n)(阿就Trie阿)
2. O(n)(理由與KMP相同)
3. O(n)(理由與2. 相同)
4. O(n)+O(m)+O(K)(這裡比較特別,前兩個跟KMP也是一樣的,O(K)卻是新的東西,他代表著所有的匹配字串總共在文章中幾次,這就是我說的會讓你以為不行的地方,但其實可以用各種方法把這問題去除掉,我是用sort的,但可以不要把所有的output function都做一遍,可以先改那個節點,最後再DP跑過去)

code: http://codepad.org/nh65NWcc

2012年7月22日 星期日

umm~傅立葉轉換+小小Latex

這是為了專題所需,是很偏的東西

我覺得資訊的部分,還蠻簡單的,但在物理的部份就比較困難

但他實在是從物理開始的,所以從物理講起還是比較合理。

喔對了,在看下去前,希望先對複數平面,和些許微積分有初步體會

而你會在維基看到什麼『DFT』,『FFT』,『Fourier Series』,概念差不多,只是用處不同

================================

首先!你要知道一個很重要的等號(原因的話,基本上是因為他們在微分的性質上是相同的且在相乘時也有相同性質,亦可以說他們其實是一體兩面的)


再來!你要對『正交』,也就是運用在非幾何地方的垂直,有一些了解:(我個人覺得這個東西很像向量的內積<只有兩個相同時才會有值,若兩個向量垂直則會得到零>所以故意用了dot,但應該是都可以的)


至於為什麼的話其實很簡單,看一下前面說的,轉換成圖形,一目了然

傅立葉級數(Fourier Series):

你也可以叫他傅立葉展開,比較貼切他的本意。傅立葉在很久以前發現一個週期函數(舉個例就是9e^i2*(pi))可以拆成唯一的sin, cos線性組合,寫成式子如下。這個東西很明顯就是已經知道 f 函數的週期才下去拆的,而在下式,我們是假設 f 函數為以2pi為週期的函數。

其實從第一式可以看出她的週期是2pi的蹤跡,第一式因為他是sigma,也就是說k為整數(就是角速度),你便可從中體會到 f 函數是由一群週期為2pi的倍數的基波(sin或cos函數)所組成(你可能會有疑問,為什麼是2pi的倍數勒?看本篇最上面的等式你就會了解了)

至於第二式,則有點套套邏輯的感覺(但他並沒有),他在裡頭又套回了第一式(想像他是由多個不同頻率的波所組成),並且用了剛剛所謂的正交的方式,將bk取了出來~實在是非常有趣的一件事情。




傅立葉轉換(Fourier Transform):

最主要的用處,是將一個時間(t)對振幅(A)的圖(簡稱A-t圖)轉換成一個頻率(f)對振幅的圖(簡稱A-f圖)。說的更清楚一些,就是將一個看似週期函數的,做某種奇特的分解,將他變成無限多個基波的組合(有種泰勒的fu~)。(我們平常看的應該是第二式吧)

這個新的等式,看起來很不一樣,但其實改變的只有兩個地方:

1. 我剛剛有說在前一節用的是以2pi為週期,但這裡所要轉換的是不被認為是週期函數的函數(所以我才說很像泰勒,舉個例的話就像常態分布函數),因為你不知道他的週期,他有可能由很大週期的sinuoid所組成,所以必須要包含到所有的w(這裡用的就是平常物理上在用的角速度,他代表的也確實是角速度)

2. 就是第二式從-2pi~2pi變成-inf~inf,原因的話跟剛剛一樣,你想要包含這個某某函數的一切,但你不知道他的範圍,故必須要全部考慮進去才行。

剩下的,像是正交什麼的概念,皆由傅立葉級數一併帶過來。




離散傅立葉變換(Discrete Fourier Transform):



這部份的公式其實就像是直接從前面的FT所轉過來,所以就不多討論了。從這裡我們要從另一個方向來看這個問題,從資訊的角度來看。

假設你現在碰到的問題是這樣的:

給你兩個多項式A(x)和B(x),求C(x) = A(x)*B(x)。

最原始的方法就是將他們依照原本的公式,以O(n^2)的時間得到答案,但這實在是太慢了,我們不想要以這麼弱的時間求得,這裡我要給你們一個method使這個問題可以在O(nlgn)內求出來。

首先,我們要先對多項式有初步了解。

 n 項多項式A(x)的表示法:

1. 係數式:就是平常寫的a0+a1*x+a2*x^2...an-1*x^n-1
(乘法所需時間:O(n^2))

2. 點值式:用n個(x,y)所表示,且y = A(x)且每個x皆相異
(乘法所需時間:O(n))

而你可以證明出一個有n個點的點值式寫成n項係數式(說的精準一點應該是一個最多n項,因為有可能高次項的係數為零),只會有一種寫法。


(Hint:其實你就是要解一個n元一次式,而由克拉馬公式可以知道,要有唯一解的充要條件便是delta不等於零,接著,你可以用數學歸納法證明出delta = product of( xk-xj ) for{ 0<=j<k<=n-1 },既然n個x皆不相同,則delta != 0,所以只有唯一解)


這時候,相信有認真看的人,腦中都會浮現一個想法,如果能很快的把係數式轉乘點值式,再以O(n)做乘法,最後再很快的轉回係數式,不就得到你想要的答案了嘛?苦苦思索,發現以平常的方法想要轉很快,還是得花上O(n^2)的時間,這不就跟原本一樣嘛?還要寫更長,所以在這裡我要給出一個演算法了!(是高斯發明的)


只要你選了好的x點,便可以在O(nlgn)的時間內,完成從係數式轉點值式的夢想。


在接下來的部分我們都把n當成是2^k,所以每次切半的n都會是偶數,至於在真實情況中,我們可以把n補乘2^k,最多變兩倍,並不會改變時間複雜度,就讓我們一起探究吧!


如果你有n項,那我們所選出的x值,便是使得 x^n = 1 的共n個解(我們分別叫他們w0~wn-1)而這裡用到的概念只有一個:


(n項)

(n/2項)
(n/2項)

這時,心機重的你可能已經發現了,如果我們定義了一個method叫做求A(w0~wn-1)(且w^n = 1),那你把他遞迴求A[0]和A[1],他在method中的 w' 剛好會是原本的 w^2 ,跟我們想要的一樣,所以我們便可以D&C的作法求出答案(細節的話,就自己想一下囉~)


相信平常愛寫程式的人,一看就知道這就是O(nlgn)了~所以我們的夢想實現了。不知道你們有沒有往上拉,我們剛剛所使用的公式,就是DFT的轉換公式呢!至於逆運算(也就是點值式轉係數式)則可以用原本正交的概念,湊出一個逆公式(其實不過就是把integrate轉乘summation),相信你湊出的逆公式就是DFT的逆轉換公式哩。(喔~對了,剛剛講的演算法就是所謂的快速傅立葉轉換,FFT,也是平常用在電路,訊號的一個快速演算法,同時也是使DFT在生活中廣受運用的一大原因)


其實多項式的乘法就是圓周卷積,而將其做傅立葉轉換後,就會變成直接乘法,是我們把傅立葉運用到資訊上所得到的結果,但你也可以把傅立葉轉換想成就是係數式轉點值式,全然看你想從哪個層面去認識他罷了。

對了,這裡面的數學式都是我用Latex打的,所以偷偷講一下他:

包住一群東西:打 {}
一群字:打 \mbox(...)
特殊字:打 \XXXX
上標:打 ^
下標:打 _
討論:打 \left\{ \begin{array}{rcl}...&...&...\\...&...&... \end{array} \right.
(rcl代表要幾塊東西,也可以打rc或rl...,而&就是分塊用的,而如果你要換行,則是打\\)
矩陣:打 \left( \begin{array}{rcl} ...&...&... \end{array} \right)
積分:打 \int_{-\infty}^{\infty}
(從負無限大積到無限大)

各種字元:CLICK HERE


2012年7月20日 星期五

Gusfield Algorithm

也就是俗稱的Z algorithm

概念也很簡單,就是記錄每個後綴與母字串的最長共同前綴

他的時間複雜度跟KMP是一樣的,都是O(n)

但我覺得他的概念比較簡單,實作方便

他的精隨在於:記錄比對到的最右界R,及比對到此右界的後綴起點L

並且可以分成三種情況討論:

1. 要比的後綴根本沒被比過 -> 就去比吧

2. 要比的後綴的前面部分已比過但長度未知 -> 繼續比吧

3. 要比的後綴的前面部分已比過且長度已知 -> 直接記錄吧

我的code: http://codepad.org/OPoY0Pu4

若要做字串匹配,直接把兩個字串黏在一起(要找的放在前面)
看哪些點的Z[]值 = 找尋字串的長度,便可知其出現的數量


有人會問這樣他的功能不是跟KMP一樣嘛?學他做什麼?

我覺得應該是為了增加另外一種想法吧!因為經過轉換後,就可以用在KMP所無法運用的地方了

所以在這邊稍稍分析一下兩者的差別:

KMP: 每個前綴與其後綴的次長共同前綴(最長的後綴)
Z_Vl: 每個後綴與母字串的最長共同前綴(單純的長度)

大致記得他們的差異,就可以在碰到題目時,適當的做運用。

2012年7月16日 星期一

還記得SA數組嘛?

之前在似曾相識的那一提,有很認真的討論過關於SA數組的construction和他的概念等,並在最後面的地方,隱約提到了有關 h 數組的事情,也提到了幾個定理,但並沒有好好的講清楚,在這裡要將他們一一證明。

==============================要進入主題了~

Longest Common Prefix:(照字面上就是最長的共同前綴,而他確實是這樣的東西)
    
名詞解釋:
i =x j: 兩個後綴前x個字元相同 
sa[i]: 第i名後綴是第幾個後綴
ra[i]: 第i個後綴是第幾名後綴
lcp( i, j ): 第i個後綴與第j個後綴的最長公共前綴
LCP( i, j ): 第i名後綴與第j名後綴的最長公共前綴(照字典序sort後)
h[i]: LCP( rank[i]-1, rank[i] )

------------------------------------------------------------------------------------- 


一。 LCP Lemma:  LCP( i, j ) = min( LCP( i, k ), LCP( k,  j ) ); { i<=k<=j }


proof: 


1. 設 x = min( LCP(i,k), LCP(k,j) ), 則i =x k 且 k =x j, 故LCP(i,j) >= x


2. 設LCP(i,j) > x, 則 i[x+1] = j[x+1].
    因 i < k < j 且 i =x j =x k, 故 i[x+1]<=k[x+1]<=j[x+1].
    因此i[x+1] = k[x+1] = j[x+1], 矛盾!!!!


3. 由1.和2.可知LCP(i,j) = x = min( LCP(i,k), LCP(k,j) ).


-------------------------------------------------------------------------------------


二。 LCP Theorem: LCP( i, j ) = min( LCP( k, k-1 ) ) { k = i+1~j }

proof:


1. j-i == 0 和 j-i == 1 皆成立


2. 若 j-i == m 成立,則LCP( i, j ) = min( LCP( k-1, k ) ) { k = i+1~j },
    由LCP Lemma -> LCP( i-1, j ) = min( LCP(i-1, i ), LCP( i, j ) )
    -> LCP( i-1, j ) = min( LCP( k-1, k ) ) { k = i~j } -> j-i == m+1成立


-------------------------------------------------------------------------------------


三。LCP Corollary: LCP( i, k ) <= LCP( j, k ) { i <= j <= k }

proof: LCP( i, k ) = min( LCP( i, j ), LCP( j, k ) ) <= LCP( j, k )

-------------------------------------------------------------------------------------

四。h[]數組的特性h[i] >= h[i-1] - 1

proof:





LCA和RMQ的詳細討論(?!)

又開始學新的東西,突然發現我了解的東西實在很少阿

今天要講的有LCA和RMQ(兩個關係匪淺的好玩有趣酷炫東西)

講完這個希望可以進入新的一個境界,並進入可怕的『字串』

=============================================入正文

LCA有兩種簡單的作法:

第一種:


我最常用的方法,就是用二分搜的方式。先以 O(nlgn) 做出parent[k][u],代表 u 往上2^k所到達的點,作法的精隨:parent[k+1][u] = parent[k][parent[k][u]]。接著,對於每個詢問O( lgn ),先讓兩個人的深度相同,再慢慢往上移但不要讓兩人的點相同(這個寫(想)法很酷,是iwi想到的,詳情可以看他的『邏輯腦』)。u, v 是所要詢問的兩點(假設兩點以再同一高度,若不同可以用十進位轉二進位的方法如法炮製),最後回傳的為他們的LCA。 

for( int k = MAX ; k >= 0 ; k-- ) if( parent[k][v] != parent[k][u] ) u = parent[k][v], v = parent[k][u];


return parent[0][u]; //最後還要再往上爬一格才行(超酷><)


第二種:

這才是今天的重點,把它壓扁成一條線,做RMQ!想把樹壓成一條線,唯一作法:時間戳記。但今天要的可能出現在從 u 走到 v 之間的任意點,所以在一個點被邊『走到』都要記錄他的時間戳記,因為在他們的時間戳記之間的所有點的 lev 一定<= 他們的LCA。(若有人 > 他們的LCA,則代表 u 必須走超出他們LCA再走下來才會碰到 v,這樣LCA就不是他們的LCA了,矛盾)所以對於每一個詢問,用RMQ求出之間的最小值即為所求。

==============================================進入RMQ

首先,最基本的RMQ大家都熟到炸開了吧?!也就是所謂的線段樹解法,有點太老梗了在此便不多提。我想要說一下Sparse Table(稀疏的桌子)解法,簡稱ST算法。

//////////////////////////////////////////////////////////////////////////

ST算法:O(nlgn) - O(1),先建立一個表,tbl[x][k],代表a[i] { i = x ~ x+2^k-1 } 的最小值,而這個算法的精隨為:tbl[x][k] = tbl[x][k-1]+tbl[x+2^(k-1)][k-1] ,簡單精巧又快速的好方法。

/////////////////////////////////////////////////////////////////////////

+-1 RMQ 算法:O(n) - O(1),概念:塊狀分解(設c個一塊,共n個)

預處理:對n/c塊做ST -> O( (n/c) * lg(n/c) );
                對所有零碎的可能狀況建表 -> O( 2^c*c^2 ); (你知道只會有2^c種,因為相鄰的接只差一)

詢問:用 ST+已有的零碎 -> O(1)

經過一番計算,可以發現 c = lgn/2時,預處理時間可以壓到O(n)

//////////////////////////////////////////////////////////////////////////

笛卡爾樹:(一個可以把序列轉換成樹的方式)

Definition: 根為此序列的min值,左子樹由min點左邊的序列所構成,右子樹則是右邊的。

Construction:

void 把一個節點放入一顆笛卡爾樹( int a[i], int root_id ){
       if( 其比此樹的根值大) 把一個節點放入一棵笛卡爾樹( a[i],  root_id*2+1 );
       else 把此樹拔掉,已此點取代後,在把被拔掉的樹當成此點的左子樹;
}
Property: 當要找出某區段L~R的RMQ,即為在此樹上二節點LCA的值。
(原因:LCA的性質便是 l 節點在他的左子樹,而 r 節點在他的右子樹(一般特性),且其為一個包含 l 和 r 的區段中的min(笛卡爾樹),故其為l~r區段中的min值。


//////////////////////////////////////////////////////////////////////////

有了上述的基礎後-

當你碰上了RMQ,你可以先轉為笛卡爾樹求LCA,再轉回+-1RMQ,以 O(n) - O(1) 解決。


2012年7月15日 星期日

講講KMP好了

所謂的KMP,想要解決的問題如下:

給你兩個字串: A 和 B,問B是否為A的子字串?
(長度為 n 和 m)

於是你想到了一個爛作法(他有一個帥氣的名字叫naive啥鬼的):

枚舉所有在A上的起點,一一比對,看有沒有跟B一樣
時間複雜度:O( (n-m+1)*m )

這裡呢,我要奉上一個超讚的演算法,由罩神Knuth和他的夥伴所創出的KMP算法。

時間複雜度:O( n+m )

==============================廢話到這裡結束

先想想在爛作法中的一些智障缺點,你像下圖這樣比對了一段,但在後面發現你們並沒有辦法在這樣繼續下去了,你把下面的B向後移了一格,又要像剛剛那樣再試一次。

但事實上你已經比對過後面的那些了,這時候,如果可以 O(1) 知道要擺在下面四個位置的哪一個(如果可以還是要選第一個),這樣就不用再一次的重新比較剛剛比過的『灰色箭頭部分』。如此便可以很順利的跑過 n 個字母,並順利的以O(n)結束掉。

其實剛剛你想要瞬間知道的就是,對於B字串,以『直線+兩顆點點』做結尾的所有後綴,哪一個可以和B的前綴完全相合(且合的越多越好)。

所以我們假設以 i 作結尾的後綴,最長可以和以 G[i] 做結尾的前綴相合。
當我們知道G[1]~G[i],可否推出G[i+1]呢?來討論一下:

假如B[i+1] == B[G[i]+1],那你就知道,G[i+1] = G[i]+1。
否則:去試B[G[G[i]]+1],再不行就是B[G[G[G[i]]]+1],試到ok 或 試到沒得試了(也就是0)

聽完之後,你想想可能會問為什麼不是O(m^2),但你知道G只會增加一,最多加到m,所以扣也只能扣m,整體來看就是O(m)。(前面的也是相同的情況)


code: http://codepad.org/JhbPTUVc


2012年3月6日 星期二

卡特蘭數

卡特蘭數:他是一串數列
來歷:只能往右走或往上走,從左下走到右上(正方形),不能超過對角線的種類數。 
遞迴式:CAT n+1 = SUM ( i = from 0 to n ) CAT i * CAT n-i
一般式:CAT n = 1/(n+1) * (C 2n取n)
帥氣遞迴式:CAT n+1 = (4n+2)/(n+2) * CAT n

要說的是他的應用和廣義的卡特蘭數:(這完全是我胡亂思想出來的,有錯請跟我說)
1.n*n正方形,不超過對角線的走法種類(基本)
2. n對括號可以產生的正確匹配方式種類數(簡單)
『解說:( 是往右走,)是往上走,其他跟 (1) 一模一樣』
3. n個節點產生的二元樹種類數(困難,請大神不要嗆我X{)
『解說:首先要先知道n個節點完整的二元樹會有n+1個空位(空位定義:上方有節點但此地無節點),pf: n=1,有兩個空位,每增一個節點,會多一個空位,故得證。接下來要靠想像力了,我有解說啦,可是沒有圖,真可惜~』

詳細講解:
        有一個空圖。放一個節點=往右走,放一個空位=往上走。
        pf: 如果放的空位比節點多,這個圖就會是一個『完整』( != 完全 )的二元樹,也就是無法在繼續放點了(看我前面的pf吧),所以不能超過對角線,這樣就對應到 (1) 啦~開心XDDDD

好,再來就要說今天比賽中的三元樹了,其實網路上真的沒什麼廣義卡特蘭數的講解,所以一切都是從我小小的大腦胡亂思考而成的,請不要把我嗆爆XD

廣義卡特蘭數:原本是『向上走的<=向右走的』,但這裡乘上一個可愛的常數 k (好啦,他不可愛)變成『向上走的<=向右走的*k』,所以本來的正方形也變成矩形了,很棒吧~XD
一般式:CAT n (k) = 1/( (k-1)n+1 ) C kn 取 n
神奇遞迴式:自己不會算喔!
應用:n個節點可產生的k元樹個數。
『解說:跟剛剛的二元樹情況其實很像,只是節點數 n 的k元樹,空位數為 kn+1,證明方式依然相同,而且想法其實也是一模一樣的(事實上,我是為了想這題,才會想剛剛那樣的解釋方式)』

2012年2月26日 星期日

Minimum Cost Flow

這真是一個蠻麻煩的東西耶XP

-最小成本流量問題-

今天了解的大概是這樣:
演算法名稱::『最短成本路線流漸增式演算法』(亂取的XD)
演算法內容:: 用『某種可以求有負邊的最短路演算法』求最短路(cost)+盡量流過去~
演算法條件::
1. 要求由s到t流動f的最小花費(這就是演算法目的X))
2. 沒有負環(原因看證明)
演算法證明::
1. 證明『f=最小成本流量,若且為若剩餘圖沒有負環』
pf: 設 f ' 小於 f ( 在某圖以某相同流量流動的花費 )
(1) 流量相同 -> 差異為一個環(f ' - f 的流量圖,任一點入度=出度)
(2) f ' 小於 f & (1) -> 差異為一個負環(f ' + (-f){邊正負顛倒}小於零,那個差異是負的)
若剩餘圖有負環,因f ' - f = 負環,故f ' = f + 負環,得證。
2. 證明『每次都取s到t的最短路,就會是最小成本流量』
pf: 數學歸納法
(1) 因圖沒有負環,f0 = 最小成本流量 = 零
(2) 若fi 是最小成本流量,fi+1 是 fi + (s到t的最短路徑),故fi+1 也是最小成本流量。(反證法:若有另一路徑f 'i+1 fi+1 小,因fi+1 是 fi + (s到t的最短路徑),但卻存在一更小成本流量,由第一個證明得知這個圖有負環,而且是fi 的問題(因為s到t是最短路徑,無更短的),那麼fi 不是最小成本流量,故矛盾,假設不成立