1. 程式人生 > >SPOJ LCS 最長公共子串 字尾自動機&字尾樹(Ukkonen)

SPOJ LCS 最長公共子串 字尾自動機&字尾樹(Ukkonen)

  終於搞清楚了這兩個噁心的演算法。其實字尾樹也不難寫嘛。

題目

  給定兩個字串a和b,求在a和b中都有出現的連續子串的最長長度。

樣例輸入

alsdfkjfjkdsal
fdjskalajfkdsla

樣例輸出

3

做法1

  使用字尾自動機。clj的課件講得很詳細了,這裡不細說。主要說幾件事:

  1. 字尾自動機的狀態的本質是right集合(見課件),parent意味著right集合的最小擴充。時刻記著這一點可以使很多性質的證明變得很直觀。
  2. 有一個沒什麼用但挺有意思的性質是,字串s的字尾自動機的每個狀態和a的反串的字尾樹的狀態是一一對應的。原因其實很簡單,字尾自動機的每個狀態代表一系列子串,這些子串在s中出現的結束位置(right集合)是一樣的。字尾樹同樣如此,每個節點連同它指向父親的邊一起代表一系列子串,而邊上的串能夠被壓縮成一條邊正是因為中間不會分叉,即出現某個字首的地方也一定會出現另一個字首,即它們在s中出現的起始位置(可以叫left集合)是一樣的。字尾自動機和字尾樹的節點的本質都可以認為是“同生共死的子串”。
  3. 有關字尾自動機的線性性質。首先點和非空邊的個數都是O(n)的,證明詳見clj課件。其次構建自動機的效率也是O(n)的:考慮追加一個字元c需要的兩個迴圈,第一個迴圈使得一些沒有c兒子的點有了c兒子,第二個迴圈使得一些有c兒子的點更新了c兒子。首先第一個迴圈的總次數顯然不超過邊數。第二個迴圈的次數其實也不超過邊數,因為每個點的每個兒子最多被更新一次,這是因為每次被更新的點都是之前所有有c兒子的點之中right集合最小的點,而只有當這個right集合可以更小時才會進行這種更新。有點繞,但如果能腦補出一個過程動畫的話,其實挺直觀的。

      現在看這道題。如果能對每個j都能求出最大的len使得b[j-len+1..j]在a中出現,那麼所有的len的最大值就是答案。
      首先建立字串a的字尾自動機,之後向其中依次輸入b的每個字元。若能夠輸入,則++len;否則意味著當前狀態代表的一系列子串後面都沒有所需字元,此時需要不斷嘗試縮短len來擴充子串集合,直到新加入的子串後面有了所需字元為止。注意自動機的每個狀態代表的是a的子串,這個串可能很長,長到超出已匹配的長度,超出的這部分和b沒有關係,只有重疊的那部分(較小的那個長度)才可以用來更新答案。

// vim: set noet ts=4 sw=4 fileencoding=utf-8:
#include <cstdio>
#include <cstring>
#include <algorithm>
using std::max;
using std::min;
using std::copy;
using std::fill;

const int MAX_N = 250009;

struct Node {
    static Node buf[], *bufp;
    void *operator new(size_t) {return bufp++;}

    int
lim; Node *ch[26], *f; Node() = default; Node(int lim_): lim(lim_) { fill(ch, ch+26, nullptr); } } Node::buf[MAX_N*4], *Node::bufp=buf, *root, *last; void append(char c) { int x = c-'a'; Node *now=new Node(last->lim+1), *p; for (p=last; p && !p->ch[x]; p=p->f) p->ch[x] = now; if (!p) { now->f = root; } else { Node *q = p->ch[x]; if (q->lim == p->lim+1) { now->f = q; } else { Node *r = new Node(p->lim+1); copy(q->ch, q->ch+26, r->ch); r->f = q->f; q->f = now->f = r; for (; p && p->ch[x]==q; p=p->f) p->ch[x] = r; } } last = now; } char a[MAX_N], b[MAX_N]; int main() { scanf("%s%s", &a, &b); last = root = new Node(0); for (char *p=a; *p; ++p) append(*p); Node *now = root; int ans = 0, pref = 0; for (char *p=b; *p; ++p) { int x = *p-'a'; for (; now && !now->ch[x]; now=now->f) ; if (!now) { now = root; pref = 0; continue; } pref = min(pref, now->lim)+1; ans = max(ans, pref); now = now->ch[x]; } printf("%d\n", ans); }

