關於TAOCP中用集合論對演算法進行嚴格數學定義的理解

陸超超發表於2012-01-02

高德納(Donald E. Knuth)在其名作《The Art Of Computer Programming》的第一卷《Fundamental Algorithms》中,用集合論對演算法進行了嚴格的數學定義,僅僅用了一頁,言簡意賅,但是就這一頁足 以體現出他深厚的數學功底,駕馭數學的高超能力,把數學語言的簡潔與精確體現得淋漓盡致。我在讀 這一頁時想到了很多,受到的啟發遠不止紙上的這些,現在把這些理解寫在這裡,希望對大家有所幫助。

在介紹演算法的數學定義之前,我們要明確演算法的五個基本特徵:

  • 有限性:描述了一個演算法在執行有限步之後必須會終止。
  • 確定性:描述了一個演算法的每個步驟都必須精確地定義,可以嚴格地無歧義地被執行。
  • 輸入:描述了一個演算法在執行之前賦給它的量或在執行過程中動態地賦給它的量。
  • 輸出:描述了一個演算法執行結束時的結果。
  • 有效性:描述了一個演算法在執行過程中的所有運算必須是充分基本的,是可行的,原則上人們可 以用筆和紙在有限的時間內精確地完成這些運算。

高教授在用集合論定義演算法時是嚴格按照這五個基本特徵來的,下面我就用常用的集合論術語隆重地介紹 演算法的定義,如下:


我們定義一個計算方法為一個四元組(Q, I, Ω, f),每個字母的意義為:
Q: 表示計算狀態的集合
I: 表示輸入的集合
Ω: 表示輸出的集合
f: 表示計算規則的集合
滿足如下約束:
enter image description here
對集合I中的每一個輸入x定義一個序列:enter image description here,滿足如下規則:
enter image description here
如果k是使enter image description hereΩ中為最小的整數,那麼就說這個計算序列在k步 中終止,而且在此種情況下說x產生了輸出enter image description here。可能有些序列永遠不會終止,這隻能稱為一個計算方法,而不能稱之為演算法演算法是一種對I中所有的x在有限步中終止的計算方法


到目前為止,該定義雖然是用集合論描述的,但是絲毫沒有難以理解的地方,就相當於一個有限狀態機, 每一節點既是上一個節點的輸出節點,又是下一個節點的輸入節點,最後f在輸出集合Ω中保持逐點 不動,很通俗易懂。然而,這個定義還不是很全面,還只包含了演算法五個基本特徵的前四個,並沒有包含 第五個有效性特徵。在為該定義補充第五個基本特徵之前,讓我們先來用上述定義描述一個具體的演算法, 用一個大家都很熟悉的歐幾里得演算法。

歐幾里得演算法E:m和n是兩個正整數,求它們最大的公因子。
E1[求餘數]:m除以n並且令r為所得的餘數。(0≤r<n)
E2[判斷餘數是否為零]:如果r=0,演算法結束,n就是輸出答案。
E3[減少]:置m←n,n←r,並返回步驟E1

這個演算法可以說是眾人皆知,最平民化的演算法。如何用上述演算法定義中的術語來描述這個演算法呢?首先在 歐幾里得演算法中,我們涉及到單點(n):表示輸出;有序數對(m,n):表示輸入。除此之外,還涉及 到三個不同的步驟,每一步基本都與m,n,r這三個數有關,為了方便區別這三個不同的步驟,我們還要 引入一個步驟的序列號,這樣我們可以用三個有序四元組分別表示每一個步驟了。所以歐幾里得演算法 嚴格的數學描述如下:


歐幾里得演算法是一個四元組(Q, I, Ω, f),每個字母的意義為:
Q: 所有單點(n),所有有序數對(m,n)和所有有序四元組(m,n,r,1),(m,n,r,2) 以及(m,n,p,3)的集合,其中m,n,p是正整數,r是一個非負整數。
I: 所有有序數對(m,n)的子集。
Ω: 所有單點(n)的子集。
f的定義如下:
1. f((m,n))=(m,n,0, 1)
2. f((m,n,r,1))=(m,n,m%n,2)
3. 如果r=0,f((m,n,r,2))=(n);否則f((m,n,r,2))=(m,n,r,3)
4. f((m,n,p,3))=(n,p,p,1)
5. f((n))=(n)


