1分治算法 Divide and Conquer
21. 前提条件
3
4分治是一种算法思想。
5
6它的核心是:把一个大问题拆成多个小问题
7
8小问题解决后
9
10再合并成大问题答案
11
12典型算法:
13
14归并排序
15快速排序
16二分查找
17最近点问题
18大整数乘法
19矩阵乘法
20
212. 分治三步
22
23分治固定三步:
24
251. Divide:分解问题
262. Conquer:解决子问题
273. Combine:合并结果
28
29比如归并排序:
30
31Divide:
32 把数组分成左右两半
33
34Conquer:
35 分别排序左右两半
36
37Combine:
38 合并两个有序数组
39
403. 分治通用递归模板
41
42void divideConquer(问题范围)
43{
44 if(问题足够小)
45 {
46 直接解决;
47 return;
48 }
49
50 分成若干子问题;
51
52 divideConquer(子问题1);
53 divideConquer(子问题2);
54
55 合并子问题答案;
56}
57
584. 例子一:归并排序
59
60归并排序是最标准的分治。
61
624.1 思想
63先分到单个元素
64
65再两两合并
66
67例如:
68
69[5,2,8,1]
70
71分:
72
73[5,2] [8,1]
74
75继续分:
76
77[5] [2] [8] [1]
78
79合并:
80
81[2,5] [1,8]
82
83再合并:
84
85[1,2,5,8]
86
874.2 代码
88#include <stdlib.h>
89
90void merge(int arr[],
91 int temp[],
92 int left,
93 int mid,
94 int right)
95{
96 int i = left;
97 int j = mid + 1;
98 int k = left;
99
100 while(i <= mid && j <= right)
101 {
102 if(arr[i] <= arr[j])
103 {
104 temp[k++] = arr[i++];
105 }
106 else
107 {
108 temp[k++] = arr[j++];
109 }
110 }
111
112 while(i <= mid)
113 {
114 temp[k++] = arr[i++];
115 }
116
117 while(j <= right)
118 {
119 temp[k++] = arr[j++];
120 }
121
122 for(int p = left; p <= right; p++)
123 {
124 arr[p] = temp[p];
125 }
126}
127//之前说过这里,其实就是分拆以后隔成两个数组每一项逐项对比之后排序加进来
128
129void mergeSortHelper(int arr[],
130 int temp[],
131 int left,
132 int right)
133{
134 if(left >= right)
135 {
136 return;
137 }
138
139 int mid = left + (right - left) / 2;
140
141 mergeSortHelper(arr, temp, left, mid);
142 mergeSortHelper(arr, temp, mid + 1, right);
143
144 merge(arr, temp, left, mid, right);
145}
146
147void mergeSort(int arr[], int n)
148{
149 int* temp = malloc(n * sizeof(int));
150
151 mergeSortHelper(arr, temp, 0, n - 1);
152
153 free(temp);
154}
1554.3 对应分治三步
156Divide:
157 mid = left + (right-left)/2
158
159Conquer:
160 mergeSortHelper(left, mid)
161 mergeSortHelper(mid+1, right)
162
163Combine:
164 merge(left, mid, right)
165
1665. 例子二:快速排序
167
168快速排序也是分治。
169
170但它和归并排序不同:
171
172归并排序:
173 先递归,再合并
174
175快速排序:
176 先分区,再递归
177
1785.1 思想
179选一个基准值 pivot。
180
181然后:
182
183比 pivot 小的放左边
184比 pivot 大的放右边
185
186pivot 归位后:
187
188左边递归快排
189右边递归快排
190
1915.2 代码
192void swap(int* a, int* b)
193{
194 int temp = *a;
195 *a = *b;
196 *b = temp;
197}
198
199int partition(int arr[], int left, int right)
200{
201 int pivot = arr[left];
202
203 int i = left;
204 int j = right;
205
206 while(i < j)
207 {
208 while(i < j && arr[j] >= pivot)
209 {
210 j--;
211 }
212
213 while(i < j && arr[i] <= pivot)
214 {
215 i++;
216 }
217
218 if(i < j)
219 {
220 swap(&arr[i], &arr[j]);
221 }
222 }
223
224 arr[left] = arr[i];
225 arr[i] = pivot;
226
227 return i;
228}
229
230void quickSort(int arr[], int left, int right)
231{
232 if(left >= right)
233 {
234 return;
235 }
236
237 int mid = partition(arr, left, right);
238
239 quickSort(arr, left, mid - 1);
240 quickSort(arr, mid + 1, right);
241}
242
2435.3 对应分治三步
244Divide:
245 partition 分区
246
247Conquer:
248 quickSort(left, mid-1)
249 quickSort(mid+1, right)
250
251Combine:
252 不需要额外合并
253 因为 pivot 已经归位
254
2556. 归并排序 和 快速排序
256归并排序:
257
258先把问题拆到底
259再合并答案
260
261重点在 Combine
262
263稳定
264需要 O(n) 辅助空间
265
266--------------------------------
267
268快速排序:
269
270先通过 partition 分好左右
271再递归解决左右
272
273重点在 Divide
274
275不稳定
276平均 O(nlogn)
277
2787. 分治例子三:二分查找
279
280二分查找也属于分治。
281
282每次把问题规模缩小一半
283
284代码:
285
286int binarySearch(int arr[], int n, int target)
287{
288 int left = 0;
289 int right = n - 1;
290
291 while(left <= right)
292 {
293 int mid = left + (right - left) / 2;
294
295 if(arr[mid] == target)
296 {
297 return mid;
298 }
299 else if(arr[mid] < target)
300 {
301 left = mid + 1;
302 }
303 else
304 {
305 right = mid - 1;
306 }
307 }
308
309 return -1;
310}
311
312对应:
313
314Divide:
315 用 mid 分成左右
316
317Conquer:
318 只进入可能的一半
319
320Combine:
321 不需要合并
322
3238. 分治和动态规划区别
324
325分治:
326
327子问题通常不重叠
328
329例如归并排序:
330
331左半边和右半边互不重叠
332
333动态规划:
334
335子问题大量重叠
336
337例如 Fibonacci:
338
339fib(n-1)
340fib(n-2)
341
342会反复算。
343
344所以 DP 需要:
345
346memo / dp数组
347
348分治一般不需要记忆化。
349
3509. 分治易错点
3511. 分治不是简单递归。
352 它必须有“拆分 + 子问题 + 合并”。
353
3542. 归并排序重点是合并。
355 快排重点是分区。
356
3573. 分治子问题一般互不重叠。
358 DP 子问题一般会重叠。
359
3604. 递归出口一定要写:
361 left >= right
362
3635. mid 推荐写:
364 left + (right-left)/2
365
36610. 分治总结
367分治算法
368
369核心思想:
370 大问题拆成小问题
371 小问题解决
372 再合并答案
373
374 Divide 分解
375 Conquer 解决
376 Combine 合并
377
378典型算法:
379 归并排序
380 快速排序
381 二分查找
382 最近点问题
383 矩阵乘法
384
385归并排序:
386 先递归
387 后合并
388
389快速排序:
390 先分区
391 后递归
392
393分治 vs DP:
394 分治子问题一般不重叠
395 DP子问题一般重叠
分治算法 Divide and Conquer
- 前提条件
分治是一种算法思想。
它的核心是:把一个大问题拆成多个小问题
小问题解决后
再合并成大问题答案
典型算法:
归并排序
快速排序
二分查找
最近点问题
大整数乘法
矩阵乘法
- 分治三步
分治固定三步:
- Divide:分解问题
- Conquer:解决子问题
- Combine:合并结果
比如归并排序:
Divide:
把数组分成左右两半
Conquer:
分别排序左右两半
Combine:
合并两个有序数组
- 分治通用递归模板
void divideConquer(问题范围)
{
if(问题足够小)
{
直接解决;
return;
}
分成若干子问题;
divideConquer(子问题1);
divideConquer(子问题2);
合并子问题答案;
}
- 例子一:归并排序
归并排序是最标准的分治。
4.1 思想
先分到单个元素
再两两合并
例如:
[5,2,8,1]
分:
[5,2] [8,1]
继续分:
[5] [2] [8] [1]
合并:
[2,5] [1,8]
再合并:
[1,2,5,8]
4.2 代码
#include <stdlib.h>
void merge(int arr[],
int temp[],
int left,
int mid,
int right)
{
int i = left;
int j = mid + 1;
int k = left;
while(i <= mid && j <= right)
{
if(arr[i] <= arr[j])
{
temp[k++] = arr[i++];
}
else
{
temp[k++] = arr[j++];
}
}
while(i <= mid)
{
temp[k++] = arr[i++];
}
while(j <= right)
{
temp[k++] = arr[j++];
}
for(int p = left; p <= right; p++)
{
arr[p] = temp[p];
}
}
//之前说过这里,其实就是分拆以后隔成两个数组每一项逐项对比之后排序加进来
void mergeSortHelper(int arr[],
int temp[],
int left,
int right)
{
if(left >= right)
{
return;
}
int mid = left + (right - left) / 2;
mergeSortHelper(arr, temp, left, mid);
mergeSortHelper(arr, temp, mid + 1, right);
merge(arr, temp, left, mid, right);
}
void mergeSort(int arr[], int n)
{
int* temp = malloc(n * sizeof(int));
mergeSortHelper(arr, temp, 0, n - 1);
free(temp);
}
4.3 对应分治三步
Divide:
mid = left + (right-left)/2
Conquer:
mergeSortHelper(left, mid)
mergeSortHelper(mid+1, right)
Combine:
merge(left, mid, right)
- 例子二:快速排序
快速排序也是分治。
但它和归并排序不同:
归并排序:
先递归,再合并
快速排序:
先分区,再递归
5.1 思想
选一个基准值 pivot。
然后:
比 pivot 小的放左边
比 pivot 大的放右边
pivot 归位后:
左边递归快排
右边递归快排
5.2 代码
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int partition(int arr[], int left, int right)
{
int pivot = arr[left];
int i = left;
int j = right;
while(i < j)
{
while(i < j && arr[j] >= pivot)
{
j--;
}
while(i < j && arr[i] <= pivot)
{
i++;
}
if(i < j)
{
swap(&arr[i], &arr[j]);
}
}
arr[left] = arr[i];
arr[i] = pivot;
return i;
}
void quickSort(int arr[], int left, int right)
{
if(left >= right)
{
return;
}
int mid = partition(arr, left, right);
quickSort(arr, left, mid - 1);
quickSort(arr, mid + 1, right);
}
5.3 对应分治三步
Divide:
partition 分区
Conquer:
quickSort(left, mid-1)
quickSort(mid+1, right)
Combine:
不需要额外合并
因为 pivot 已经归位
- 归并排序 和 快速排序
归并排序:
先把问题拆到底
再合并答案
重点在 Combine
稳定
需要 O(n) 辅助空间
快速排序:
先通过 partition 分好左右
再递归解决左右
重点在 Divide
不稳定
平均 O(nlogn)
- 分治例子三:二分查找
二分查找也属于分治。
每次把问题规模缩小一半
代码:
int binarySearch(int arr[], int n, int target)
{
int left = 0;
int right = n - 1;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(arr[mid] == target)
{
return mid;
}
else if(arr[mid] < target)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
return -1;
}
对应:
Divide:
用 mid 分成左右
Conquer:
只进入可能的一半
Combine:
不需要合并
- 分治和动态规划区别
分治:
子问题通常不重叠
例如归并排序:
左半边和右半边互不重叠
动态规划:
子问题大量重叠
例如 Fibonacci:
fib(n-1)
fib(n-2)
会反复算。
所以 DP 需要:
memo / dp数组
分治一般不需要记忆化。
-
分治易错点
-
分治不是简单递归。
它必须有“拆分 + 子问题 + 合并”。
-
归并排序重点是合并。
快排重点是分区。
-
分治子问题一般互不重叠。
DP 子问题一般会重叠。
-
递归出口一定要写:
left >= right
-
mid 推荐写:
left + (right-left)/2
-
分治总结
分治算法
核心思想:
大问题拆成小问题
小问题解决
再合并答案
Divide 分解
Conquer 解决
Combine 合并
典型算法:
归并排序
快速排序
二分查找
最近点问题
矩阵乘法
归并排序:
先递归
后合并
快速排序:
先分区
后递归
分治 vs DP:
分治子问题一般不重叠
DP子问题一般重叠