字串:KMP Eentend-Kmp 自動機 trie圖 trie樹 尾碼樹 尾碼數組

來源:互聯網
上載者:User

涉及到字串的問題,無外乎這樣一些演算法和資料結構:自動機 KMP演算法 Extend-KMP 尾碼樹 尾碼數組 trie樹 trie圖及其應用。當然這些都是比較進階的資料結構和演算法,而這裡面最常用和最熟悉的大概是kmp,即使如此還是有相當一部分人也不理解kmp,更別說其他的了。當然一般的字串問題中,我們只要用簡單的暴力演算法就可以解決了,然後如果暴力效率太低,就用個hash。當然hash也是一個面試中經常被用到的方法。這樣看來,這樣的一些演算法和資料結構實際上很少會被問到,不過如果使用它們一般可以得到很好的線性複雜度的演算法。

老實說,我也一直覺得字串問題挺複雜的,出來一個如果用暴力,hash搞不定,就很難再想其他的方法,當然有些可以用動態規劃。不過為瞭解決這個老大難問題,還是仔細對這些演算法和資料結構研讀了一番。做個筆記,免得忘了還得重新思考老長時間。如果碰到字串問題,也一般不會超過這些方法的範圍了。先看一張圖吧,主要說明下這些演算法資料結構之間的關係。圖中黃色部分主要寫明了這些演算法和資料結構的一些關鍵點。

 

圖中可以看到這樣一些關係:extend-kmp 是kmp的擴充;ac自動機是kmp的多串形式;它是一個有限自動機;而trie圖實際上是一個確定性有限自動機;ac自動機,trie圖,尾碼樹實際上都是一種trie;尾碼數組和尾碼樹都是與字串的尾碼集合有關的資料結構;trie圖中的尾碼指標和尾碼樹中的尾碼連結這兩個概念及其一致。

下面我們來分別說明這些演算法和資料結構,並對其涉及的關鍵問題進行分析和解釋。

kmp

首先這個匹配演算法,主要思想就是要充分利用上一次的匹配結果,找到匹配失敗時,模式串可以向前移動的最大距離。這個最大距離,必須要保證不會錯過可能的匹配位置,因此這個最大距離實際上就是模式串當前匹配位置的next數組值。也就是max{Aj 是 Pi 的尾碼  j < i},pi表示字串A[1...i],Aj表示A[1...j]。模式串的next數組計算則是一個自匹配的過程。也是利用已有值next[1...i-1]計算next[i]的過程。我們可以看到,如果A[i] = A[next[i-1]+1] 那麼next[i] = next[i-1],否則,就可以將模式串繼續前移了。
整個過程是這樣的:
void next_comp(char * str){
   int next[N+1];
   int k = 0;
   next[1] = 0;
   //迴圈不變性,每次迴圈的開始,k = next[i-1] 
   for(int i = 2 ; i <= N ; i++){
      //如果當前位置不匹配,或者還推進到字串開始,則繼續推進
      while(A[k+1] != A[i] && k != 0){
           k = next[k];
      }     
      if(A[k+1] == A[i]) k++;
      next[i] = k;
   } 
}
複雜度分析:從上面的過程可以看出,內部迴圈再不斷的執行k = next[k],而這個值必然是在縮小,也就是是沒執行一次k至少減少1;另一方面k的初值是0,而最多++ N次,而k始終保持非負,很明顯減少的不可能大於增加的那些,所以整個過程的複雜度是O(N)。

上面是next數組的計算過程,而整個kmp的匹配過程與此類似。

extend-kmp

為什麼叫做擴充-kmp呢,首先我們看它計算的內容,它是要求出字串B的尾碼與字串A的最長公用首碼。extend[i]表示B[i...B_len] 與A的最長公用前置長度,也就是要計算這個數組。

觀察這個數組可以知道,kmp可以判斷A是否是B的一個子串,並且找到第一個匹配位置?而對於extend[]數組來說,則可以利用它直接解決匹配問題,只要看extend[]數組元素是否有一個等於len_A即可。顯然這個數組儲存了更多更豐富的資訊,即B的每個位置與A的匹配長度。