我在這裡有必要說明一下,上面的f定義了5條規則,第1條規則是得到輸入數對之後初始化為四元組,才 能參與後面的步驟;第2條對應於E1;第3條對應於E2;第4條對應於E3;第5條是演算法結束的 終止條件。顯而易見,歐幾里得演算法這樣的數學定義與先前的步驟是一一對應的,但是更加嚴格準確了。

現在,大家應該對演算法的集合論定義有了比較清晰的瞭解。前面已經說過,該集合論的數學定義還不盡如 人意,還不包括前面的有效性基本特徵。例如,當Q是僅用紙和筆而無法計算的無窮序列,或者f是緊 靠人腦也無法實現的計算,那麼這時就不滿足有效性的特徵,也就不能稱之為演算法。所以為了使演算法的數 學定義更加完善,我們應該給Q和f加一些約束,使運算的每一步都是可行的有效的,或更加具體一點說,就是使每一運算都只涉及初等運算。在計算機中,什麼樣的初等運算最具有代表性呢?當然是字串 的刪除,新增,替換等操作啊,這些操作是絕對可行的,而且表示的含義也很廣泛,當然你也可以使用其 他的初等運算。接下來我們就在Q和f上加一些約束了:


首先令A是字母的有限集合,並且令A上所有串的集合,我們用的串來對計 算的狀態進行編碼,即不同的串代表不同的計算狀態。於是我們有:
Q:是所有(σ,j)的集合,其中σ∈,0≤j≤N,N為非負整數
I:是所有j=0時Q的子集,即是所有(σ,0)的集合
Ω:是所有j=N時Q的子集,即是所有(σ,N)的集合

為了方便描述,我們還要給出一個定義:對於θ,σ,α,ω∈,如果σ=αθω,那麼則稱θ 出現在σ中

現在我們可以給出f的約束了,如下:
enter image description here
——F1式:如果enter image description here不出現在enter image description here中,那麼: enter image description here

——F2式:如果enter image description here是滿足enter image description here的最短的字串,那麼:enter image description here

——F3式enter image description here

[注]
1. F1式通俗地說就是:如果在一個字串enter image description here中如果找不到另一個字串enter image description here,就跳轉第enter image description here步。
2. F2式通俗地說就是:如果在一個字串enter image description here中找到了另一個字串enter image description here,就用字串enter image description here替換字串enter image description here,然後跳轉到第enter image description here步。
3. F3式是終止條件。


上面對於f的約束顯然能確保演算法的有效性基本特徵,因為上述定義的每一步都是對字串進行了簡單的 操作。事實上,F1式F2式F3式這三個式子足夠可以描述我們手頭的所有事情,更具 體一點就是,足夠可以描述所有的計算機程式。我為什麼這樣說呢?我們知道所有的計算機演算法程式都是 用某種程式語言寫的,而所有的程式語言無外乎三種結構:順序選擇迴圈。上面的三 個式子正好可以描述這三種結構。我將一一進行解釋:

  • 順序結構
    這個最簡單,這三個式子任意執行就可以了,而且只要指定j,下一步就會進行你指定的第j個運算,所以不再贅述了。
  • 選擇結構
    F1式F2式結合起來使用便是選擇結構:如果enter image description here不出現在enter image description here中,就執行F1式,如果enter image description here出現在enter image description here中,就執行F2式。這裡字串中的出現與不出現就分別表示的是相反的兩種計算狀態。
  • 迴圈結構
    F2式中,令enter image description here,便會進行迴圈運算,然後用F1式判斷是否跳出迴圈,非常容易。

F3式是描述演算法執行結束的狀態。所以用上面的F1式F2式F3式三個式子可以 描述所有的演算法,而且每一步都是有效的,到這裡演算法的集合論數學定義才算完備,囊括了演算法所有的五 個基本特徵。