做法2

  就是字尾樹啦。建立字串a…[email protected]…b#的字尾樹。每個節點到根的路徑代表一個子串。一個節點代表的子串同時在a和b中出現,當且僅當此節點的子樹的所有葉子裡既有開始於@之前也有開始於@之後的。

  字尾樹構建的演算法也不細說了,直接去看Ukkonen的原文就好。其實演算法的原理很簡單,首先後綴樹就是壓縮過的Trie,而在Trie上所有問題都是顯然的。在Trie上追加一個字元的步驟是追著字尾連結跑,在後綴樹上同樣如此,只不過因為我們只關心分叉而省掉了對葉子和不許要分叉的點的處理。這裡只說幾個困擾過我的地方:
1. 對於字尾樹的所有葉子,我們不關心也不記錄它們的字尾連結。葉子永遠是在隨著字串往後生長的。我們只關心分叉的點和進行分叉的過程。
2. 一般來說,在一顆合法的字尾樹上,分叉節點x的字尾連結指向的節點y也是分叉節點,這是因為所有x出現的地方,y都有出現。不過,在追加字串的過程中,字尾樹可能會臨時變得不完整、不合法。考慮字串[email protected]。在新增@之前,字尾樹只有一條邊,而在新增@時,我們需要知道分叉節點aaaa的字尾連結,而此時aaa還沒有分叉,由此衍生出一堆特殊情況。Ukkonen對此的處理方式是新建一個點作為根的字尾連結點,同時將它的所有兒子都賦成根節點。這是一個十分巧妙的處理,至少它可以利用root的字尾連結神奇地處理[email protected]這種情況。Ukkonen的論文還給出了這種處理的數學意義(單字元的逆),但我覺得這種解釋不如對上述例子的處理有說服力。

  最後說一句,只要用心構造程式碼,字尾樹真的不難寫。核心程式碼和字尾自動機一樣也就20多行。

// vim: set noet ts=4 sw=4 fileencoding=utf-8:
#include <cstdio>
#include <climits>
#include <cstring>
#include <algorithm>
using std::fill;
using std::max;

const int MAX_N = 250009;

struct Node {
    static Node buf[], *bufp;
    void *operator new(size_t) {return bufp++;}
    Node *link, *ch[28];
    int l, r, len;
    Node() = default;
    Node(int _l, int _r, int _len): l(_l), r(_r), len(_len), link(nullptr) {
        fill(ch, ch+28, nullptr);
    }
} Node::buf[MAX_N*4], *Node::bufp=buf, *root, *neg, *par;

char a[MAX_N*4];
inline int idx(int i) {return a[i]-'a';}
int span = 0;

void insert(int pos, char c) {
    Node *now, *last=nullptr, *next;
    int x = c-'a';
    for (;;par=par->link) {
        while ((now=par->ch[idx(pos-span)])
                && span>=now->r-now->l) {
            span -= now->r-now->l;
            par = now;
        }
        if (par==neg || (now && a[now->l+span]==c)) {
            if (last) last->link = par;
            ++span;
            return;
        }
        if (span>0) {
            next = new Node(now->l, now->l+span, par->len+span);
            now->l += span;
            next->ch[idx(now->l)] = now;
            par->ch[idx(next->l)] = next;
        } else next = par;
        next->ch[x] = new Node(pos, INT_MAX, 0);
        if (last) last->link = next;
        last = next;
    }
}