計算這個數組extend也採用了於kmp類似的過程。首先也是需要計算字串A與自身尾碼的最長公用前置長度。我們設為next[]數組。當然這裡next數組的含義與kmp裡的有所過程。但它的計算,也是利用了已經計算出來的next[1...i-1]來找到next[i]的大小,整體的思路是一樣的。

具體是這樣的:觀察可以發現

首先在1...i-1,要找到一個k,使得它滿足k+next[k]-1最大,也就是說,讓k加上next[k]長度盡量長。

實際上下面的證明過程中就是利用了每次計算後k+next[k]始終只增不減,而它很明顯有個上界,來證明整個計算過程複雜度是線性。如所示,假設我們已經找到這樣的k,然後看怎麼計算next[i]的值。設len = k+next[k]-1(圖中我們用Ak代表next[k]),分情況討論:

如果len < i 也就是說,len的長度還未覆蓋到Ai,這樣我們只要從頭開始比較A[i...n]與A的最長公用首碼即可,這種情況下很明顯的,每比較一次,必然就會讓i+next[i]-1增加一.
如果len >= i,就是我們在圖中表達的情形,這時我們可以看到i這個位置現在等於i-k+1這個位置的元素,這樣又分兩種情況
如果 L = next[i-k+1] >= len-i+1,也就是說L處在第二條虛線的位置,這樣我們可以看到next[i]的大小,至少是len-i+1,然後我們再從此處開始比較後面的還能否匹配,顯然如果多比較一次,也會讓i+A[i]-1多增加1.
如果 L < len-i+1 也就是說L處在第一條虛線位置,我們知道A與Ak在這個位置匹配,但Ak與Ai-k+1在這個位置不匹配,顯然A與與Ai-k+1在這個位置也不會匹配,故next[i]的值就是L。

這樣next[i]的值就被計算出來了,從上面的過程中我們可以看到,next[i]要麼可以直接由k這個位置計算出來,要麼需要在逐個比較,但是如果需要比較,則每次比較會讓k+next[k]-1的最大值加1.而整個過程中這個值只增不減,而且它有一個很明顯的上界k+next[k]-1 < 2*len_A,可見比較的次數要被限制到這個數值之內,因此總的複雜度將是O(N)的。

trie樹

首先trie樹實際上就是一些字串組成的一個字元尋找樹,邊由代表組成字串的字元代表,這樣我們就可以在O(len(str))時間裡判斷某個字串是否屬於該集合。trie樹的節點內分支可以用鏈表也可以用數組實現,各有優劣。

簡單的trie樹每條邊由一個字元代表,但是為了節省空間的,可以讓邊代表一段字元,這就是trie的壓縮表示。通過壓縮表示可以使得trie的空間複雜度與單詞節點數目成正比。

AC自動機

ac自動機,可以看成是kmp在多字串情況下擴充形式,可以用來處理多模式串匹配。只要為這些模式串建立一個trie樹,然後再為每個節點建立一個失敗指標,也就是類似與kmp的next函數,讓我們知道如果匹配失敗,可以再從哪個位置重新開始匹配。ac實際上兩個人的名字的首字母,Aho-Corasick。

應該還記得,在kmp構造next數組時,我們是從前往後構造,即先構造1...i-1,然後再利用它們計算next[i],這裡也是類似。不過這個先後,是通過bfs的順序來體現的。AC自動機的失敗指標具有同樣的功能,也就是說當我們的模式串在Tire上進行匹配時,如果與當前節點的關鍵字不能繼續匹配的時候,就應該去當前節點的失敗指標所指向的節點繼續進行匹配。而從根到這個失敗指標指向的節點群組成的字串,實際上就是跟當前節點的尾碼的匹配最長的字串。

過程如下:

--------------引用:AC(Aho-Corasick)自動機演算法http://hi.baidu.com/luyade1987/blog/item/5ba280828dcb9eb96d811972.html

如同KMP中模式串得自我匹配一樣.從根節點開始,對於每個結點:設該結點上得字元為k,沿著其父親結點得失敗指標走,直到到達根節點或者當前失敗指標結點也存在字元為k得兒子結點,
那麼前一種情況當然是把失敗指標設為根節點,而後一種情況則設為當前失敗指標結點得字元為k得兒子結點.

