• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

                 摘要: 【例題】[SDOI2011]染色(注:數據范圍木有交代,應為:點數N<=105,操作數M<=105,所有的顏色C為整數且在[0, 109]之間。一、樹的路徑剖分當中點權型(點上有權值而邊上木有)的處理辦法:(1)找重鏈建線段樹的時候,w0中存儲tot+1個數,為該重鏈自上而下的各點的權值(例題中為顏色);(2)除了父邊是輕邊的葉結點之外,樹中的每個結點都屬于且僅屬于一條重鏈(根據定義得...  閱讀全文

            posted @ 2012-01-12 20:44 Mato_No1 閱讀(515) | 評論 (0)編輯 收藏

                 摘要: 原題地址【有關樹的路徑剖分的東東在網上介紹的太多了……】常見的路徑剖分的方法是輕重邊剖分,即把樹中的邊分為輕重兩部分,方法:設SZ[i]為以i為根的子樹的大小(結點總數),則若點x不是葉結點,則其子結點中SZ值最大的(注意,有多個SZ值最大的子結點應任選一個,只能選一個,防止出現重鏈相交,引發歧義)點y,邊(x, y)稱為重邊,其余的邊都是輕邊。首尾相連的重邊稱為重鏈(注意...  閱讀全文

            posted @ 2012-01-03 16:41 Mato_No1 閱讀(1517) | 評論 (2)編輯 收藏

            【本沙茶以前曾經搞過矩形的面積并、周長并問題,當時幾乎是抄神犇代碼的,根本木有理解,現在在看了掃描線以后理解一些了】
            “掃描線”嚴格來說不是算法,而是一種思想,它在解決很多幾何問題或者矩陣問題中有大用。所謂“掃描線”,就是用一條假想的直線(一般是水平或者豎直的)沿x軸(或y軸)方向掃過整個平面,在此過程中,每當發現有特殊事件(比如插入、刪除事件等),就執行這一事件。一般來說,它需要配合數據結構進行加速,常用的數據結構有線段樹、平衡樹、樹狀數組、堆、凸多邊形集合等。

            【1】矩形的面積并和周長并問題:
            這個在以前總結的那一篇里面曾經說明過,這里只是補充一下。

            對于面積并,可以假想一條水平直線沿y軸正方向掃過整個平面,每當遇到矩形的上下邊界,就插入或刪除一條線段(上邊界為插入,下邊界為刪除),在每兩個相鄰上下邊界之間得到覆蓋面積為:目前插入(尚未刪除)的所有線段覆蓋的總長度乘以兩邊界間的距離。顯然,求前者需要用離散化(橫坐標)+線段樹實現。

            這里講一下離散化的步驟和易疵點:首先將要離散化的數據(設為A數組)存儲在一個數組B里,然后對B排序(一般是遞增排序)、去重(方法:若B長度為0則不需去重,否則B[0]保留,從B[1]開始,若B[i]>B[i-1]則保留B[i]否則跳過B[i]),得到一個新的數組B0,最后,再對原來的A數組中的每個元素在B0中二分查找,找到其位置即可。顯然,對N個數據離散化時間復雜度為O(NlogN),具體實現時,可不設B0,直接存在B里面(見代碼)。易疵的地方主要是B的長度為0的情況。
            re(i, n0) B[i] = A[i].x;
            qsort(B, n0, 
            sizeof(B[0]), cmp);
            = 1; re2(i, 1, n0) if (B[i] > B[i - 1]) B[n++= B[i];
            re(i, n0) A[i].x 
            = find_x(A[i].x);
            其中n0為數據個數,n為去重后的數據個數(也就是離散化后的編號范圍,很重要的!),find_x為二分查找過程。

            對于周長并可以這樣理解:照樣是水平直線沿y軸正方向掃過整個平面,照樣是遇到矩形的上下邊界就插入或刪除,這樣,周長的水平部分是很好得到的,只要統計每兩個相鄰上下邊界的線段覆蓋總長之差即可,對于豎直部分則需要統計被線段覆蓋的端點個數——或者說,需要統計至少作為一條目前插入線段的端點,且木有被任何一條線段部包含的端點個數(因為只有這些點所延伸下來的豎直線段有可能作為周長的豎直部分)。維護方法:給每個結點設立ss、lbd與rbd,分別表示該結點區間內這種點的個數,以及該線段區間的左右端點是不是這種點。lbd=LCH.lbd; rbd=RCH.rbd; ss=LCH.ss+RCH.ss-2*(LCH.rbd || RCH.lbd)(因為我們需要的是線段樹而不是點樹,因此其中的每個葉結點表示的實際上是一條單位線段,而不是一個點,這樣,LCH區間的右端點與RCH區間的左端點其實是同一個點,如果它們都是這種點,則都不能算,因為被包含在內部了)。例外的是,如果某個結點區間完全被某條插入線段包含,則ss為2,lbd、rbd均為1(這個結果可能不正確,因為可能左右端點被包含在內部,因此,整棵樹上只有根結點的ss值是絕對正確的ss值,其余結點都需要特判)。這樣,根結點的ss值就是這種點的個數,乘以兩相鄰上下邊界的距離就是之間的豎直部分總長度。

            代碼:
            面積并(HDU1542)
            周長并(HDU1828)

            (此外,周長并還有一種更容易理解的算法:就是先求出水平部分總長后,將所有的矩形旋轉90度,再求一次水平部分總長,這就是原來的豎直部分總長了)

            【2】應用:
            PKU2482

            posted @ 2011-12-25 16:10 Mato_No1 閱讀(393) | 評論 (0)編輯 收藏

            原題地址
            這是一道線段樹操作的極品題,因為它的4個操作剛好覆蓋了線段樹操作問題的3種處理思路,可以說是把線段樹操作的全部內容都包含進去了。

            線段樹是一種靜態的數據結構,因為一棵線段樹一旦建成,其形態就永遠不會發生變化,改變的僅僅是結點上記錄的各種信息的值。因此,對于線段樹操作,核心問題也就是如何維護和處理這些信息。總的來說,對于線段樹結點信息的維護和處理,有以下3種基本思路:
            (1)左右歸中型:
            所謂左右歸中,就是用左右子結點存儲的信息來得到父結點存儲的信息的值,這是最常見的線段樹維護方法。舉一些簡單的例子:比如結點的SUM域表示該結點區間上所有元素的和,那么這個域維護的方法就是“父結點SUM=左子結點SUM+右子結點SUM”,再比如結點的MAX/MIN域表示該結點區間上所有元素的最大/小值,那么維護方法就是“父結點MAX/MIN=max/min{左子結點MAX/MIN,右子結點MAX/MIN}”。這種維護方法也比較簡單,只要在每次對子結點進行修改之后upd一下就行了(對于那些自頂向下遞歸,而且涉及到改值的操作,在遞歸完左右子結點后一定要記得upd一下),在這之中有一個很重要的思想就是“左右連續段”思想:如果題目中要求任意一個區間內的具有某種性質的最長的連續序列(也就是子區間),比如最長連續上升子序列、01值問題中連續的0段或者非0段(在具體問題中就是連續的空閑段或者占用段)等,可以在每個結點上維護三個域:lS、rS和S,分別表示該結點區間左端(從區間的最左端開始的)具有這種性質的最長連續序列的長度,該結點區間右端(到區間的最右端結束的)具有這種性質的最長連續序列的長度和該區間內具有這種性質的最長連續序列的長度(也就是要求的那個東東),則維護方法為“父結點lS=左子結點lS(左子結點lS<左子結點len)或左子結點len+右子結點lS(左子結點lS=左子結點len),父結點rS類似,父結點S=max{左子結點S,右子結點S,左子結點rS+右子結點lS}”,此外,由于要求的這個區間可能被拆成多個連續的結點區間,因此需要按順序合并這些區間,合并的方法是:設立S0和S1,S0表示不保證能延伸下去的區間長度,S0=上一個區間的S1+本區間的lS;S1表示可以延伸下去的區間長度,S1=上一個區間的S1+本區間len(如果本區間整個都是滿足條件的,即S=len)或本區間的rS(本區間不都是滿足條件的,即S<len),取過程中所有S0和區間S的最大值即為結果。
            在HDU2871中,應用左右歸中的方法維護的信息就是“最長連續空閑段”的長度,New操作需要;

            (2)調整邊界型:
            考慮這樣一個問題:現在要插入、刪除一些[0, 100000)的整數,并且在此過程中不斷詢問第K小的整數是多少,怎么辦?平衡樹可以實現,但線段樹顯然是更好的方法。對每個結點,存儲一個K0值表示位于該結點區間內的整數的個數,則查找第K小的時候只需要不斷執行以下操作:Kth(A, K),表示在結點A代表的區間內找第K小的,然后,若K<=結點A的左子結點K0值,則執行Kth(A的左子結點, K),否則執行Kth(A的右子結點, K-A左子結點的K0)(這和平衡樹神似),直到找到葉結點為止。這種方法稱為“調整邊界型”,即隨著結點深入,不斷縮小(自頂向下)或擴大(自底向上)范圍,最后找到結果。像找第K小這樣的操作屬于自頂向下型,而像“找到X所在的具有某種性質的最長的連續區間”就屬于自底向上型(注意和本題的Free不一樣);

            (3)標記輔助型:
            這種維護信息的方法,特點是利用標記來維護信息,即對于某些結點(主要是葉結點,因為其標記不再下放),直接使用標記來得到一些數據,比如對于HDU2871這一題,其中對于葉結點位于的插入線段的標號,使用的就是標記。

            下面是本題4個操作的具體實現:
            <1>線段樹結點定義:
            本題需要兩棵線段樹,這是因為New與Free操作對于tot域(插入線段左端點的總個數)會造成不同的影響,具體來說,在New操作中,tot值不會被同時加上(需要另外一個操作加上),然而在Free操作中,tot值會被同時清空,這樣就會導致在對某個結點清空(該結點包含在某個Free操作要清空的線段中)并打上0標記之后,如果緊接著又插入一條包含這個結點區間的新線段,則這個結點的0標記就會喪失,這樣在緊接著下傳的時候,其子結點的tot值就不會被清空(事實上已經被清空了)。所以,將tot域徹底轉移到另一棵線段樹里。
            struct node {
                
            int len, mr, lsc, rsc, sc;
            } T[MAXN 
            << 2];
            struct node0 {
                
            int tot;
                
            bool mr;
            } T0[MAXN 
            << 2];
            其中lsc、rsc、sc就是連續空閑段的長度(用左右歸中的方法維護),mr是一個整體賦值標記,在T中,mr的值若為-1表示無標記(未被整體賦值),若為0表示被整體清空,若大于0表示整體被一條線段覆蓋,mr值就是這條線段的編號(為區分不同線段,這里按照輸入順序對每一條New操作插入的線段以1、2……編號),在T0中,mr=1表示在Reset中被整體清空,mr=0表示無標記。
            <2>操作處理:
            1)Reset操作:將T、T0的根結點打上清空標記即可;
            2)New:涉及到兩個操作,分別是找最左的長度為x的連續空閑段,以及插入一條線段。對于前者,可以根據lsc、rsc、sc的值,按照“先左子結點,再跨越兩個子結點,最后右子結點”的順序求得(詳見代碼);對于后者就不用說了,太容易實現了(注意標記的處理以及upd,另外要插入一個tot);
            3)Free:也涉及兩個操作,分別是找一個點x所在的線段(插入過的線段)長度以及刪除一條線段,對于前者可將New插入過的所有線段的左右端點預存起來,然后找到代表區間[x, x]的結點的mr值(也就是結點x被編號為神馬的線段覆蓋),再在預存的線段中找到即可。對于后者,直接清空即可(不要在T0中打標記,而要單獨刪除一個tot);
            4)Get:直接利用T0中的tot找到第K小的值即可;

            代碼

            posted @ 2011-12-18 08:58 Mato_No1 閱讀(1150) | 評論 (0)編輯 收藏

            有一類動態規劃(其中也包含遞推)問題,要求滿足一些限制條件的字符串,這些限制條件是“需要含有某個子串”或“不能含有某個子串”,那么KMP、AC自動機等就有大用了。

            【例1】HDU3689
            題意:字符集中有一些字符,給出每個字符的出現概率(它們的和保證為1),再給出一個子串B,求:任給一個長度為N的字符串A(只能包含字符集中的字符),使得S是A的子串的概率。

            求解這類問題首先要進行補集轉化。因為子串可能有重疊(比如"ababa"中就出現了兩個"aba"),所以先轉化為“求任給一個長度為N的字符串A(只能包含字符集中的字符),使得B不是A的子串的概率”,然后再用1減去這個概率即為結果。
            設F[i][j]為“在所有長度為i的不出現B的字符串中,后綴與B的前綴匹配長度為j(即該字符串的后綴與B的前綴的最大匹配長度為j)的概率”,很顯然,F是由遞推得到了,關鍵是如何進行狀態轉移?或者說,在遞推過程中,哪些狀態可能成為F[i][j]的前趨狀態?
            假設F[i-1][k]是F[i][j]的前趨狀態,也就是說,在字符集中至少存在一個字符c,使得主串的第i位(最后一位)取c時,能夠從F[i-1][k]轉移到F[i][j]。這就需要求一個值S[k][c],表示當主串的后綴與B的前綴的(最大)匹配長度為k時,在主串后再加上一個字符c,其匹配長度會變成什么。舉例:設目前主串A'="abasab",B="asabs",其匹配長度為4,若在A'后加上一個字符's',則匹配長度變為5,所以S[4]['s']=5,而若在A'后加上一個字符'a',則匹配長度會變成1,所以S[4]['a']=1。顯然S值和A前面的哪些字符是沒有關系的。
            那么這個S值如何計算?其實可以發現,S和KMP算法中的nx數組神似,因此完全可以按照計算nx數組的辦法來計算S。具體來說,先要對B作KMP自身匹配,求出其nx數組,然后,在求S[k][c]的時候,嘗試在B的第k位(由于B的下標從0開始所以B[k-1])后加上字符c,看看會“回退”到哪里即可。代碼:
                 int j = 0; nx[0= 0;
                 re2(i, 
            1, m) {
                        
            while (j && A[i] != A[j]) j = nx[j - 1];
                        
            if (A[i] == A[j]) j++;
                        nx[i] 
            = j;
                 }
                 re(i, m) re(k, SZ) {
                       j 
            = i;
                       
            while (j && A[j] != k + 97) j = nx[j - 1];
                       
            if (A[j] == k + 97) S[i][k] = ++j; else S[i][k] = 0;
                 }
            這里m是B的長度。注意,當i=m時,S[i][j]是無意義的,因為前面已經說過了不能出現B。
            在求出S值后就能求出F值了。對于狀態F[i][j],若存在一個字符c使得x=S[i][c](滿足0<=x<m),則F[i][j]是F[i+1][x]的前趨狀態。當然,由于本題是求概率而不是求總數,且每個字符出現的概率還不一樣,所以轉移的時候,應是將F[i+1][x]加上F[i][j]*P[c](P[c]是字符c出現的概率),邊界:F[0][0]=1,F[0][1..m-1]均為0。
            最終結果為1-∑F[N][0..m-1]。

            代碼

            【例2】PKU1625URAL1158
            題意:給出一些子串,求長度為N,各個字符都屬于給定的字符集的所有字符串中,不包含任何一個給出的子串的字符串個數(需要使用壓9位的高精度)。

            本題顯然是【例1】的多子串形式,而用來解決多個字符串同時匹配的只有AC自動機,那么如何在本題中使用AC自動機求解呢?
            觀察【例1】中的F[i][j],可以想象一下,一個圖中有m個頂點,分別表示匹配長度為0..(m-1),然后不斷新加入的字符讓這些狀態在這些結點間不斷轉移(狀態轉移就是圖中的邊),這樣,F[i][j]就表示“階段i到達結點j上”。而AC自動機是基于Trie(樹)的,其中有現成的結點,這就揭示了本題的狀
            F[i][j]長度為i的合法的字符串(就是滿足字符集限制且不包含任何一個給定子串)中,在匹配到最后一位(第i位)后,剛好到達結點j的字符串的個數
            同樣,S[k][c]表示“目前到達結點k,接下來的一個字符是c的時候,會到達哪個結點。在對所有的子串建立了自動機之后,S值只要類似地搞就能求出來了。然后F的轉移也就搞定了。
            不過,本題要萬分注意AC自動機的一個BUG:在建立了自動機以后,需要把所有本身不危險(如果一個結點代表的字符串剛好是某一個給出的不能出現的子串,則該結點是危險結點),但通過失敗指針不斷上溯能夠到達一個危險結點的結點,也標記為危險結點,比如兩個子串是"abcde"和"bc",則代表"abcd"的那個結點由于包含了"bc"所以也是危險的。
            此外,本題的輸入要注意,字符集的ASCII碼范圍是-128~127,所以必須用char而不是unsigned char,且由于可能包含空格所以必須用gets()而不是scanf()輸入,又因為C/C++中木有負數下標,因此在輸入之后還要轉化一下(加128)。

            代碼

            【例3】PKU3691
            題意:給出一些子串和一個字符串A(其每個字符均屬于字符集{'A', 'C', 'G', 'T'}),求至少要改動A的幾個字符(不能改成不屬于字符集的字符),使得它不包含任何一個給出的子串,若不管怎么改都不行,則結果為-1。

            這就是真正的DP了。設F[i][j]為前i位,到達的結點為j,最少改動的字符個數,則轉移方程為
            F[i][j] = min{F[i-1][x] + (A[i] != c)},c∈{'A', 'C', 'G', 'T'},S[x][c]=j。邊界:F[0][root]=0,其余的F[0][]=+∞,A的實際下標從1開始。
            求S數組的方法見【例2】

            代碼

            【例4】PKU3208
            題意:含有連續的三個數字6的正整數,稱為"beastly number",求第P個(1<=P<=50000000)"beastly number"(其位數不會超過15位)。
            (這題是本沙茶在PKU上至今為止,自己想出算法的AC人數最少的題)
            本題其實是用不著KMP的,因為"666"這樣簡單的子串……

            思路:由于位數不會超過15位(后來發現最多只有10位),所以每個"beastly number"都可以看成一個長度為15,字符集為['0'..'9']的字符串(注意是可以有前導0的,因為位數可能不足15位)A,整個過程也就是從高位(第0位)向低位(第14位)求出A的各位。

            預處理:求出F[i][j],表示若A的前i位已經確定(其中不含"666",準確來說是非末尾不含"666"),且前i位的末尾剛好有j個'6'(j的范圍是0到3)時,有多少個"beastly number"(注意,前i位既然已經確定,就不可更改了,能夠決定的只有第i位的后面)。
            顯然先要求出F0[i][j]表示有多少個不是"beastly number"。其遞推方程不好寫,見代碼(其實也是很好理解的)。然后F[i][j]=1014-i - F0[i][j]。

            然后就是不斷調整邊界來構造了。準確來說,設前i-1位已經確定,現在要確定第i位,則枚舉第i位是0~9中的哪個值,然后求出滿足條件的最小的"beastly number"和最大的"beastly number"的名次(注意,名次是從1開始的),看看P在不在其中,這樣就能確定了。嚴重注意:如果已確定的位數中已經出現了"666",接下來的就不用枚舉了,直接在后面接上P-L就行了,L為左邊界。

            但是,為什么要把本題放在KMP的專題里面呢囧?因為如果這個子串不是"666"而是一些結構復雜的東東比如"123131"這樣的,只有借助KMP算法了。這時,F[i][j]就表示 A的前i位已經確定(非末尾不含這個子串),且其后綴與這個子串的前綴匹配長度為j,有多少個"beastly number" ,轉移方程與前幾個例子類似。

            代碼

            總結:
            KMP算法和AC自動機的狀態轉移性質決定了它們在字符串匹配類DP問題中的巨大作用。在實際應用中,要注意靈活使用它們。此外,AC自動機的那個BUG是一定要注意的。

            posted @ 2011-10-30 11:22 Mato_No1 閱讀(2379) | 評論 (0)編輯 收藏

            【后綴數組真難懂啊啊……就20+行的代碼搞了幾天才理解……不知是不是我太沙茶了】

            【1】一些定義:
            字符串:廣義的字符串是指“元素類型有序,且元素值有一定范圍的序列”,其元素不一定非要是字符,可以是數字等,因此整數、二進制數等也是字符串;
            字符集:字符串的元素值的范圍稱為字符集,其大小記為SZ。
            字符串的長度:字符串中元素的個數,一般記為N,長度為N的字符串A第一次提到時一般用A[0..N-1]來表示;
            前綴:字符串A[0..N-1]的從A[0]開始的若干個連續的字符組成的字符串稱為A的前綴,以下“前綴i”或者“編號為i的前綴”指的都是A[0..i];
            后綴:字符串A[0..N-1]的到A[N-1]終止的若干個連續的字符組成的字符串稱為A的后綴,以下“后綴i”或者“編號為i的后綴”指的都是A[i..N-1];

            對于一個長度為N的字符串,將其N個后綴按字典序大小進行排序,得到兩個數組sa[i]和rank[i],sa[i]為排在第i位的后綴的編號(也就是一般說的ord[i]),rank[i]為排在后綴i排在的位置(稱為后綴i的名次)。sa、rank值的范圍均為[0..N-1]。sa和rank互逆,即sa[i]=j等價于rank[j]=i,或者說成sa[rank[i]]=rank[sa[i]]=i。這里,sa稱為后綴數組,rank稱為名次數組

            【2】用倍增算法求后綴數組:
            在論文里,后綴數組有兩種求法:倍增算法和DC3算法,前者的時間復雜度為O(NlogN),但常數較小,后者的時間復雜度為O(N),但常數較大,在實際應用中,兩者的總時間相差不大,且后者比前者難理解得多(本沙茶理解前者都用了幾天時間……后者就木敢看了)。這里就總結一下倍增算法吧囧……
            首先,貼一下本沙茶的用倍增算法求后綴數組的模板:
            void suffix_array()
            {
                
            int p, v0, v1, v00, v01;
                re(i, SZ) S[i] 
            = 0;
                re(i, n) rank[i] 
            = A[i];
                re(i, n) S[A[i]]
            ++;
                re2(i, 
            1, SZ) S[i] += S[i - 1];
                rre(i, n) sa[
            --S[A[i]]] = i;
                
            for (int j=1; j<n; j<<=1) {
                    p 
            = 0; re2(i, n-j, n) tmp[p++= i;
                    re(i, n) 
            if (sa[i] >= j) tmp[p++= sa[i] - j;
                    re(i, SZ) S[i] 
            = 0;
                    re(i, n) S[rank[i]]
            ++;
                    re2(i, 
            1, SZ) S[i] += S[i - 1];
                    rre(i, n) sa[
            --S[rank[tmp[i]]]] = tmp[i];
                    tmp[sa[
            0]] = p = 0;
                    re2(i, 
            1, n) {
                        v0 
            = sa[i - 1]; v1 = sa[i];
                        
            if (v0 + j < n) v00 = rank[v0 + j]; else v00 = -1;
                        
            if (v1 + j < n) v01 = rank[v1 + j]; else v01 = -1;
                        
            if (rank[v0] == rank[v1] && v00 == v01) tmp[sa[i]] = p; else tmp[sa[i]] = ++p;
                    }
                    re(i, n) rank[i] 
            = tmp[i];
                    SZ 
            = ++p;
                }
            }
            這里A是待求sa和rank的字符串。

            <1>倍增算法的思想:
            記R[i][j]為A[i..i+2j-1](如果越界,則后面用@填充)在A的所有長度為2j的子串(越界則后面用@填充)中的名次(rank)值。倍增算法就是按階段求出所有R[i][j]的值,直到2j>N為止。首先,R[i][0]的就是字符A[i]在A[0..N-1]中的名次,是可以直接用計數排序來實現的。然后,若R[0..N-1][j-1]已知,則可以按照以下方法求出R[0..N-1][j]的值:對每個i(0<=i<N),構造一個二元組<Xi, Yi>,其中Xi=R[i][j-1],Yi=R[i+2j][j-1](若i+2j>=N,則Yi=-∞),然后對這N個二元組按照第一關鍵字為X,第二關鍵字為Y(若兩者都相等則判定為相等)進行排序(可以用基數排序來實現),排序后,<Xi, Yi>的名次就是的R[i][j]的值。

            <2>一開始,對A中的各個字符進行計數排序:
            re(i, SZ) S[i] = 0;
            re(i, n) rank[i] 
            = A[i];
            re(i, n) S[A[i]]
            ++;
            re2(i, 
            1, SZ) S[i] += S[i - 1];
            rre(i, n) sa[
            --S[A[i]]] = i;
            這個木有神馬好說的,在搞懂了基數排序之后可以秒掉。唯一不同的是這里加了一句:rank[i]=A[i],這里的rank[i]是初始的i的名次,MS不符合rank[i]的定義和sa與rank間的互逆性。這里就要解釋一下了囧。因為在求sa的過程中,rank值可能不符合定義,因為長度為2j的子串可能會有相等的,此時它們的rank值也要相等,而sa值由于有下標的限制所以不可能有相等的。因此,在過程中,rank其實是用來代替A的子串的,這樣rank值只需要表示一個“相對順序”就行了,也就是:rank[i0]>(=, <)rank[i1],當且僅當A[i0..i0+2j-1]>(=, <)A[i1..i1+2j-1]。這樣,可以直接將A[i]值作為初始的rank[i]值。

            <3>j(代替2j)的值從1開始不斷倍增,對二元組進行基數排序求出新階段的sa值:
            for (int j=1; j<n; j<<=1) {
                p 
            = 0; re2(i, n-j, n) tmp[p++= i;
                re(i, n) 
            if (sa[i] >= j) tmp[p++= sa[i] - j;
                re(i, SZ) S[i] 
            = 0;
                re(i, n) S[rank[i]]
            ++;
                re2(i, 
            1, SZ) S[i] += S[i - 1];
                rre(i, n) sa[
            --S[rank[tmp[i]]]] = tmp[i];
            注意這個基數排序的過程是很特別的。首先,它并不是對A在進行排序,而是對上一階段求出的rank在進行排序。因為前面已經說過,在求sa的過程中,rank就是用來代替A的對應長度的子串的,由于不能直接對子串進行排序(那樣的話時間開銷很恐怖的),所以只能對rank進行排序。另外,這里在對二元組<x, y>的第二關鍵字(y)進行排序的過程中加了優化:這些y其實就是把上一階段的sa整體左移了j,右邊空出的部分全部用@(空串)填充得到的,由于空串的字典序肯定最小,因此將右邊的空串按照下標順序先寫入臨時sa(代碼中用tmp表示的就是臨時sa,也就是對第二關鍵字y排序后的ord結果),然后,上一階段的sa如果左移后還木有消失的(也就是sa值大于等于j的),再按順序寫入臨時sa,就得到了排序結果。剩下的對x的排序結果就是上一階段的sa,唯一不同的是對于x相同的,按照臨時名次遞增的順序。

            <4>求出新階段的rank值:
            tmp[sa[0]] = p = 0;
            re2(i, 
            1, n) {
                v0 
            = sa[i - 1]; v1 = sa[i];
                
            if (v0 + j < n) v00 = rank[v0 + j]; else v00 = -1;
                
            if (v1 + j < n) v01 = rank[v1 + j]; else v01 = -1;
                
            if (rank[v0] == rank[v1] && v00 == v01) tmp[sa[i]] = p; else tmp[sa[i]] = ++p;
            }
            re(i, n) rank[i] 
            = tmp[i];
            SZ 
            = ++p;
            由于下一階段需要使用本階段的rank值,因此在求出了本階段的sa值以后,需要求rank值。(代碼中的tmp起了臨時rank的作用,目的是節省空間)
            因為sa值已經求出,因此只要依次掃描sa就可以得到rank值,唯一要做的工作就是找到哪些子串是相等的,它們的rank值應該相等,除此之外,rank值只要依次加1即可。判定相等的方法:只需判定rank[i]和rank[i+j]是否都對應相等即可。若rank[i+j]越界,用-∞(當然任何一個負數都行,代碼中用了-1)來表示。
            最后還有一個優化:由于本階段的名次的范圍只有[0..p]這么多,下一階段的“字符集”(其實就是rank集)的大小SZ可以設為p+1,這樣可以省一些時間。

            這樣后綴數組sa和名次數組rank就全部求完了。

            以后還有一些更重要的東東就是AC自動機、后綴數組等的應用問題,算了,以后再搞吧囧。

            posted @ 2011-10-23 16:51 Mato_No1 閱讀(2851) | 評論 (2)編輯 收藏

            (神犇看到這個不要鄙視啊啊……饒了本沙茶啊啊……)

            剛才本沙茶在看后綴數組(還木有看完)的時候,里面的那個基數排序寫法各種看不懂……

            后來到網上一看才發現這是一種極其神犇的基數排序算法,它不需要任何鏈式或類鏈式結構,也不需要在每次排序后都把所有元素按照本次排序的結果重新換位置(其實這樣是相當耗時間的,尤其是字符串排序的時候,因為復制一個字符串的時間復雜度取決于它的長度),只需要存儲每個元素在每次排序之后的“相對下標”即可。

            【1】先考慮這樣一個問題:對一個含N個元素,每個元素值的范圍為[0..SZ-1]的整數序列進行計數排序(第一關鍵字為字符,第二關鍵字為下標),并且在排序后求出ord[0..N-1]數組:ord[i]表示排在第i位的元素在排序前的下標。要求這個算法中不能使用任何鏈式或類鏈式結構(鏈表、Dancing Links、vector等)。

            設立數組S,S[i]表示值為i的元素的個數。求S[i]可在O(N)時間內求出(一般地,還要用O(SZ)的時間進行初始化)。再設立數組S0(在實現的時候,可以直接用S來存儲S0的值),S0[i]=∑S[0..i](也就是值不大于i的元素的個數),求S0只需要在S的基礎上用O(SZ)時間即可。然后可以得到一個重要的性質:對于任意i(0<=i<SZ),原序列中的所有值為i的元素,按照其下標從小到大,其名次(或者說是序號,排序前下標為i的元素的名次記為rank[i],下同)依次為S0[i-1]..S0[i]-1(i=0時,為0..S0[i]-1,這里認為名次從0開始),這樣,只要在求出S0以后用O(N)時間掃描一遍原序列,每掃描到一個值為i的元素,立刻可以從S0中獲得其名次,同時將S0[i]的值加1,掃描完后所有的元素以后即得出了rank[0..N-1]。然后,ord與rank其實是互逆的(因為ord[i]=j等價于rank[j]=i),因此求出了rank以后可以在O(N)時間內求出ord(當然,根據ord[rank[i]]=i這一性質,可以在掃描過程中不求rank而直接求ord)。不過,在掃描這一步,更好的方法是倒序掃描,這樣在掃描到一個值為i的元素之后,可以不動用S[i-1](這個當i=0時還要特判,比較麻煩),直接調用S[i]的值(當然要先將S[i]減1),就是該元素的名次了。
            很明顯,該算法的時間復雜度為O(2SZ+2N)(如果不求rank直接求ord的話)=O(N),且唯一用到的輔助空間就是S(前面已經說過了,直接在S上存儲S0,不單獨設S0),故空間復雜度也是線性的,沒有用到鏈式或類鏈式結構。

            【2】然后來解決基數排序的問題。假設這里是對字符串進行基數排序(注意,各元素的長度可能不相等,此時排序的總位數L應取所有元素長度的最大值,且排序過程中遇到不足位數的元素要特判:對該元素的該位記為@,@是一個比字符集中的所有字符都小的字符,其在字符集中名次為0,所有實際在字符集中的元素的名次為1..SZ,這樣總的字符個數就是SZ+1,在寫代碼的時候要注意),從最后一位(第L-1位)開始一位一位排序,直至第0位,中間每次排序的過程實質上就是像【1】這樣的計數排序。

            按照本沙茶以前的方法,每進行一位的排序后就要將所有元素重新調換位置(也就是構造一個新的序列代替原來的序列,原序列的下標為i的元素在新序列中下標為rank[i]),但這樣的時間開銷很大(前面已經說過了)。更好的方法是在整個排序過程中,序列的各個元素的下標都不變,但每位排序后,序列有一個“相對下標”,相對下標為i的元素就是此次排序中排在第i位的元素(即ord[i]),這樣在每次排序時,只要操縱各元素的上一位相對下標即可(在求S的時候不用相對下標,因為S的值是由個數決定的,與順序無關,但是在求本位的ord的時候,倒序掃描序列其實是倒序掃描相對下標的序列,即掃描到下標為i,實際指的是相對下標為i,即ord[i]),注意一開始所有元素的相對下標與下標相等,即ord[i]=i。這樣就不需要調換位置了,只需要ord就行了。

            最后總時間復雜度顯然是O(NL)的。

            代碼(對于字符串進行基數排序,這里假設字符串只含小寫字母,注意這里的SZ是27而不是26,因為有@的存在):
            #include <iostream>
            #include 
            <stdio.h>
            #include 
            <string.h>
            using namespace std;
            #define re(i, n) for (int i=0; i<n; i++)
            #define re2(i, l, r) for (int i=l; i<r; i++)
            #define rre(i, n) for (int i=n-1; i>=0; i--)
            const int MAXN = 100000, MAXLEN = 101, SZ = 27;
            int n, L0[MAXN], ord[MAXN], tmp[MAXN], S[SZ];
            char A[MAXN][MAXLEN];
            void init()
            {
                scanf(
            "%d"&n);
                re(i, n) scanf(
            "%s", A[i]);
            }
            void solve()
            {
                
            int L = 0;
                re(i, n) {
                    L0[i] 
            = strlen(A[i]); ord[i] = i;
                    
            if (L0[i] > L) L = L0[i];
                }
                rre(j, L) {
                    re(i, SZ) S[i] 
            = 0;
                    re(i, n) S[L0[i] 
            > j ? A[i][j] - 96 : 0]++;
                    re2(i, 
            1, SZ) S[i] += S[i - 1];
                    rre(i, n) tmp[
            --S[L0[ord[i]] > j ? A[ord[i]][j] - 96 : 0]] = ord[i];
                    re(i, n) ord[i] 
            = tmp[i];
                }
            }
            void pri()
            {
                re(i, n) puts(A[ord[i]]);
            }
            int main()
            {
                init();
                solve();
                pri();
                
            return 0;
            }
            最后,Orz一下這個無比神犇的算法!!!

            posted @ 2011-10-19 21:59 Mato_No1 閱讀(652) | 評論 (1)編輯 收藏

            具體題目見HDU2222,其實就是一個裸的多串匹配的問題(給出一個主串和N個子串,求出幾個子串在主串中出現過)。

            我真是太沙茶了……這么水的題目調了N久,找了N位神犇幫我看代碼,最終才找出來BUG……

            易疵點:
            (1)本題的子串是可以相同的,此時Trie的每個結點要設一個mul值,表示該結點對應的字符串在所有子串中重復的次數,另外,不要為了省空間把mul定義成char型,有可能所有的字符串全相同,因此需要定義成int(事實證明不會爆空間),這是本沙茶被折磨了這么久的主要原因
            (2)Trie采用靜態存儲,0號結點作為空結點(NULL),因此真正的結點編號從1開始,另外root一般都是1號結點;
            (3)注意在建立自動機以及匹配的時候,所有要沿fail上溯的地方,其邊界都是0(NULL,注意不是root)或者找到一個有對應子結點的結點。注意到0還沒有找到的處理方法:在建立自動機的時候,將T[j]置為root;在匹配的時候,將x置為root;

            代碼(模板)(那些標了Attention的地方都是易疵的):
            #include <iostream>
            #include 
            <stdio.h>
            #include 
            <string>
            using namespace std;
            using std::string;
            #define re(i, n) for (int i=0; i<n; i++)
            #define root 1
            const int MAXN = 500001, MAXLEN = 1000001, SZ = 26, INF = ~0U >> 2;
            struct node {
                
            int mul, ch[SZ], fail;    //Attention
            } T[MAXN];
            int N, Q[MAXN], res;
            string s0, A;
            char tmp[MAXLEN], tmp0[51];
            void ins()
            {
                
            int len = s0.length(), x = root, c;
                re(i, len) {
                    c 
            = s0[i] - 97;
                    
            if (!T[x].ch[c]) {T[x].ch[c] = ++N; T[N].mul = 0; re(j, SZ) T[N].ch[j] = 0;}
                    x 
            = T[x].ch[c];
                }
                T[x].mul
            ++;
            }
            void mkf()
            {
                Q[
            0= root; T[root].fail = 0;
                
            int i, j, x;
                
            for (int front=0, rear=0; front<=rear; front++) {
                    i 
            = Q[front];
                    re(k, SZ) 
            if (j = T[i].ch[k]) {
                        x 
            = T[i].fail;
                        
            while (x && !T[x].ch[k]) x = T[x].fail;        //Attention
                        if (x) T[j].fail = T[x].ch[k]; else T[j].fail = root;    //Attention
                        Q[++rear] = j;
                    }
                }
            }
            void solve()
            {
                
            int len = A.length(), x = root, y, c; res = 0;
                re(i, len) {
                    c 
            = A[i] - 97;
                    
            while (x && !T[x].ch[c]) x = T[x].fail;    //Attention
                    if (!x) x = root; else x = T[x].ch[c];    //Attention
                    y = x;
                    
            while (y) {res += T[y].mul; T[y].mul = 0; y = T[y].fail;}      //Attention
                }
            }
            int main()
            {
                
            int tests, n;
                scanf(
            "%d"&tests);
                re(testno, tests) {
                    N 
            = 1; T[root].mul = 0; re(i, SZ) T[root].ch[i] = 0;
                    scanf(
            "%d"&n); getchar();
                    re(i, n) {
                        gets(tmp0);
                        s0 
            = tmp0;
                        ins();
                    }
                    gets(tmp);
                    A 
            = tmp;
                    mkf();
                    solve();
                    printf(
            "%d\n", res);
                }
                
            return 0;
            }

            【2011年10月19日】今天發現了匹配過程中的一個可優化的地方:對于一個點x以及它的所有返回結點(這里把所有沿著x的失敗指針不斷上溯直到root路徑上的結點都稱為返回結點),由于不可重復計數,可以將它們的mul值置為原來mul值的相反數(-mul),而不是0,表示該結點已經統計過。這樣在下一次y的上溯過程中一旦發現一個mul值為負的點就不用繼續上溯了,因為上面的點一定也已經統計過了。
            當然,這僅限于單主串,如果是多主串則需要在每次匹配之前把Trie樹中所有結點的mul值(如果是負數的的話)全部重新取反。為了節省時間,可以在匹配過程中把所有統計過的(mul值改為負數的)結點全部放進一個輔助的隊列里,然后取反時只要處理隊列中的結點就行了。

            加入該優化后的代碼(solve部分):
            void solve()
            {
                
            int len = A.length(), x = root, y, c; res = 0;
                re(i, len) {
                    c 
            = A[i] - 97;
                    
            while (x && !T[x].ch[c]) x = T[x].fail;
                    
            if (!x) x = root; else x = T[x].ch[c];
                    y 
            = x;
                    
            while (y && T[y].mul >= 0) {res += T[y].mul; T[y].mul = -T[y].mul; y = T[y].fail;}
                }
            }

            下面是優化的實測結果(第一個為優化后的,第二個為優化前的),可以看出,該優化的力度很大。

            posted @ 2011-10-19 19:47 Mato_No1 閱讀(970) | 評論 (0)編輯 收藏

            AC自動機就是在Trie樹上加入一些失敗指針(fail,類似KMP中的next),使得它在某個結點失配的時候能夠轉移到該結點失敗指針指向的結點繼續匹配,從而實現多串匹配(單主串多子串)。

            【1】Trie樹的結構:
            首先是結點類型:
            struct node {
                
            int mul, ch[SZ], fail;
            } T[MAXN];
            其中SZ是字符集的大小,比如小寫字母集SZ=26,數字集SZ=10等。
            另外這個mul表示的是該結點的重復次數(和平衡樹中的比較像),就是這個結點所對應的字符串(從根到該結點路徑上的所有邊上的字符組成的字符串)出現了幾次。
            (不過這種表示法MS不是很好……如果有卡空間的,比如結點總數和SZ之積達到了100M以上,而真正的邊又很少的時候……就囧掉了……而如果用指針的話在Linux下面又會CE掉……各位神犇來說一下怎么解決啊囧……)
            然后,Trie樹的0號結點(T[0])是空結點(用來代替指針中的NULL),因此真正結點下標從1開始。1號結點(T[1])一般都作為根結點,所以下面直接寫#define root 1。
            還有(這句話是除草用的……)Trie樹的字符全部都在邊上而不在點上,因此T[x].ch[c]其實指的是“結點x引出的字符為c的邊所指向的結點”,簡稱結點x的c子結點。

            【2】自動機的建立:
            首先要建立Trie樹(也就是在空的Trie樹上插入所有要匹配的子串)。這個隨便搞一下就傻掉了,因此直接上代碼:
            void ins()
            {
                
            int len = s0.length(), x = root, c;
                re(i, len) {
                    c 
            = s0[i] - 97;
                    
            if (!T[x].ch[c]) {T[x].ch[c] = ++N; T[N].mul = 0; re(j, SZ) T[N].ch[j] = 0;}
                    x 
            = T[x].ch[c];
                }
                T[x].mul
            ++;
            }
            這里string s0是待插入的字符串,定義成全局變量,因為在參數中出現string有可能爆掉。

            然后就是建立自動機了。
            這一過程其實是用BFS遍歷來實現的。首先,T[root].fail=0(root也是整棵樹中唯一的fail為0的結點)并將root入隊。下面按照BFS的順序依次取出隊所有的點,對于結點i,若它存在k子結點j(也就是j=T[i].ch[k],且j≠0),則結點j入隊,并計算j的失敗指針fail,方法:從T[i].fail(不是i)開始不斷上溯,直到找到一個存在k子結點的結點或者到root都木有找到(結點下標為0,即NULL)。若找到了一個存在k子結點的結點x,則將T[j].fail置為T[x].ch[k],否則(直到root都木有找到),T[j].fail=root。

            到這里失敗指針的用處就顯現了:如果匹配到結點x的時候失配(即接下來的一個字符是c而T[x].ch[c]=0),可以立刻轉到T[x].fail進行匹配,因為T[x].fail有以下三個特征:
            <1>其深度嚴格小于x的深度;
            <2>其代表的字符串一定是x代表的字符串的后綴;
            <3>該結點一定是滿足條件<1><2>的所有結點中深度最小的結點;

            代碼:
            void mkf()
            {
                Q[
            0= root; T[root].fail = 0;
                
            int i, j, x;
                
            for (int front=0, rear=0; front<=rear; front++) {
                    i 
            = Q[front];
                    re(k, SZ) 
            if (j = T[i].ch[k]) {
                        x 
            = T[i].fail;
                        
            while (x && !T[x].ch[k]) x = T[x].fail;
                        
            if (x) T[j].fail = T[x].ch[k]; else T[j].fail = root;
                        Q[
            ++rear] = j;
                    }
                }
            }

            【3】匹配過程:
            在有了失敗指針時,其匹配過程就和KMP差不多了。
            設主串為A(代碼中同),依次掃描A[0..A.length()-1]進行匹配。設目前匹配到了第i位,A[i]=c,當前結點為x。匹配過程中將進行以下操作:
            <1>成功匹配(T[x]有c子結點),則進入T[x]的c子結點;
            <2>失配(T[x]無c子結點),則從T[x].fail開始,沿著失敗指針上溯,直到找到一個有c子結點的結點為止。如果到了root都木有找到這樣的結點,則停止在root處;
            <3>設立結點y,從當前的結點x開始不斷上溯到root(注意root也要算,因為子串中可能有空串),將中間所有結點的mul值累加進最終結果(表示這些字符串在主串中出現了,統計次數),如果題目中要求統計不重復的子串個數(如HDU2222),則在累加之后將它們全部置為0,防止下次再次累加。這一步操作實質上就是統計A中所有以A[i]為右端點的子串個數。

            這樣,整個過程就傻掉了。

            代碼:
            void solve()
            {
                
            int len = A.length(), x = root, y, c; res = 0;
                re(i, len) {
                    c 
            = A[i] - 97;
                    
            while (x && !T[x].ch[c]) x = T[x].fail;
                    
            if (!x) x = root; else x = T[x].ch[c];
                    y 
            = x;
                    
            while (y) {res += T[y].mul; T[y].mul = 0; y = T[y].fail;}
                }
            }

            有關例題:
            【1】HDU2222
            裸的多串匹配問題,模板題;
            有關該題的詳細資料(包括易疵點)見這里
            【2】HDU2896
            比2222稍難一些,但還是模板題。注意這題的子串木有重復的,因此mul可以設為bool。
            【3】HDU3065
            統計每個子串出現的次數(可以重復),也是模板題。
            以上三題均不卡空間。

            posted @ 2011-10-19 19:03 Mato_No1 閱讀(590) | 評論 (0)編輯 收藏

            【1】BZOJ1571
            DP題,寫起來比較繁瑣……
            首先轉移方程是不難想的囧……F[i][j],表示i時間后能力為j,
            然后要設一些輔助數組,G[i]表示F[i][1..MAXJ]的最大值,H2[i]表示能力不超過i的一次滑雪的最小時間(這個還要用一個H1[i]表示能力剛好為i的來輔助求出)……
            剩下的也就傻掉了,
            當然,WJMZBMR神犇用記憶化搜索……省去了一些計算量……有效縮短時間……Orz啊……
            (其實,如果大多數狀態都是無效狀態或者根本導不出最優解的狀態,可以用記憶化的……)
            代碼

            【2】BZOJ1572
            任務調度問題(貪心模型)的加強版,用堆優化囧……
            先把所有的任務按照結束時間遞減排序,然后掃描,對于當前任務A[i],結束時間為T[i],上一個任務A[i-1]的結束時間為T[i-1],設D=T[i-1]-T[i],則在堆中取出收益最大的D個任務(顯然該堆是以收益為關鍵字的大頂堆),用它們填上[T[i]+1, T[i-1]]這個時間段(原因很簡單,A[i]及以后的任務在T[i]時刻以前就結束了,不能插入到此段內,因此此段內只能插入A[i-1]及其以前的,也就是在堆中的任務),若堆中的任務數<D,則全部取出,進行完這一步后,再將A[i]插入到堆中即可。
            總時間復雜度:O(NlogN);
            代碼

            【3】BZOJ1574
            很容易想到最小點割(怎么看怎么像囧),但它和最小點割又不一樣,因為本題是求T部分點數最少的點割……
            正解仍然是貪心。對于每個報告點,由于它沒壞且到1沒有只經過未壞點的路徑,所以與它相鄰的所有的點要么是壞點,要么到1也沒有路徑,因此可以認為它們都是壞點(在最優方案中一定是這樣),這樣標記出所有的壞點以后,從1開始做一次遍歷(只經過未壞點的),最終結果就是遍歷到的點數;
            代碼

            【4】BZOJ1575
            裸的DP題啊啊……關鍵是本沙茶WA了N次還用暴搜代碼來對拍啊啊……被折磨死了啊啊……
            簡單講一下易疵點:
            <1>不可把兩邊都加上一個0來簡化,因為前兩條(處理兩邊的)規則和加上0之后的并不等價;
            <2>注意邊界點(i=0或j=1時)的情況;
            <3>注意最終結果,要在F[0..N-1]中找最小的合法的j而不是只在F[N-1]中找;
            代碼

            posted @ 2011-10-05 09:42 Mato_No1 閱讀(1624) | 評論 (0)編輯 收藏

            僅列出標題
            共12頁: First 3 4 5 6 7 8 9 10 11 Last 
            麻豆久久久9性大片| 国产精品99久久99久久久| 国产日产久久高清欧美一区| 精品国产乱码久久久久久呢| 久久婷婷五月综合97色直播| 国产精品亚洲美女久久久| 中文字幕成人精品久久不卡| 中文字幕亚洲综合久久2| 久久精品一区二区| 久久婷婷国产麻豆91天堂| 久久精品国产福利国产秒| 狠狠干狠狠久久| 色综合色天天久久婷婷基地| 精品久久久无码中文字幕| 国产精品无码久久综合网| 久久99精品久久久久久不卡| 亚洲欧美精品一区久久中文字幕| 一级a性色生活片久久无少妇一级婬片免费放| 久久久噜噜噜久久| 精品久久久久久久国产潘金莲| 精品国产99久久久久久麻豆 | 新狼窝色AV性久久久久久| 久久久久亚洲AV成人网人人网站| 亚洲狠狠婷婷综合久久久久| 久久AV高清无码| 久久精品国产一区二区三区不卡| 一本大道久久香蕉成人网| 少妇高潮惨叫久久久久久 | 久久99精品国产99久久6男男| 91久久福利国产成人精品| 亚洲另类欧美综合久久图片区| 久久精品天天中文字幕人妻| 久久国产免费观看精品| 欧美亚洲国产精品久久高清 | 国内精品久久久久| 亚洲第一永久AV网站久久精品男人的天堂AV | 中文成人久久久久影院免费观看| 久久久久亚洲AV片无码下载蜜桃| 久久国产香蕉视频| 99久久99久久| 久久人人添人人爽添人人片牛牛|