天天看點

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

就像上一篇文章說的,“程式設計永遠是後話”!在有了可靠的問題分析過程和資料結構的選擇,能正确運作的“二分搜尋”代碼出現之前,把其主要的思路先在草稿上實作,即僞代碼。但由于僞代碼執行結果的不确定性,需要有一個驗證的過程。筆者非常不喜歡這個過程,因為這個過程很繁瑣,而且推出的結論不一定是正确的(畢竟沒有實實在在在機器上運作得到正确的結果),在筆者看來,給一個算法題,知道用什麼算法,資料結構,如果能用僞代碼實作,離成功已經不遠了。 

但後來我又反駁了自己的觀點(沖突體啊),理由:至少到目前為止,寫的都是小程式、小算法題,驗證過程可能已經被潛移默化解決了。

麻煩來了,今天晚上在實作“動态規劃矩陣連乘最優組合”的算法在這個問題中需要填表,通過動态規劃解體,就因為表的下标混亂,是以填表的過程比較枯燥(debug了好多次)。 我先在稿紙上用僞代碼大概解決了這個問題,但是在真正敲寫代碼的時候,卻發現“僞代碼”除了整體上的走向之外(大概的結構),很多細節都有問題。

“大概”僞代碼:

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

for i=[0,n-1) 

    for j=[0,n-1-i) 

        col =...    //col是填表元素的列 

        min =... 

        for k=[0,i) 

            t =.... 

            if t<min 

                t = min 

        a[j][col] = min;

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

其中省略号内的東西待敲進去之後都不正确!需要重新分析這個填表的過程。

順序填表分成n-1組,編号i=[0,n-2],如圖: 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

而每組有j=n-1-i個元素需要填寫。于是僞代碼的前兩行是這樣的來的

    for j=[0,n-1-i)

首先把當下需要填寫元素的列值得出是col = i+j+1;通過觀察很容易發現的;而j即為當下需要填寫元素的行,于是(j,col)就是需要填寫元素的位置。 

而min的計算是瓶頸,畫圖

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

可以發現計算min的兩個輔助元素的(第一個元素)行和(第二個元素)列都不被當下需填寫元素的(j,col)決定了。于是:

min = a[j][j] + a[j+1][col] + tab[j]*tab[j+1]*tab[col]; 

接下來的t的計算由上面的min的計算的出來的:      

t = a[j][j+k+1] + a[j+k+2][col] + tab[j] * tab[j+k+2] * tab[col+1]; 

其實是一樣的,隻不過紅色部分多加了個k+1。

分析過程不夠嚴謹細膩,但是縱觀下來,自己有一個清晰的思路。

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

View Code 

要做到上面的條條框框實在是不容易的,但是如果養成“條條框框”的習慣的話,即使沒有稿紙,隻操手MSPAINT,相信敲代碼的效率會提高的。

“腳手架”簡單來說是“驗證程式”的程式,但筆者認為“斷言”的魅力更大些。在每一個程式中,有一些變量資料是至關重要的,經常Debug就是為了這些變量的檢測,看是否和預期中的結果一樣;如果不一樣我的做法就是:結束debug,開始艱苦的錯誤排查,這個過程非常頭疼。assert能夠可以掃清很多的錯誤細節,包括除數為0,資料超出規定的範圍,數組下标越界雲雲。是以添加斷言,能在邏輯上保證你的程式不會出錯,即使現實并非如此。

故在上面“動态規劃矩陣連乘最優組合”中,添加了 

assert(n!=0); 

assert(col+1<n+1); 

assert(j+k+2<n); 

來保證數組下标越界問題,很明顯,如果下标越界,将是毀滅性的bug。

另外,添加了一個show函數: 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

void show(int ** a,int n) 

    for(int i=0; i<n; i++) 

    { 

        for(int j=0; j<n; j++) 

        cout << setw(6) <<a[i][j]; 

        cout << endl; 

    }// for 

    cout << endl; 

}

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

非常遺憾,第67章看懂沒多少,大概是要讀者明白如何預見程式的開銷!!相反,第八章有趣多了。第八章提出了找出一個數字序列中最大的、連續的子序列,并且規定全負子序列的和為0。

“如果沒有閱讀過《程式設計珠玑》跳到第四點。”

最原始窮舉的算法時間開銷大到O(n^3); 

另一種窮舉的算法,即平方算法。通過儲存中間結果或者預處理資料,省去了之後重複的計算,是備忘錄算法(簡單的動态規劃),開銷下降了一個數量級O(n^2); 

分治法,這個真沒想到,開銷再次下降O(nlogn); 

以前做過類似的題目,是以最先想到的就是這個方法。形象點就是,“邊吃邊拉”——掃描算法,運作時間為O(n)。 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

for i=[0,n) 

    t += a[i] 

    if t<max case 

        max = t 

    if t<0 case 

        t = 0 

end.

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

課後習題第10題,“找到總和最接近0的子序列或者最接近某個數的子序列”,嘗試着用上面的“邊吃邊拉”算法解決,但是沒有成功;隻能按着上面說的平方算法,僞代碼如下: 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

nearest = INF 

    for j=[i,n) 

        t = tab[j] - tab[i] 

        if nearest>|t| case 

            nearest = t 

end

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

第八章最有趣的地方就是“數組索引下标居然可以出現複數”!寫了将近兩年的程式居然還不知道有這個東西,略有自慚形穢的味道。 

設原數組a[n],pa = &a[1],那麼pa[-1]亦即a[0]。但是,這麼巧妙的東西,該怎麼用? 大家一開始都有寫過“冒泡排序”,看看利用這個“巧妙”能有什麼效果?給出僞代碼: 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

bubble(a,n) 

    mustbe(n>1) 

    pa = &a[1] 

    for i=[0,n) 

        for j=[0,n-i) 

            if a[j]>pa[j] case 

                swap(a[j],pa[j]) 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

是的,它既沒有改善冒泡的運作開銷和效率,但是代碼美觀了很多:通過把pa定位在a的第二個元素上,是以a[j]和pa[j]其實不是同一個元素,pa[j]在a[j]的後面,即便他們的下标相同。筆者突然想到了一個比較現實而有用例子,大家一開始學習程式設計的時候,幾乎都遇到過這樣的題目,給定一個數字數組,求數組的各元素的總和,最原始的想法:

sum(a,n) 

        sum += a[i]; 

看看結合上面的“巧妙”的僞代碼: 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

    pa = &a[n-1] 

    t = n>1; 

    for i=[0,t) 

        sum += (a[i]+pa[i|0x8000])    //下标變為負數 

    if n&1 case 

        sum += a[t] 

《程式設計珠玑,字字珠玑》45678讀書筆記——程式設計技巧

沒錯,他還是做了n次的加法,一次都沒減,但是也有小小的優化:for循環裡對i的判斷判斷和自增都減半!加減開銷比位運算大的。“啊哈,靈機一動!”。非常文藝青年的一個優化,非常詩意...

本文完 Friday, April 06, 2012

繼續閱讀