我們也可以動手操作一下,如果我們的ac自動機只包含一個模式串,這個過程實際上就是kmp的計算過程。

接下來要做的就是進行文本匹配:
首先,Trie-(模式串集合)中有一個指標p1指向root,而文本串中有一個指標p2指向串頭。下面的操作和KMP很類似:如果設k為p2指向的字母 ,而在Trie中p1指向的節點存在字元為k的兒子,那麼p2++,p1

則改為指向那個字元為k的兒子,否則p1順著當前節點的失敗指標向上找,直到p1存在一個字元為k的兒子,或者p1指向根結點。如果p1路過一個標記為模式串終點的結點,那麼以這個點為終點的的模式
串就已經匹配過了.或者如果p1所在的點可以順著失敗指標走到一個模式串的終結結點,那麼以那個結點結尾的模式串也已經匹配過了。
在下面的連結中可以找到相關的資料:
www.cs.uku.fi/~kilpelai/BSA05/lectures/slides04.pdf

主要是根據模式串構造三個函數goto fail和output.

q := 0; // initial state (root)
for i := 1 to m do
while g(q, T[i]) = 0 do
q := f(q); // follow a fail
q := g(q, T[i]); // follow a goto
if out(q) != 0; then print i, out(q);
endfor;
-----------------------------------------引用結束-------------------------------------------------------------------------------------------
以ababa為例,我們可以得到它的kmp next數組值為 0 0 1 2 3,ac自動機和trie圖如下:

 

trie圖

trie圖實際上一個確定性自動機,比ac增加了確定性這個屬性,對於ac自動機來說,當碰到一個不匹配的節點後可能要進行好幾次回溯才能進行下一次匹配。但是對於trie圖來說,可以每一步進行一次匹配,每碰到一個輸入字元都有一個確定的狀態節點。

從上面的圖中我們也可以看到trie圖的尾碼節點跟ac自動機的尾碼指標基本一致,區別在於trie圖的根添加了了所有字元集的邊。另外trie圖還會為每個節點補上所有字元集中的字元的邊,而這個補邊的過程實際上也是一個求節點的尾碼節點的過程,不過這些節點都是虛的,我們不把它們加到圖中,而是找到它們的等價節點即它們的尾碼節點,從而讓這些邊指向尾碼節點就可以了。(比如中的黑節點c,它實際上並未出現在我們的初始tire裡,但我們可以把它作為一個虛節點處理,把指向它的邊指向它的尾碼節點)

trie圖主要利用兩個概念實現這種目的。一個是尾碼節點,也就是每個節點的路徑字串去掉第一個字元後的字串對應的節點。計算這個節點的方法,是通過它父親節點的尾碼節點,很明顯它父親的尾碼節點與它的尾碼節點的區別就是還少一個尾字元,設為c。所以節點的父節點的指標的c孩子就是該節點的尾碼節點。但是因為有時候它父親不一定有c孩子,所以還得找一個與父親的c孩子等價的節點。於是就碰到一個尋找等價節點的問題。

而trie圖還有一個補邊的操作,不存在的那個字元對應的邊指向的節點實際上可以看成一個虛節點,我們要找一個現有的並且與它等價的節點,將這個邊指向它。這樣也實際上是要尋找等價節點。

我們看怎麼找到一個節點的等價節點,我們所謂的等價是指它們的危險性一致。那我們再看一個節點是危險節點的充要條件是:它的路徑字串本身就是一個危險單詞,或者它的路徑字串的尾碼對應的節點是一個危險節點。因此我們可以看到,如果這個節點對應的路徑字串本身不是一個危險單詞,那它就與它的尾碼節點是等價的。所以我們補邊的時候,實際指向的是節點的尾碼節點就可以了。

trie圖實際上對trie樹進行了改進,添加了額外的資訊。使得可以利用它方便的解決多模式串的匹配問題。跟kmp的思想一樣,trie圖也是希望利用現在已經匹配的資訊,對未來的匹配提出指導。提出了一些新的概念。定義trie樹上,從根到某個節點的路徑上所有邊上的字元連起來形成的字串稱為這個節點的路徑字串。如果某個節點的路徑字串以一個危險字串結尾,那麼這個節點就是危險節點:也就是說如果到達這個點代表是匹配的狀態;否則就是安全節點。 那麼如何判斷某個節點是否危險呢?

