又被分治題卡住好幾個小時!用最笨的方法搞懂分治法邊界,告別死迴圈!
這篇文章寫於我剛學演算法時。好傢伙,第一道題快排就卡我老半天。但是好訊息是,我算是沒有得過且過,花了一晚上和一上午,把所有情況都捋了一遍、把迭代過程考慮清楚了。之後便感覺入了門,有了感覺,後續其他題目都沒有卡我這麼久過。
被很簡單的快排 程式碼執行狀態: Memory Limit Exceeded
老半天。
最後琢磨半天越界這事兒。總結起來一句話:避免出現 func(l, r) { ... func(l, r) ... }
(遞迴時傳遞到下一層的邊界值不縮小)這種情況,因為這是死迴圈。 如何避免? 比如func(l, r) { func(l, j), func(j + 1, r)}
中,j
至少滿足 j > r
j
從r
身上離開,防止 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 1
和 1 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 1
和 1 0
的雙重考驗。
這是為什麼呢?
- 這裡有相關證明:AcWing 785. 快速排序演算法的證明與邊界分析
- 如果你沒耐心看上述較為嚴謹的證明,可以看文末我寫的
我用比較笨的方法理解是:
int mid = l+r >> 1;
:則可證明j
的取值範圍是[l, r-1]
,因此對於邊界組合j j+1
有quick_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 i
有quick_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:j
在r
處就不再q[j] > x
,而i
在l
處還滿足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) {}
j
在r
處就不再q[j] > x
,而i
在l
處還滿足q[i] < x
;因此對於l < r
,還要再跳一輪,因為是 do while
而不是 while do
,所以不管 i
或 j
什麼條件,都得再至少來一次 i++; j--;
。
情況2:j
在r
處還滿足q[j] > x
,而i
在l
處就不再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) {}
j
在r
處還滿足q[j] > x
,因此,一定會繼續執行j--
,j
一定會小於r
。
情況3:j
在r
處就不再q[j] > x
,且i
在l
處就不再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) {}
j
在r
處就不再q[j] > x
,且i
在l
處就不再q[i] < x
;此時有 i < j
,因此不跳出迴圈,執行 swap
;對於l < r
,還要再跳一輪,因為是 do while
而不是 while do
,所以不管 i
或 j
什麼條件,都得再至少來一次 i++; j--;
。
這裡的魅力在於 do while
:不管咋的,你滿不滿足條件,我先給你移動一下,你再判斷。
對於二分法,核心思想也是避免出現func(l, r) { func(l, r); }
,因此出現 mid = l + r >> 1;
則必有 r = mid;
,因為 mid
是向下取整,l < r
時 mid
肯定碰不到 r
。
我是小拍,記得關注給個在看!