int main() {
    scanf("%s", &a);
    int len1=strlen(a), len2=len1;
    a[len1] = 'z'+1;
    scanf("%s", &a[++len2]);
    len2 += strlen(&a[len2]);
    a[len2++] = 'z'+2;
    a[len2] = '\0';

    par = root = new Node(-1, 0, 0);
    neg = root->link = new Node();
    fill(neg->ch, neg->ch+28, root);
    for (int i=0; i<len2; ++i)
        insert(i, a[i]);

    static Node *q[MAX_N*4];
    static int head, tail, pre[MAX_N*4], mask[MAX_N*4], ans;
    q[head=tail=0] = root;
    for (; head<=tail; ++head) {
        Node *k = q[head];
        for (int i=0; i<28; ++i)
            if (k->ch[i]) {
                q[++tail] = k->ch[i];
                pre[tail] = head;
            }
    }
    for (int i=tail; i>=0; --i) {
        if (q[i]->r == INT_MAX)
            mask[i] = q[i]->l<=len1 ? 0x1 : 0x2;
        else if (mask[i]==0x3)
            ans = max(ans, q[i]->len);
        if (i>0) mask[pre[i]] |= mask[i];
    }
    printf("%d\n", ans);
}

相關推薦

SPOJ LCS 公共 字尾自動機&字尾(Ukkonen)

  終於搞清楚了這兩個噁心的演算法。其實字尾樹也不難寫嘛。 題目   給定兩個字串a和b,求在a和b中都有出現的連續子串的最長長度。 樣例輸入 alsdfkjfjkdsal fdjskalajfkdsla 樣例輸出 3 做法1

【演算法 in python | DP】LCS公共

1. LCS,最長公共子串 動態規劃,狀態轉移方程: #該版本是返回最長公共子串和其長度,若只返回長度,則可以簡化 def lcs(s1, s2): l1 = len(s1) l2 = len(s2) # res[i][j]儲存子串s1[0:i] 和 子串s2[

SPOJ 1811. Longest Common Substring (LCS,兩個字串的公共字尾自動機SAM)