根節點顯然是安全節點。一個節點是危險節點的充要條件是:它的路徑字串本身就是一個危險單詞,或者它的路徑字串的尾碼(這裡特指一個字串去掉第一個字元後剩餘的部分)對應的節點(一個字串對應的節點,是指從trie圖中的根節點開始,依次沿某個字元指定的邊到達的節點)是一個危險節點。

那麼如何求每一個節點的尾碼節點呢?這裡就可以裡利用以前的計算資訊,得到了。具體來說就是利用父親節點的尾碼節點,我們只要記住當前節點的最後一個字元設為C,那麼父親節點的尾碼節點的C分支節點就是要求的尾碼節點了。首先我們限定,根節點的尾碼節點是根本身,第一層節點的尾碼節點是根節點。這樣我們可以逐層求出所有節點的尾碼節點。但是這個過程中,可能出現一個問題:父親節點的尾碼節點可能沒有c分支。這時候該怎麼辦呢?

如所示如果設當前節點的父親節點的尾碼節點為w,我們假設w具有c孩子為,我們可以看到對於w的整個c子樹來說,因為根本不存在通向它們的邊c,它們也就不可能是不良字串,這樣這些節點的危險性也就等價與它們的尾碼節點的危險性了,而它們的尾碼節點,實際上就是w的尾碼節點的c孩子,如此回溯下去,最後就能找到。

 

--------------------引用:http://huangwei.host7.meyu.net/?paged=7

其實Trie圖所起到的作用就是建立一個確定性有限自動機DFA,圖中的每點都是一個狀態,狀態之間的轉換用有向邊來表示。Trie圖是在Tire的基礎上補邊過來的,其實他應該算是AC自動機的衍生,AC自動機只儲存其尾碼節點,在使用時再利用尾碼節點進行跳轉,並一直迭代到找到相應的狀態轉移為止,這個應該算是KMP的思想。這篇文章可以參考。

而Trie圖直接將AC自動機在狀態轉移計算後的值儲存在當前節點,使得不必再對尾碼節點進行迭代。所以Trie圖的每個節點都會有|∑|個狀態轉移(∑指字元集)。構造具體方法可見WC2006《Trie圖的構建、活用與改進》。我簡單敘述下流程:
(1)構建Trie,並保證根節點一定有|∑|個兒子。
(2)層次遍曆Trie,計算尾碼節點,節點標記,沒有|∑|個兒子的對其進行補邊。
尾碼節點的計算:
(1)根結點的尾碼節點是它本身。
(2)處於Trie樹第二層的節點的尾碼結點也是根結點。
(3)其餘節點的尾碼節點,是其父節點的尾碼節點中有相應狀態轉移的節點(這裡類似AC自動機的迭代過程)。
節點標記:
(1)本身就有標記。
(2)其尾碼節點有標記。
補邊:
用其尾碼節點相應的狀態轉移來填補當前節點的空白。
最後Trie圖中任意一個節點均有相應的狀態轉移,我們就用這個狀態轉移做動態規劃。
設dp[i][j]表示第i個狀態產生j個字元時,與DNA序列最小的改變值。
假設Tire圖中根節點是0,則初始化dp[0][0]=1。
其後,對圖進行BFS遍曆,可知處於第j層時,就說明以產生了j長度的字串。
dp[0][0] = 1;for i = 1 to m do  for 圖中每條邊(s1,ch,s2) do    dp[s2][i] = min{dp[s1][i-1] + (txt[i-1] != ch)};    for 圖中每個結點x do  ans = min{dp[x][m]};

-----------------------------------------------引用結束-----------------------------------------------------------------------------------

尾碼樹

尾碼樹,實際上就是字串的所有尾碼組成的字串集合構成的trie樹。如果採用不壓縮方式的trie儲存,這樣整個內部節點和外部節點的總和就可能達到O(n^2).所以不能利用這種儲存方式,因為如果採用它那麼構建的複雜度下界就是O(n^2),不會再低了。所以必須使用壓縮方式,才有可能降到O(n)。