除此之外,程式語言中最基本的資料型別——整型,可以用某個字元的個數來表示,而程式語言中最基本的初等運算——加減法,可以用字串的替換來表示,所以現在,我可以說基於字串集合F1式F2式F3式三個式子就構成了一門程式語言,通過它們可以編寫任何你想要的合法程式,和那些高階語言(如C語言等)效果一樣。但是這是一門有嚴格數學定義的程式語言,為了以後敘述的方便,我將其命名為M語言(是Math語言的簡稱,後面均用此簡稱)。M語言中僅僅涉及enter image description here這四個變數(變數的定義和前面一致),所以這四個變數的取值就決定了M語言中語句的每一步執行,換句話說,在M語言中每一條語句就是這四個變數的取值,因此每一條語句就可以用這個四元組表示。以後,我們如果說用M語言來編寫程式,就是說用這四元組來編寫程式,程式的每一條語句都是四元組,整個程式就是由這些四元組構成的。



為了熟練使用這種由集合論進行了嚴格數學定義的演算法語言——M語言,最後,我將M語言應用於 一個具體的例子,用來說明其強大的描述能力。當然,我們還是以歐幾里得演算法為例,考慮到用字串來 描述初等運算,我們使用歐幾里得輾轉相減法,而不是原先的輾轉相除法。

題目表述如下:
通過描述上面的F1式F2式F3式三個式子中的enter image description here,給出計算正整數m和n的最大公因子的一個“有效性”演算法,令輸入為字串enter image description here,輸出結果是enter image description here,當然你完全可以輸出除b 以外的其他字母,這裡只是為了方便而已。希望你儘可能給出簡單的解。

我用自己的話重述一下這個題目:就是用M語言編寫求最大公因子的歐氏輾轉相減法。根據我前面 所述,就是求一系列四元組。首先我還是按照前面介紹歐氏輾轉相除法的方式來介紹輾轉相減法,如下:

歐氏輾轉相減法ERM:m和n是兩個正整數,求它們最大的公因子。 ERM1[求差]:m減n並且令r為所得的差的絕對值。(0≤r<n)
ERM2[判斷差是否為零]:如果r=0,演算法結束,n就是輸出答案。
ERM3[減少]:置m←min(m,n),n←r,並返回步驟ERM1

這個演算法ERM和前面的演算法E的正確性很容易證明,限於篇幅,這裡就略去了。下面我就主要講 解一下我解決這道題目的思路:


題中兩個正整數m和n分別是用a和b的個數表示的,所以要求m和n的差,只要每次分別去掉一個“a”和一 個“b”,為了便於字串的操作,每次直接去掉“ab”,一直到只剩下“a┄”序列或“b┄”序列為止,這時剩下的“a”或“b”的個數就是m和n差的絕對值r,這就完成了步驟ERM1。為了方便步驟ERM3的賦值操作,我們需要知道min(m,n)到底是哪一個,顯然min(m,n)就是去掉的“ab”的個數,因 此我們需要換一種簡單的方式保留“ab”的個數,就用另外一個字母“c”替換“ab”,這就是F2式中 的操作。但是“c”會把字串中的“a”序列和“b”序列分開,無法進行繼續去掉“ab”的操作,因此我們要把“c”移到字串的最左邊或最右邊(這就導致了兩種解法,下文我都會給出。)這種移到最左邊或最右邊的操作一般會是連續重複的,這就會使用到上文所說的F2式中的迴圈操作。然後繼續替換“ab” 也會使用該迴圈操作。等替換完所有的“ab”後,這時可以執行步驟ERM3,“c”的個數就是min(m,n),餘下的“a”或“b”的個數就是r,接下來要分兩種情況,如果把“c”移到了最左端,由於跳到步 驟ERM1後要繼續替換“ab”,所以這時要將所有的“c”變成“a”,將所有的“b”或“a”都變成“b”;如果把“c”移到了最右端,這時要將所有的“c”變成“b”,將所有的“b”或“a”都變成“a”,這些就是執行步驟ERM3,當然這些過程都會用到F1式中的跳轉操作和F2式中的循 環操作。執行完步驟ERM3後就會重複執行步驟ERM1,以此迴圈直到執行步驟ERM2,即r=0, 也就是說字串中只剩下“c”的序列,這時“c”的個數就是m和n的最大公因子(gcd(m,n))。當然不一 定最後以“c”的表示輸出,可以輸出任意其他字元,那麼就可以用F2式中的迴圈替換操作。


