1. 程式人生 > 其它 >又被分治題卡住好幾個小時!用最笨的方法搞懂分治法邊界,告別死迴圈!

又被分治題卡住好幾個小時!用最笨的方法搞懂分治法邊界,告別死迴圈!

這篇文章寫於我剛學演算法時。好傢伙,第一道題快排就卡我老半天。但是好訊息是,我算是沒有得過且過,花了一晚上和一上午,把所有情況都捋了一遍、把迭代過程考慮清楚了。之後便感覺入了門,有了感覺,後續其他題目都沒有卡我這麼久過。

被很簡單的快排 程式碼執行狀態: Memory Limit Exceeded 老半天。

最後琢磨半天越界這事兒。總結起來一句話:避免出現 func(l, r) { ... func(l, r) ... } (遞迴時傳遞到下一層的邊界值不縮小)這種情況,因為這是死迴圈。 如何避免? 比如func(l, r) { func(l, j), func(j + 1, r)}中,j至少滿足 j > r

jr身上離開,防止 func(l, j) 是 func(l, r)),就可用。

#include <iostream>
using namespace std; const int N = 1e6 + 10; int n; int q[N];

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++; while (q[i] < x);
        do j --; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

int main() { scanf("%d", &n); for (int i = 0; i < n; i ++) scanf("%d", &q[i]); quick_sort(q, 0, n-1); for (int i = 0; i < n; i ++) printf("%d ", q[i]); return 0; }

手賤,非得寫成:

quick_sort(q, l, i - 1), quick_sort(q, i, r);

好傢伙,報錯。半天沒看出來,後來才恍然大悟,你要是用 i 分界,上面得是 x = q[l + r + 1 >> 1];

那我下面這樣不行嗎?

x = q[l+r >> 1];
...
quick_sort(q, l, j - 1), quick_sort(q, j, r);

// 或者這樣不行嗎?
x = q[l+r >> 1];
...
quick_sort(q, l, i - 1), quick_sort(q, i, r);

// 或者這樣不行嗎?
x = q[l+r >> 1];
...
quick_sort(q, l, i), quick_sort(q, i + 1, r);

// 或者這樣不行嗎?
x = q[l+r+1 >> 1];
...
quick_sort(q, l, j), quick_sort(q, j + 1, r);

// 或者這樣不行嗎?
x = q[l+r+1 >> 1];
...
quick_sort(q, l, j - 1), quick_sort(q, j, r);

// 或者這樣不行嗎?
x = q[l+r+1 >> 1];
...
quick_sort(q, l, i), quick_sort(q, i + 1, r);

上述都不行,看我一一舉反例。

我們輸入長度是2的陣列,則第一層迴圈:l = 0, r = 1(即 quick_sort(0, 1)),如果進入第二層迴圈時,還出現 quick_sort(0, 1)的情況,則陷入死迴圈。

下表中,“傳到函式的i, j”指呼叫 quick_sort(q, l, ?i/j), quick_sort(q, ?i/j, r)i, j 的值。

下表中,最後一列標記 x 表示將使程式陷入死迴圈。

對於 int mid = l+r >> 1;

測試用例 q[mid] 傳到函式的i, j 傳入引數
0 1 0 0, 0 j-1 j => (0, -1), (0, 1)x
0 1 0 0, 0 i-1 i => (0, -1), (0, 1)x
0 1 0 0, 0 j j+1 => (0, 0), (1, 1)
1 0 1 1, 0 i i+1 => (0, 1)x, (2, 1)
1 0 1 1, 0 j j+1 => (0, 0), (1, 1)

可見,在 int mid = l+r >> 1; 時,四種組合中只有 j j+1 經受住了 0 11 0 的雙重考驗。

對於 int mid = l+r+1 >> 1;