構建之前,我們首先給字串加上一個未在字串中出現過的單詞,比如"$",為什麼這樣做呢?是為了避免尾碼節點出現在內部,如果我們加上"$",很明顯就不會有尾碼出現在內部了,可以用反證法證明:假設出現了一個這樣的尾碼是內部節點,那麼意味著這條字串路徑上會有兩個"$",但這是不可能的,因為我們的"$"只在結尾出現,之前沒有出現過。

構建過程中,我們看如果採用普通的構建過程是怎樣的?普通的構建,假設字串為A[1....N],我們從以A[1]開頭的尾碼開始插入trie樹,插入的時候,逐步比對,直到找到不匹配的分支,在這個節點將原來的節點分裂,並加入這個新的節點。可以這個過程關鍵是尋找,之前sufix[1]...sufix[i-1]這些已經插入的字串與sufix[i]的最長公用首碼。之後插入的時間O(1)就可以完成,因此主要的時間花在這個最長公用首碼(稱為head[i])的尋找上。Headi是W(i,n)和W(j,n)的最長公用首碼,其中j是小於i的任意正整數,Taili使得Headi + Taili = W(i,n)。

那我們看到現在關鍵是這個最長公用首碼head[i]的計算了。我們再次考慮如何利用head[1]...head[i-1]來計算head[i],為加快尋找hi的速度我們需要使用輔助結構——尾碼連結。

尾碼連結的定義(McCreight Arithmetic):
令Head[i-1] = az,其中a是字串W的第i-1位字元。由於z在範圍i內出現過至少兩次(因為az也是A[i-1...N]與之前某尾碼的最長公用首碼,也就是說另外的那個尾碼也是一az開頭的一個串,這樣就意味著它的後繼者,就比然是以z為首碼的,這樣A[i...N]與它的公用首碼就是z。{實際上這個性質在我們計算尾碼數組的lcp時也會利用到}),所以一定有|Head[i]| >=  |z|,z是Head[i]的首碼。所謂hi-1的尾碼連結(Suffix Link)實際是由hi-1指向z對應節點d的指標Link h[i-1]。當然,z有可能是空串,此時Link hi-1由hi-1指向根節點Root。

和前面 ac自動機的失敗指標 trie樹的尾碼指標比較,我們可以發現這裡的z它剛好就是head[i-1]去掉第一個字元後的那個尾碼,所謂的尾碼連結,實際上是指向head[i]自身的尾碼的連結,這個定義也就跟我們trie樹裡的尾碼指標所指向的那個位置一致了。這樣這個head[i]的尾碼連結怎樣建立就很清楚了。

建立方法:
1)根節點Root的尾碼連結指向它自身
2)任何一個非分葉節點的尾碼連結都在該節點出現以後被立即建立

演算法主架構如下:
For i = 1 -> n do
    步驟1、函數Find從Link hi-1開始向下搜尋找到節點hi
    步驟2、增添葉子節點Leafi
    步驟3、函數Down建立hi的尾碼連結Link hi      
End for

尾碼樹效能分析:
接著剛才文字框內的虛擬碼來談論。對於給定的i,步驟2的複雜度為O(1),但由於無法確定Link hi-1到hi之間的節點個數,所以不能保證步驟1總是線性。局部估算失敗,不妨從整體入手。有一點是肯定的,那就是i + |Headi|總隨著i的遞增而遞增。因此,W中的每個字元只會被Find函數遍曆1次,總體複雜度是O(n)的。

這個分析就與extend-kmp的複雜度分析很類似了。

尾碼數組

尾碼數組實際上就是對字串的尾碼按照字典序進行排序,然後把排好序後的順序放到一個數組sa[]裡儲存,數組元素代表了尾碼在原串裡的起始索引。通過這個我們可以很容易得到另一個數組rank[],rank[i]代表了原來的尾碼A[i...N]在sa數組裡的排名。

這個資料結構,主要涉及兩個方面的內容,一個是如何快速的對這些尾碼排序,有很多方法,這裡只說明倍增演算法,這個方法比較好理解,思路也比較巧妙。

還有就是尾碼數組求出來後,如果要發揮比較強的作用,還需要求出各個尾碼的最長公用首碼lcs。所以lcs的計算也是一個重點。