下面我就給出該題目的一種解答(將“c”移到最左邊),並將詳細解釋該M語言編寫的程式的每一 步,該程式如下:
enter image description here
【上述M語言版的輾轉相減法程式註解】:j表示程式的步驟號,每個步驟的具體含義為:

  • 第0步:如果找到“ab”就用“c”替換,然後跳轉到第1步;如果找不到“ab”,就直接跳轉到第2 步。(這一步就是用“c”替換“ab”
  • 第1步:如果找到“ac”就用“ca”替換,然後迴圈執行這一步;否則直接跳轉到第0步。(這一步 就是將“c”移到最左邊
  • 第2步:如果找到“a”就用“b”替換,然後迴圈執行這一步;否則直接跳轉到第3步。(這一步就 是執行ERM3中的n←r操作
  • 第3步:如果找到“c”就用“a”替換,然後迴圈執行這一步;否則直接跳轉到第4步。(這一步就 是執行ERM3中的m←min(m,n)操作
  • 第4步:如果找到“b”就用“b”替換,然後跳轉到第0步;否則直接跳轉到第5步。(這一步就是相 當於執行ERM2的判斷
  • 第5步:如果找到“a”就用“b”替換,然後迴圈執行這一步(這一步就是為了滿足題目的要求最終 以“b”序列的形式輸出來結果

為了使該M語言版演算法的步驟更加清晰,我給出一個例項:m=6,n=4,此時的輸入為σ=aaaaaabbbb,執行過程圖如下:

aaaaaabbbb→aaaaacbbb→aaaacabbb→aaacaabbb→aacaaabbb →acaaaabbb→caaaaabbb→caaaacbb→caaacabb→caacaabb→cacaaabb →ccaaaabb→ccaaacb→ccaacab→ccacaab→cccaaab→cccaac→cccaca →ccccaa→ccccba→ccccbb→acccbb→aaccbb→aaacbb→aaaabb→aaacb →aacab→acaab→caaab→caac→caca→ccaa→ccba→ccbb→acbb→aabb →acb→cab→cc→ac→aa→ba→bb

執行結果輸出結果σ=bb,即gcd(6,4)=2。

同理,我再給出該題目的另外一個解(將“c”移到最右邊),該程式如下:
enter image description here
分析同上面的將“c”移到最左邊的解。

最後為了完整性,同時考慮通用性,我將用虛擬碼的形式實現由F1式F2式F3式所嚴 格定義的演算法通用程式,如下:

while(!terminate condition){
    find the first Theta_j in the input_string;
    if(No Found){
       j=a_j;    
    }else{
       input_string=the sub_string in front of Theta_j 
                    + Phi_j 
                    + the sub_string behind Theta_j;
       j=b_j;
    }
}

上面虛擬碼中選擇語句if···else···的第一部分相當於F1式,第二部分相當於F2式,而terminate condition相當於F3式,所以具有通用性,很容易將上述虛擬碼用高階語言程式設計實現,例如依照該虛擬碼,可以輕鬆的將上面講的輾轉相減法的M語言版用高階語言實現。下面就是我用C++實現的輾轉相減法的第一種解(第二種解幾乎一樣,只要改一下狀態表中的變數值即可),如下:

#include    <iostream>
#include    <string>

using namespace std;

int main (){
    int j=0; 
    string theta[]={"ab","ac","a","c","b","a"};
    string phi[]={"c","ca","b","a","b","b"};
    int b[]={1,1,2,3,0,5};
    int a[]={2,0,3,4,5,5}; 
    string input;

    cout<<"Input:";
    cin>>input;
    cout<<"Output:"<<endl;

    while(input.find("a",0)!=-1 || input.find("c",0)!=-1){
        int location=input.find(theta[j],0);
        if (location==-1){
            j=a[j];
        }else{
            input=input.substr(0,location)
                  +phi[j]
                  +input.substr(location+theta[j].length());
            j=b[j];

        cout<<input<<"→";
        }
    }
    cout<<"Success"<<endl;
    system("pause");
    return 0;
}                

假設輸入aaaaaabbbb,則執行該程式的結果為:
enter image description here

至此,就是我閱讀這一頁Knuth教授用集合論對演算法進行嚴格數學定義的全部理解。

相關文章