Divide&Conquer
Divide&Conquer
思想只能作为指导 实践中的细节才是魔鬼藏身之处——二分边界问题
排序
快速排序
1 |
|
变式:第K 大/小 数
归并排序
两种写法:
自顶向下:简洁 可读性好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41#include <iostream>
using namespace std;
int NUM[10] = { 10,9,7,8,6,5,4,4,1,2 };
void display(int a[], int n) {
int i(0);
while (i < n)
cout << a[i++] << " ";
}
void Merge(int a[], int l, int mid, int r) {
int* b = new int[r - l + 1];
int b_index = 0;
int left = l; int right = mid + 1;
//判断并复制
while (left <= mid and right <= r) {
if (a[left] < a[right]) b[b_index++] = a[left++];
else b[b_index++] = a[right++];
}
//多余元素全部复制
while (left <= mid) b[b_index++] = a[left++];
while (right <= r) b[b_index++] = a[right++];
//复制临时数组序列给原数组
int start = l;
for (int b_index_this = 0; start <= r; start++)
a[start] = b[b_index_this++];
delete[] b;
}
void MergeSort(int a[], int l, int r) {
if (l < r) {
int mid = l + r >> 1;
MergeSort(a,l,mid);
MergeSort(a,mid+1,r);
Merge(a,l,mid,r);
}
}
int main() {
MergeSort(NUM, 0, 9);
display(NUM,10);
}自底向上:高效 但要考虑的细节多 较繁琐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54#include<iostream>
#include<algorithm>
using namespace std;
int NUM[10] = { 10,9,7,8,6,5,4,4,1,2 };
int times = 1;
void merge(int a[], int i, int mid, int j) {
int* b = new int[j - i + 1];
int b_index = 0;
int left = i; int right = mid + 1;
//判断并复制
while (left <= mid and right <= j) {
if (a[left] < a[right]) b[b_index++] = a[left++];
else b[b_index++] = a[right++];
}
//多余元素全部复制
while (left <= mid)
b[b_index++] = a[left++];
while (right <= j)
b[b_index++] = a[right++];
//复制临时数组序列给原数组
int start = i;
for (int b_index_this = 0; start <= j; start++)
a[start] = b[b_index_this++];
delete[] b;
}
void mergePass(int a[], int len, int n) {
int index(0);
//合并相邻表,若能够合并,当前表下标,加上连续两个表长后的下标小于末尾下标,则合并。
for (; index + 2 * len - 1 < n; index += 2 * len)
merge(a, index, index + len - 1, index + 2 * len - 1);
//考虑最后一个轮空的表,其长度会较小,因此当满足index+2*len-1>n and index + len -1<n 则说明有小表,小表的末尾就是n-1.
if (index + len - 1 < n)
merge(a, index, index + len - 1, n - 1);
}
void mergeSort(int a[], int n) {
//循环的增长与归并算法的趟数有关
for (int len = 1; len < n; len *= 2)
mergePass(a, len, n);
}
void display(int a[], int n) {
int i(0);
while (i < n)
cout << a[i++] << " ";
}
int main() {
mergeSort(NUM,10);
display(NUM, 10);
return 1;
}
查找
第K小
用快排的思想写是最好的。
画数轴确定递归的判断条件:
1 |
|
等长有序序列中位数
两个等长序列A B,求他们的中位数。(递增序列)
分析:
- 若A B只有一个元素, 取较小者;
- 若A B有多个元素,则分别求出A B的中位数,若二者中位数相同,那就是最终答案,若不同:
- 若A的中位数>B的中位数:限制AB序列,取A前半段,B后半段,然后递归;
- 若A的中位数<B的中位数:限制AB序列,取A后半段,B前半段,然后递归;
1 |
|
二分
整数二分
区间是 [L,MID-1] 则mid=l+r+1>>1 满足条件 L=mid 因为右侧是[mid,R]
区间是[L,MID] 则mid=l+r>>1 满足条件 R=mid 因为左侧是[L,mid]
1 |
|
浮点数二分
1 |
|
组合
棋盘覆盖
2n规模的棋盘中,有一个特殊块,用L型骨牌填满棋盘,围住特殊块。
分析:为什么用分治法可以完成?因为当棋盘很小时,可以很简单的求出来,那么问题就在于划分棋盘,每次都将棋盘划分为四个小棋盘,说着很简单,但其实最重要的不在于单纯的划分,因为按照一开始所说,棋盘很小时问题很容易解决(一个特殊块的2*2棋盘 只需要一个L型骨牌即可),这其实意味着棋盘即使很小,但也要存在那个特殊块,但目前只有一个特殊块,而棋盘却又很大,因此不仅需要划分,还要在每次划分时选定一个特殊块,而对于本问题,L型骨牌其实就是我们手动构造出来的特殊块了,因为每次划分为4份小棋盘,而其中一份必然是特殊块所在的,其余三个棋盘就没有特殊块了,因此需要为他们添加特殊块,问题来了,构造的特殊块应该放在哪里呢?实际上,放在三个棋盘的交接处即可,这样也就构造了L型骨牌,依次类推..最终一定能填满。
最复杂的就是分析交接处,要判断不同的象限,将特殊块放在不同的位置:右下,左下,右上,左上。但其实在构造的过程中很容易发现规律,发现规律后就可以直接套了,规律见注释确定是否+1
处
1 |
|
循环日程
n=2k个选手,在n-1天内彼此都比赛一次,求日程表。
设表格行代表第i个 选手 行的下标从1开始,列代表天数 下标从0开始 那么日程二维表的第一列(下标为0)用来显示行号,以此来表示选手的编号,从第二列(下标为1)开始,元素值表示选手的对手,下标代表比赛的天数。
比如S[0][0] 代表第一个选手第0天 第0天没有意义 此时为第一列 不如填入选手的编号1
S[1][1]代表第二个选手第1天要对阵的人 S[4][6]代表第五个选手 第六天要对阵的人
为什么说此问题可以用递归解决呢,首先问题规模很小时,当然可以解决,于是第一个性质就满足了,那如何由小问题推出大问题呢?实际上本题的选手数为2的幂,因此选手规模只能成2倍的增长,而对于两倍的增长,这个二维表数据是可以复用的。
1 |
|
最大连续子序列和
用dp最好 但还是介绍一下DC吧
分析:对于一个序列,将它分为左右两边,那么那段最大连续子序列要么在纯左侧,要么在纯右侧,要么就横跨左右两侧,对于横跨左右两侧的情况,只需要从中点开始,往左右两侧蔓延判断即可。这是宏观上的划分。具体而言,真正能一口气确定要不要选择某个元素是在只有一个元素时,元素>0就选择,否则就不选,当序列稍微有点长,就要开始斟酌了,但这是计算机需要考虑的,因为每一次递归,都要在divided的序列中求这三种情况,疯狂套娃,直到能轻而易举地conquer,最终再合并,就能conquer最终的大问题。
1 |
|
大整数乘法
求两个二进制的数的乘积(这两个二进制数很长很大)
分析:将两个二进制数分为两段。记这两个二进制数分别为A,B则要求A*B 可以把A的高n/2位取出,记为a1,低n/2位取出,记为a2,因此A=a1*2n/2+a2,同理B=b1*2n/2+b2,因此A*B=(a1*2n/2+a2)*(b1*2n/2+b2)=a1*b1*2n+(a1*b2+a2*b1)*2n/2+a2*b2。
思路:
- 初始化A B的字符串 并将字符串转为整数数组。
- 递归将两个数组分别拆为前后两段,代表高位和低位。出口:数组长度为1,此时可以计算出结果并返回给上一层。回归处理:将得到的四个结果转为10进制并做分析中的计算A*B=(a1*2n/2+a2)*(b1*2n/2+b2)=a1*b1*2n+(a1*b2+a2*b1)*2n/2+a2*b2;再将该结果转换为2进制。层层返回,最终得到一个二进制的结果。
1 |
|
总结
思路:
宏观划分(在这里不需要思考太多具体实现)
递归出口(问题在什么地步可以轻而易举地解决)
问题合并(大问题是怎样由小问题得到的)
这三大步分别从大问题 小问题 和大问题与小问题之间的联系 着手考虑,好美的思想,有点哲学惹…