首先看排序,如果我們採用普通的排序演算法,那麼需要nlogn次比較,但是每次比較需要O(n),這樣總的複雜度將是O(n*nlogn).

倍增演算法是這樣的,主要是第i次排序,比較時的大小時利用了第i-1次的排序結果,這樣可以讓比較在O(1)時間裡完成:
我們首先對所有從原字串各個位置開始的長度為1的字元進行排序,然後再對從這些位置開始的長度為2的排序,之後是長度為2^i的排序,直到2^i >= N.可以看到這中間,總共需要log N次排序。然後我們看第i次排序,比較大小時怎樣利用了第i-1次的排序結果。

比如在第i次排序時,我們需要比較A[j]和A[k]開始的長度為2^i的串,那麼我們可以將它們分成兩塊:
A[j]開始的長度為2^i的串 = A[j]開始的2^(i-1)長 + A[j+2^(i-1)]開始的2^(i-1)長
A[k]開始的長度為2^i的串 = A[k]開始的2^(i-1)長 + A[k+2^(i-1)]開始的2^(i-1)長
要比較A[j]開始的長度為2^i的串 和 A[k]開始的長度為2^i的串,我們只要先比較第一部分,如相等再比較第2部分,而這兩部分大小因為之前已經排好序了,我們完全可以給它們一個rank值,只比較它們的rank值就可以得到大小關係,這樣比較就可以在O(1)時間內完成了。另外如果我們的排序演算法是O(n)的,這樣整個演算法的複雜度就是O(nlogn)的了。

再看lcs的計算,如果要計算任意兩個尾碼的lcs[i][j],我們有一個結論:

設 i<j  LCP(i,j)=min{LCP(k-1,k)|i+1 =< k <= j}  LCP Theorem 這裡的i,j指而是sa[i] sa[j]

如果要證明上面那個結論,首先要證明這個:對任意的 1=<i<j<k<=n, LCP(i,k)=min{LCP(i,j),LCP(j,k)},這裡不再證明。

上面那個結論實際上說:如果要找i j的最長公用前置長度,只需要找到i j之間相鄰尾碼的最小lcs長度即可。這樣我們只需要求出sa數組中相鄰尾碼的lcs長度,就轉化成了一個rmq問題,即區間內的最小值問題。這個可以O(1)解決。這樣問題就變成:如何在O(n)時間裡,計算sa數組中相鄰尾碼的lcs長度。

這個問題如果要O(n),又利用了下面這樣一個結論:定義一個一維數組 height,令height[i]=LCP(i-1,i) 1<i<=n 並設 height[1]=0。如何盡量高效的算出height數組呢?

為了描述方便,設h[i]=height[Rank[i]],即height[i]=h[SA[i]],而h數組滿足一個性質:

對於 i>1 且 Rank[i]>1  一定有 h[i] >= h[i-1]-1.

為什麼會有這個結論呢?實際上就與上面尾碼樹的尾碼連結那部分提到的想呼應了。h[i]=height[Rank[i]],實際上就是我們的原來的尾碼A[i...N]與某個串的最長公用首碼,而h[i-1]就是A[i-1...N]與某個串的最長公用首碼。而我們可以看到如果把A[i-1...N]去掉第一個字元後,就變成了A[i...N],我們假設A[i-1...N]相鄰的那個尾碼串是XYYYYYY,在這裡它們的lcs長度是h[i]。尾碼串XYYYYYY,去掉x之後就是YYYYYYY,這樣如果沒有比它更接近A[i...N]的,那麼h[i]=h[i-1]-1,如果A[i...N]的鄰居不是它,那麼h[i]只可能比h[i-1]-1大不可能比它小。

這樣利用這個結論,我們在O(n)時間內就可以把h[i]計算出來了。因為h[i]最大不超過N,而它每次最多減少不超過1.

計算出來之後,再根據height[i]=h[SA[i]],就可以計算出height數組,這樣就求出了sa中相鄰尾碼的lcs長度。

總結:
實際上我們可以看到上面的演算法思想,都有一個共同點:利用已經得到的計算結果得到下一次計算的結果,盡量利用現有資訊,減少計算量。
轉載請註明作者:phylips@bmy 出處:http://duanple.blog.163.com/blog/static/709717672009825004092/

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.