測試用例 q[mid] 傳到函式的i, j 傳入引數
1 0 0 1, 0 j-1 j => (0, -1), (0, 1)x
1 0 0 1, 0 i i+1 => (0, 1)x, (2, 1)
1 0 0 1, 0 i-1 i => (0, 0), (1, 1)
0 1 1 1, 1 j j+1 => (0, 1)x, (2, 1)
0 1 1 1, 1 i-1 i => (0, 0), (1, 1)

可見,在 int mid = l+r+1 >> 1; 時,四種組合中只有 i-1 i 經受住了 0 11 0 的雙重考驗。

這是為什麼呢?

我用比較笨的方法理解是:

  • int mid = l+r >> 1;:則可證明 j 的取值範圍是 [l, r-1] ,因此對於邊界組合 j j+1quick_sort(q, l, j小於r), quick_sort(q, j+1大於l, r) ,永遠都不會有 quick_sort(q, l, r) 出現。
  • int mid = l+r+1 >> 1;:則可證明 i 的取值範圍是 [l+1, r] ,因此對於邊界組合 i-1 iquick_sort(q, l, i-1小於r), quick_sort(q, i大於l, r) ,永遠都不會有 quick_sort(q, l, r) 出現。

OK,那下面就是背誦:

  • 快排中,int mid = l+r >> 1;mid 向下取整),是 j j+1,因為j 取值範圍是 [l r-1]
  • 我個人是不太喜歡背誦的,還是知道原理,覺得到時候可以快速推匯出來靠譜,推導如下。

用較清晰但是笨拙的方法證明一下 mid 向下取整時 j 屬於 [l, r-1]

向下取整時 j 屬於 [l, r-1] 等價於 向下取整時至少有兩次 j-- 被執行

下面分三種特殊情況討論(普通情況不討論),可以看出三種情況中都至少有兩次 j-- 被執行

情況1:jr處就不再q[j] > x,而il處還滿足q[i] < x

q[mid]     x
           9  8
begin   i        j
step1      i     j  do i++; while(q[i] < x);
step2      i  j     do j--; while(q[j] > x);
step3      8  9
step4      i  j     swap(q[i], q[j]);
step5         ij    do i++; while(q[i] < x);
step6     j  i     do j--; while(q[j] > x);
跳出迴圈 while(i < j) {}

jr處就不再q[j] > x,而il處還滿足q[i] < x;因此對於l < r,還要再跳一輪,因為是 do while 而不是 while do ,所以不管 ij 什麼條件,都得再至少來一次 i++; j--;

情況2:jr處還滿足q[j] > x,而il處就不再q[i] < x

q[mid]     x
           8  9
begin   i        j
step1      i     j  do i++; while(q[i] < x);
step2      ij       do j--; while(q[j] > x);
step3      8  9
跳出迴圈 while(i < j) {}

jr處還滿足q[j] > x,因此,一定會繼續執行j--j一定會小於r

情況3:jr處就不再q[j] > x,且il處就不再q[i] < x

q[mid]     x
           8  8
begin   i        j
step1      i     j  do i++; while(q[i] < x);
step2      i  j     do j--; while(q[j] > x);
step3      8  8
step4      i  j     swap(q[i], q[j]);
step5         ij    do i++; while(q[i] < x);
step6      j  i     do j--; while(q[j] > x);
跳出迴圈 while(i < j) {}

jr處就不再q[j] > x,且il處就不再q[i] < x;此時有 i < j ,因此不跳出迴圈,執行 swap;對於l < r,還要再跳一輪,因為是 do while 而不是 while do ,所以不管 ij 什麼條件,都得再至少來一次 i++; j--;

這裡的魅力在於 do while :不管咋的,你滿不滿足條件,我先給你移動一下,你再判斷。

對於二分法,核心思想也是避免出現func(l, r) { func(l, r); } ,因此出現 mid = l + r >> 1; 則必有 r = mid; ,因為 mid 是向下取整,l < rmid 肯定碰不到 r

我是小拍,記得關注給個在看!