/* *********************************************** Author :kuangbin Created Time :2013-9-8 23:27:46 File Name :F:\2013ACM練習\專

SPOJ 1812 Longest Common Substring II 字尾自動機求多字串公共

題意: 給若干字串,求它們的最長公共子串的長度。 題解:字尾自動機。 對第一個串建立SAM,並拓撲排序。 用後面的串分別匹配。 對於SAM,每個節點新增兩個值ml,ans; ml代表該節點滿足單一字串時的最大值,匹配完一個字串後重置為0; an

718. Maximum Length of Repeated Subarray 字尾陣列解公共 O(n log^2 n)時間複雜度

題意 找最長公共子串 思路 用dp的方法很容易在O(n^2)解決問題,這裡主要討論用字尾陣列的思路解決這個問題 字尾數組裡有兩個經典的概念或者稱為資料結構,就是字尾陣列SA,以及高度陣列LCP SA陣列的定義是:將原串S所有的字尾按字典序排序

[字尾陣列] 兩公共 POJ - 2774

Long Long Message Time Limit: 4000MS   Memory Limit: 131072K Total Submissions: 35426 &n

公共LCS問題(動態規劃及備忘錄方法)

動態規劃與備忘錄的LCS實現 動態規劃從下到上積累能量,中間值全部記錄以方便後期計算時呼叫,但有些時候很多記錄的值用不到,這個時候備忘錄方法則更好,可以減少記錄的值,其特點是自上到下,記錄少部分值。以LCS最長公共子串問題威力,分別給出兩種實現。 動態規劃法: pa

[Hihocoder](1415)字尾陣列三·重複旋律3 ---- 字尾陣列(公共)

題目傳送門 做法: 我們知道,字串中任意一個子串都是某個字尾的字首 我們也知道了Height陣列的含義是排名為i的字尾與排名i-1的字尾的最長公共字首,即就是最長公共子串。 現在題意讓我們找兩個串的最

字尾陣列求兩個字串的公共

對於兩個字串,不好直接運用字尾陣列,所以我們可以把兩個子串串中間用一個在字串中不會出現的字元連線起 來,比如'$‘,計算字尾陣列,檢查字尾陣列中所有相鄰字尾。分屬於兩個字串的字尾的lcp的最大值就是答案。 因為字串的任何一個子串都是這個字串某個字尾的字首。求A和B 的最長公

常考的經典演算法--公共序列(LCS)與公共(DP)

https://blog.csdn.net/qq_31881469/article/details/77892324 《1》最長公共子序列(LCS)與最長公共子串(DP) http://blog.csdn.net/u012102306/article/details/53184446 h

poj 2774 公共(字尾陣列)

分析: 字串的任何一個子串都是這個字串的某個字尾的字首。求 A 和 B 的最長公共子串等價於求 A 的字尾和 B 的字尾的最長公共字首的最大值。如果列舉 A和 B 的所有的綴,那麼這樣做顯然效率低下。由於要計算 A 的字尾和 B 的字尾的最長公共字首

2217 (公共問題&&字尾陣列與高度陣列的運用)

題目連結 題意 給定兩個串a,b。計算兩個字串的最長公共子串的長度。 分析 先考慮簡化問題: 求在一個串中至少出現兩次的最長子串。 答案就是在後綴陣列中相鄰的字尾的最長公共字首。因為在後綴陣列中的起始位置相距越遠,他們的最長公共字首就越小

Longest Common Substring II(字尾自動機求多個公共

A string is finite sequence of characters over a non-empty finite set Σ. In this problem, Σ is the set of lowercase letters. Substring, also called factor

poj 2774 公共--字串hash或者字尾陣列或者字尾自動機

http://poj.org/problem?id=2774 想用字尾陣列的看這裡:http://blog.csdn.net/u011026968/article/details/22801015 本文主要講下怎麼hash去找 開始的時候寫的是O(n^2 logn)演算法

1159 Palindrome(迴文&LCS公共序列&滾動陣列)

A palindrome is a symmetrical string, that is, a string read identically from left to right as well as from right to left. You are

[SPOJ] (1812) Longest Common Substring II ---- SAM(多個公共

題目傳送門 做法: 類似求兩個串的最長公共子串。 我們對第一個串建立自動機,然後把剩餘的n-1個串放進自動機上匹配。 每個串都儲存它們在每個狀態上的匹配的最大長度ml, 然後對於每個狀態,維護一個數組

公共 字尾自動機

【題目描述】 給定兩個字串A和B,求它們的最長公共子串 【分析】 我們考慮將A串建成字尾自動機 令當前狀態為s,同時最大匹配長度為len 我們讀入字元x。如果s有標號為x的邊, 那麼s=trans(s

SPOJ1811公共問題(字尾自動機)

題意:給兩個串A和B,求這兩個串的最長公共子串。 分析:其實本題用字尾陣列的DC3已經能很好的解決,這裡我們來說說利用字尾自動機如何實現。 對於串A和B,我們先構造出串A的字尾自動機,那麼然後用

字尾自動機(多個穿的公共)spoj1812

SPOJ Problem Set (classical) 1812. Longest Common Substring II Problem code: LCS2 A string is finite sequence of characters over a non-

SPOJ1812(字尾自動機求n個公共)

題意:給定n個串,求它們的最長公共子串。 思路就是:先將一個串建SAM,然後用後面的串去匹配,對於每一個串,儲存最大值,對於不同的串,更新最小值。 SAM結點多兩個值,ml表示多個串的最小值,nl表示當前串匹配的最大值。 #include <iostream&