本章进行分治策略部分的总结,其中内容包含算法导论和算法分析设计上边儿的例子,我主要是想学会其中的每个问题,以及每个问题背后的解法,最好是能够形成C++的代码。
分治策略
分治模式在每层递归上都有三个步骤:
- 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
- 解决这些子问题,递归的求解各个子问题。然而,若子问题的规模足够小,可以直接求解
- 合并这些子问题的解成为原问题的解
归并排序
归并排序应该是分治中最经典的例子,也是算法导论上边儿讲解分治策略的开始,归并排序算法完全遵循分治模式:
- 分解:分解待排序的n个元素成各具 n/2 个元素的两个子序列
- 解决:使用归并排序递归的排序两个子序列
- 合并:合并两个已排序的子序列以产生已排序的答案
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100;
int A[maxn], L[maxn], R[maxn];
int n;
void merge(int a[], int p, int q, int r) {
int n1 = q - p + 1;
int n2 = r - q;
for (int i = 1; i <= n1; i++)
L[i] = a[p + i - 1];
for (int i = 1; i <= n2; i++)
R[i] = a[q + i];
L[n1 + 1] = INT_MAX;
R[n2 + 1] = INT_MAX;
int i = 1, j = 1;
for (int k = p; k <= r; k++) {
if (L[i] <= R[j]) {
a[k] = L[i];
i++;
} else {
a[k] = R[j];
j++;
}
}
}
void merge_sort(int a[], int p, int r) {
if (p < r) {
int q = (p + r) / 2;
merge_sort(a, p, q);
merge_sort(a, q + 1, r);
merge(a, p, q, r);
}
}
int main() {
cout << "input n: " << endl;
cin >> n;
cout << "input the n numbers: " << endl;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
merge_sort(A, 1, n);
for (int i = 1; i <= n; i++) {
cout << A[i] << " ";
}
return 0;
}
在上述代码中,使用了两个哨兵,其实还有不加哨兵的版本,对于复杂度的影响并不是会很大,但是可能常数影响会增大,具体增大的地方在双指针归并的时候会增加比较的次数。
逆序对
算法导论2-4求解逆序对,可以再代码检测网站完成测试:https://www.luogu.com.cn/problem/P1908
根据逆序对的定义( i < j , a [ i ] > a [ j ] i<j,a[i]>a[j] i<j,a[i]>a[j]),我们可以在分治排序的过程中,统计逆序对的数目。需要知道的是,从大到小的排序是逆序对数目最多的,由小到大排序好的数组内是不存在逆序对的,因此我们在合并的过程中可以进行逆序对数目的统计。
按照算法导论2.3-2之中,修改merge操作,将其修改成不使用哨兵的形式。申请一个暂时保存的数组来完成。其中统计的时候,mid左右的两个序列分别是从小到大的序列,然后当a[i]>a[j]时,i到mid的肯定都是大于当前的a[j]的,也就是存在mid-i+1个逆序对,累计到全局变量。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 6e6 + 10;
int A[maxn], c[maxn];
int n;
long long cnt;
void merge(int a[], int p, int q, int r) {
int i = p, j = q + 1;
int k = p;
while (i <= q && j <= r) {
if (a[i] <= a[j]) {
c[k] = a[i];
i++;
k++;
} else {
c[k] = a[j];
k++;
j++;
cnt = cnt + q - i + 1;
}
}
while (i <= q) {
c[k] = a[i];
i++;
k++;
}
while (j <= r) {
c[k] = a[j];
j++;
k++;
}
for (i = p; i <= r; i++) {
a[i] = c[i];
}
}
void merge_sort(int a[], int p, int r) {
if (p < r) {
int q = (p + r) / 2;
merge_sort(a, p, q);
merge_sort(a, q + 1, r);
merge(a, p, q, r);
}
}
int main() {
cnt = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
merge_sort(A, 1, n);
cout << cnt << endl;
return 0;
}
最大子数组
最大子数组问题来源于股票购买,原问题是想要求得如何购买股票可以获得最大的收益,我们通过将第i天的股票价格减去前一天的价格差价作为数组 A[i] 的值,这样原问题就抽象为求 A 数组的最大子数组。
最大子段和:https://www.luogu.com.cn/problem/P1115
暴力解法
能够想到的暴力的解法就是,枚举子数组的左端点,然后扩大数组的长度,当然这样的算法复杂度是 O ( n 2 ) O(n^2) O(n2)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 5;
int A[maxn];
int n;
int max_subarray(const int *a, int len) {
int i, j, sum, ans;
ans = -INT_MAX;
for (i = 0; i < len; i++) {
sum = 0;
for (j = i; j < len; j++) {
sum = sum + a[j];
if (sum > ans) {
ans = sum;
}
}
}
return ans;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> A[i];
}
cout << max_subarray(A, n) << endl;
return 0;
}
这并不是想要的最快的方法,提交上述代码到评测网站,果然过不了全部的数据,因此得尝试降低复杂度。
分治法
我们可以考虑将原问题进行分治,为什么产生分治的想法呢?我们去看原问题的规模是否可以分解,分解的方法是怎样的呢?
假设数组 A[i,j] 是数组 A[low,high] 的最大子数组,那么 i, j ,low, hight之间存在如下关系:
-
mid 为A[low, high]的中间
-
最大子数组完全位于A[low, mid]中,$low \leq i \leq j \leq mid $
-
最大子数组完全位于A[mid+1, high]中, m i d < i ≤ j ≤ h i g h mid < i \leq j \leq high mid<i≤j≤high
-
最大子数组跨mid,一部分在A[low,mid]中,一部分在A[mid+1, high]中, l o w ≤ i ≤ m i d < j ≤ h i g h low \leq i \leq mid <j \leq high low≤i≤mid<j≤high
当我们采用分治的思想,我们可以发现,原问题可以分解为三个子问题,将原问题的规模降低到一半,接下来考虑合并问题。当问题规模到T(1)的时候,这个时候可以直接计算返回数组的值;比较三种情况中较大的,进行返回。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e5 + 5;
int A[maxn];
int n;
struct ans {
int i;
int j;
int sum;
};
ans max_cross_subarray(const int *a, int low, int mid, int high) {
int lef_sum = -INT_MAX;
int max_left;
int sum = 0;
for (int i = mid; i >= low; i--) {
sum = sum + a[i];
if (lef_sum < sum) {
lef_sum = sum;
max_left = i;
}
}
int right_sum = -INT_MAX;
int max_right;
sum = 0;
for (int i = mid + 1; i <= high; i++) {
sum = sum + a[i];
if (right_sum < sum) {
right_sum = sum;
max_right = i;
}
}
return ans{max_left, max_right, lef_sum + right_sum};
}
ans max_subarray(const int *a, int low, int high) {
if (high == low) {
return ans{low, high, a[low]};
} else {
int mid = (low + high) / 2;
ans left = max_subarray(a, low, mid);
ans right = max_subarray(a, mid + 1, high);
ans cross = max_cross_subarray(a, low, mid, high);
if (left.sum >= right.sum && left.sum >= cross.sum) {
return left;
} else if (right.sum >= left.sum && right.sum >= cross.sum) {
return right;
} else {
return cross;
}
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
ans t = max_subarray(A, 1, n);
// cout << t.i << " " << t.j << " " << t.sum << endl;
cout << t.sum << endl;
return 0;
}
根据分治的思想,我们可以得到上述的算法过程,这里是为了更好的说明思路,函数传参的时候较多,但是C语言传地址可以忽略掉部分常数,不用考虑。
动态规划
算法导论上边儿给出的最后一个思考题4.1-5,思考:
从数组的左边界开始,由左至右处理,记录到目前为止已经处理过的最大的子数组。
如果已知 A[1…j] 的最大子数组,基于以下性质将解扩展成为A[1…j+1]的最大子数组:A[1…j+1]的最大子数组要么是A[1…j]的最大子数组,要么是某个子数组A[i…j+1] (1<=i<=j+1);在已知A[1…j]的最大子数组的情况下,可以在线性时间内找到A[i…j+1]的最大子数组。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e5 + 5;
int A[maxn];
int n;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
int sum[maxn];
int res = INT_MIN;
sum[1] = A[1];
for (int i = 2; i <= n; i++) {
sum[i] = max(sum[i - 1] + A[i], A[i]);
}
for (int i = 1; i <= n; i++) {
res = max(res, sum[i]);
}
cout << res << endl;
return 0;
}
递归式时间复杂度
在分治篇会接触有关递归式的时间复杂度,算法导论上讲解了三种方法,分别是代入法求解递归式,递归树的方法求解,以及使用主方法进行求解。
我们在刻画递归式的时间复杂度的时候,会忽略掉一些细节,比如向上取整,向下取整等
代入法
代入式法,这个方法对于我这种菜鸟来说,难度过大,不太适合。代入法求解递归式分为两步:
- 猜测解的形式
- 用数学归纳法求出解中的常数,并证明解是正确的
啥意思呢?就是我们得先假设应用于较小的值时,将猜测的解带入函数。代入法很强大,但是得猜测出解的形势,以便将其带入才行。比如我们用代入法为递归式建立上界或者下界。我们确定下面的递归式的上界:
T
(
n
)
=
2
T
(
⌊
n
2
⌋
)
+
n
T(n) = 2T( \lfloor \frac{n}{2} \rfloor ) + n
T(n)=2T(⌊2n⌋)+n
我们猜测其解为
T
(
n
)
=
O
(
n
lg
n
)
T(n) = O(n\lg n)
T(n)=O(nlgn)。代入法要求证明,选择常数
c
>
0
c>0
c>0,有
T
(
n
)
≤
c
n
lg
n
T(n) \leq cn\lg n
T(n)≤cnlgn。假定此上界对所有的
m
<
n
m < n
m<n都成立,特别是对于
m
=
⌊
n
2
⌋
m = \lfloor \frac{n}{2} \rfloor
m=⌊2n⌋,有
T
(
⌊
n
2
⌋
)
≤
c
⌊
n
2
⌋
lg
(
⌊
n
2
⌋
)
T(\lfloor \frac{n}{2} \rfloor) \leq c\lfloor \frac{n}{2} \rfloor \lg (\lfloor \frac{n}{2} \rfloor)
T(⌊2n⌋)≤c⌊2n⌋lg(⌊2n⌋)。将其带入递归式,得到:
T
(
n
)
≤
2
(
c
⌊
n
2
⌋
lg
(
⌊
n
2
⌋
)
)
+
n
≤
c
n
lg
(
n
/
2
)
+
n
=
c
n
lg
n
−
c
n
lg
2
+
n
=
c
n
lg
n
−
c
n
+
n
≤
c
n
lg
n
T(n) \leq 2(c\lfloor \frac{n}{2} \rfloor \lg (\lfloor \frac{n}{2} \rfloor)) + n \leq cn\lg(n/2) +n \\ =cn\lg n-cn\lg 2 + n \\ =cn\lg n -cn + n \leq cn\lg n
T(n)≤2(c⌊2n⌋lg(⌊2n⌋))+n≤cnlg(n/2)+n=cnlgn−cnlg2+n=cnlgn−cn+n≤cnlgn
其中,只要
c
≥
1
c \geq 1
c≥1,最后一步都会成立。
可是,当归纳证明的时候,我们发现我们需要证明边界条件,比如当 n=1 的时候,边界条件 T ( n ) ≤ c n lg n T(n) \leq cn\lg n T(n)≤cnlgn 推导出 T ( 1 ) ≤ c 1 lg 1 = 0 T(1) \leq c1 \lg 1 = 0 T(1)≤c1lg1=0,与假设 T ( 1 ) = 1 T(1) = 1 T(1)=1矛盾。因此我们归纳证明的基本情况不成立。
所以要克服一些东西,比如上述提到的反例,对特定的边界条件证明归纳假设成立。例如,渐近符号仅要求我们对 n ≥ n 0 n \geq n_0 n≥n0证明 T ( n ) ≤ c n lg n T(n) \leq cn \lg n T(n)≤cnlgn,其中 n 0 n_0 n0时我们自己可以 选择的常数,保留 T ( 1 ) = 1 T(1) = 1 T(1)=1并且将其从归纳中去除。所以可以考虑 n 0 = 2 , 3 n_0 = 2,3 n0=2,3等等,一次尝试得出一个符合条件的边界。
代入法需要一些技巧性的东西出现:(见算法导论p48-p49)
-
做出好的猜测:可以考虑先假设一个较为接近的下界,然后假设一个较为接近的上界,完了以后逐渐降低上界,提升下界
-
微妙的细节:有的时候假设出正确的渐进界,但是在归纳证明时失败了。问题常常出现在归纳假设不够强,无法证出准确界。这个时候得修改猜测,将它减去一个低阶的项,数学证明就能顺利进行了
-
避免陷阱:必须显示的归纳证明边界关系
-
改变变量: T ( n ) = 2 T ( ⌊ n ⌋ ) + lg n T(n) = 2 T(\lfloor \sqrt{n} \rfloor) + \lg{n} T(n)=2T(⌊n⌋)+lgn,令 m = lg n m = \lg{n} m=lgn得到 T ( 2 m ) = 2 T ( 2 m / 2 ) + m T(2^m) = 2 T(2^{m/2}) + m T(2m)=2T(2m/2)+m的形式,然后重命名 S ( m ) = T ( 2 m ) S(m) = T(2^m) S(m)=T(2m)得到新的递归式 S ( m ) = 2 S ( m / 2 ) + m S(m) = 2 S(m/2) + m S(m)=2S(m/2)+m ,这里取一个有关指数的变化,得到 S ( m ) = O ( m lg m ) S(m) = O(m\lg{m}) S(m)=O(mlgm),然后再换回去得到 T ( n ) = O ( lg n lg lg n ) T(n) = O(\lg{n}\lg{\lg{n}}) T(n)=O(lgnlglgn)
-
技巧:向上取整的时候搞一个(n-a)的形式,去掉上整;求上界的时候,取一个更紧的上界;求下界的时候取一个更紧的下界
递归树
递归树是最适合用来生成好的猜测的,然后通过代入法来验证猜测是否正确。当使用递归树来生成好的猜测时,常常需要忍受一点儿“不精确”,因为稍后才会验证猜测是否正确;但是如果在画递归树和代价求和时非常仔细,就可以用递归树直接证明解是否正确。
以递归式: T ( n ) = 3 T ( n / 4 ) + Θ ( n 2 ) T(n) = 3T(n/4) + \Theta(n^2) T(n)=3T(n/4)+Θ(n2) 为例子
其递归树长酱紫:
可以看到,将原问题递归分解成子问题的时候,会展开成一颗递归树,在分解了i层树的深度,也就是当 n / 4 i = 1 n/4^i = 1 n/4i=1的时候,或等价于 i = log 4 n i = \log_4 n i=log4n的时候,子问题规模变成1。因此,递归树的深度为 log 4 n + 1 \log_4n + 1 log4n+1层(深度为0, 1, 2, …, log 4 n − 1 \log_4n -1 log4n−1)。
接下来确定树的每一层的代价。将递归树的每层的代价进行相加,完了以后就可以得到全部的代价:
T
(
n
)
=
c
n
2
+
3
16
c
n
2
+
(
3
16
)
2
c
n
2
+
.
.
.
+
(
3
16
)
log
4
n
−
1
c
n
2
+
Θ
(
n
log
4
3
)
=
∑
i
=
0
log
4
n
−
1
(
3
16
)
i
c
n
2
+
Θ
(
n
log
4
3
)
=
(
3
/
16
)
log
4
n
−
1
(
3
/
16
)
−
1
c
n
2
+
Θ
(
n
log
4
3
)
T(n) = cn^2 + \frac{3}{16} cn^2+ (\frac{3}{16})^2 cn^2+...+(\frac{3}{16})^{\log_4{n-1}} cn^2+\Theta(n^{\log_4{3}})\\ =\sum_{i=0}^{\log_4{n-1}}{(\frac{3}{16})^i cn^2} + \Theta(n^{\log_4{3}})\\ = \frac{(3/16)^{\log_4{n}}-1}{(3/16)-1}cn^2+\Theta(n^{\log_4{3}})
T(n)=cn2+163cn2+(163)2cn2+...+(163)log4n−1cn2+Θ(nlog43)=i=0∑log4n−1(163)icn2+Θ(nlog43)=(3/16)−1(3/16)log4n−1cn2+Θ(nlog43)
然后使用一定的不精确,使用无线递减作为几何级数作为上界:
T
(
n
)
=
∑
i
=
0
log
4
n
−
1
(
3
16
)
i
c
n
2
+
Θ
(
n
log
4
3
)
<
∑
i
=
0
∞
(
3
16
)
i
c
n
2
+
Θ
(
n
log
4
3
)
=
1
1
−
(
3
/
16
)
c
n
2
+
Θ
(
n
log
4
3
)
=
16
13
c
n
2
+
Θ
(
n
log
4
3
)
=
O
(
n
2
)
T(n) =\sum_{i=0}^{\log_4{n-1}}{(\frac{3}{16})^i cn^2} + \Theta(n^{\log_4{3}}) < \sum_{i=0}^{\infty}{(\frac{3}{16})^i cn^2} + \Theta(n^{\log_4{3}}) \\ =\frac{1}{1-(3/16)}cn^2 + \Theta(n^{\log_4{3}}) \\ = \frac{16}{13}cn^2 + \Theta(n^{\log_4{3}}) \\ = O(n^2)
T(n)=i=0∑log4n−1(163)icn2+Θ(nlog43)<i=0∑∞(163)icn2+Θ(nlog43)=1−(3/16)1cn2+Θ(nlog43)=1316cn2+Θ(nlog43)=O(n2)
我们可以推导出一个猜测的上界,我们可以发现整棵树的时间代价消耗,由根节点所支配。
接下来,就是利用代入法进行验证,归纳证明该渐进上界的猜想是正确的。
主方法
主方法为如下形式的递归式提供了一种求解方法,这也是最便捷的解法:
T
(
n
)
=
a
T
(
n
/
b
)
+
f
(
n
)
T(n) = aT(n/b) + f(n)
T(n)=aT(n/b)+f(n)
其中
a
≥
1
,
b
>
1
a \geq 1,b > 1
a≥1,b>1是常数,
f
(
n
)
f(n)
f(n)是渐进正函数。首先,我们需要注意的是,我们将一个问题划分为了a个子问题,其中每个子问题的规模为(n/b)。a个子问题递归的进行求解,每个划分时间T(n/b)。函数f(n)包含了问题的分解和合并子问题的解的合并代价。从技术上来说,写成n/b的形式不是很好,因为不是整数,但是算法导论上证明了向上或者向下取整并不影响递归式的渐进性质。
主定理
主方法依赖于以下定理:
主定理,令
a
≥
1
,
b
>
1
a \geq 1, b >1
a≥1,b>1是常数,
f
(
n
)
f(n)
f(n)是一个函数,
T
(
n
)
T(n)
T(n)是定义再非负整数上的递归式:
T
(
n
)
=
a
T
(
n
/
b
)
+
f
(
n
)
T(n) = aT(n/b) + f(n)
T(n)=aT(n/b)+f(n)
其中我们将
n
/
b
n/b
n/b解释为
⌊
n
/
b
⌋
\lfloor n/b \rfloor
⌊n/b⌋或
⌈
n
/
b
⌉
\lceil n/b \rceil
⌈n/b⌉。那么T(n)有如下渐进界:
- 情况1:若对某个常数 ϵ > 0 \epsilon > 0 ϵ>0有 f ( n ) = O ( n log b a − ϵ ) f(n) = O(n^{\log_b{a-\epsilon}}) f(n)=O(nlogba−ϵ),则 T ( n ) = Θ ( n log b a ) T(n) = \Theta(n^{\log_b{a}}) T(n)=Θ(nlogba)
- 情况2:若 f ( n ) = O ( n log b a ) f(n) = O(n^{\log_b{a}}) f(n)=O(nlogba),则 T ( n ) = Θ ( n log b a lg n ) T(n) = \Theta(n^{\log_b{a}} \lg n) T(n)=Θ(nlogbalgn)
- 情况3:若对某个常数 ϵ > 0 \epsilon > 0 ϵ>0有 f ( n ) = Ω ( n log b a + ϵ ) f(n) = \Omega(n^{\log_b{a + \epsilon}}) f(n)=Ω(nlogba+ϵ),且对某个常数 c < 1 c < 1 c<1和所有足够大的n有 a f ( n / b ) ≤ c f ( n ) af(n/b) \leq cf(n) af(n/b)≤cf(n)则 T ( n ) = Θ ( f ( n ) ) T(n) = \Theta(f(n)) T(n)=Θ(f(n))
主定理描述的三种情况可以直观的理解为,我们将函数 f ( n ) f(n) f(n)与函数 n log b a n^{\log_b{a}} nlogba进行比较,谁更大,谁决定了递归式的解。如果相等的话,则乘上一个对数因子。
需要注意的是,这里的“大”是多项式意义上的大,也就是 n log b a n^{\log_b{a}} nlogba和 f ( n ) f(n) f(n)之间要相差一个因子 n ϵ n^{\epsilon} nϵ。而且这三种情况并没有覆盖所有的情况,有可能落入三种情况的间隙的。
使用主方法
T ( n ) = 9 T ( n / 3 ) + n T(n) = 9T(n/3) + n T(n)=9T(n/3)+n
a
=
9
,
b
=
3
,
f
(
n
)
=
n
a = 9, b = 3, f(n) = n
a=9,b=3,f(n)=n, 因此
n
log
b
a
=
n
log
3
9
=
Θ
(
n
2
)
n^{\log_b{a}} = n ^{\log_3{9}} = \Theta(n^2)
nlogba=nlog39=Θ(n2)。由于
f
(
n
)
=
O
(
n
log
3
9
−
ϵ
)
f(n) = O(n^{\log_3{9 - \epsilon}})
f(n)=O(nlog39−ϵ),其中
ϵ
=
1
\epsilon = 1
ϵ=1,因此可以使用主定理的情况1,从而得到
T
(
n
)
=
O
(
n
2
)
T(n) = O(n^2)
T(n)=O(n2)
T
(
n
)
=
T
(
2
n
/
3
)
+
1
T(n) = T(2n/3) + 1
T(n)=T(2n/3)+1
a
=
1
,
b
=
3
/
2
,
f
(
n
)
=
1
a = 1,b=3/2,f(n)=1
a=1,b=3/2,f(n)=1,因此
n
log
b
a
=
n
log
3
/
2
1
=
n
0
=
1
n^{\log_b{a}} = n^{\log_{3/2}{1}} = n^0 = 1
nlogba=nlog3/21=n0=1。由于
f
(
n
)
=
Θ
(
n
log
b
a
)
=
Θ
(
1
)
f(n) = \Theta(n^{\log_b{a}}) = \Theta(1)
f(n)=Θ(nlogba)=Θ(1),因此对应情况2,从而得到解
T
(
n
)
=
Θ
(
lg
n
)
T(n) = \Theta(\lg{n})
T(n)=Θ(lgn)。
T
(
n
)
=
3
T
(
n
/
4
)
+
n
lg
n
T(n) = 3T(n/4) + n\lg n
T(n)=3T(n/4)+nlgn
a
=
3
,
b
=
4
,
f
(
n
)
=
n
lg
n
a = 3,b=4,f(n)=n\lg{n}
a=3,b=4,f(n)=nlgn,因此
n
log
b
a
=
n
log
4
3
=
O
(
n
0.793
)
n^{\log_b{a}} = n^{\log_4{3}} = O(n^{0.793})
nlogba=nlog43=O(n0.793)。由于
f
(
n
)
=
Ω
(
n
log
4
3
+
ϵ
)
f(n) = \Omega(n^{\log_4{3+\epsilon}})
f(n)=Ω(nlog43+ϵ),其中
ϵ
≈
0.2
\epsilon \approx 0.2
ϵ≈0.2,因此如果可以证明正则条件则可以使用情况3。当
c
=
3
4
c = \frac{3}{4}
c=43时,满足该条件,得到
T
(
n
)
=
O
(
n
lg
n
)
T(n) = O(n\lg n)
T(n)=O(nlgn)
不能使用主方法
T ( n ) = 2 T ( n / 2 ) + n lg n T(n) = 2T(n/2) + n\lg{n} T(n)=2T(n/2)+nlgn
a = 2 , b = 2 , f ( n ) = n lg n a=2,b=2,f(n)=n\lg{n} a=2,b=2,f(n)=nlgn,以及 n log b a = n n^{\log_b{a}} = n nlogba=n。看起来应该使用情况3,实际上这是不符合多项式意义的。
对于任意正常数 ϵ \epsilon ϵ,比值 f ( n ) / n log b a = ( n lg n ) / n = lg n f(n)/n^{\log_b{a}} = (n\lg{n})/n = \lg{n} f(n)/nlogba=(nlgn)/n=lgn都渐进小于 n ϵ n^{ \epsilon } nϵ。因此,递归式落入了情况2和情况3之间的间隙。
最近点对
问题:给定平面中的n个点,找到最近点对。
没搞定啊还。。。
整数乘法
具体的例子是说,两个数
x
,
y
x,y
x,y相乘,将
x
x
x写成
x
=
x
1
∗
2
n
/
2
+
x
0
x = x_1 * 2^{n/2} + x_0
x=x1∗2n/2+x0,
y
=
y
1
∗
2
n
/
2
+
y
0
y=y_1 * 2^{n/2}+y_0
y=y1∗2n/2+y0,利用二进制的思想将问题划分为4个子问题,每个子问题规模为n/2,其时间复杂度为:
T
(
n
)
=
4
T
(
n
/
2
)
+
c
n
T(n) = 4T(n/2) + cn
T(n)=4T(n/2)+cn
此时该时间复杂度仍然为
O
(
n
2
)
O(n^2)
O(n2),考虑简化问题,
(
x
1
+
x
0
)
(
y
1
+
y
0
)
=
x
1
y
1
+
x
0
y
1
+
x
1
y
0
+
x
0
y
0
(x_1 + x_0)(y_1 + y_0) = x_1y_1+x_0y_1+x_1y_0+x_0y_0
(x1+x0)(y1+y0)=x1y1+x0y1+x1y0+x0y0,其中中间两项通过左边儿减去外边儿两项得到。此时问题就只需要进行三次递归,子问题的规模降低:
T
(
n
)
=
3
T
(
n
/
2
)
+
c
n
T(n) = 3T(n/2) + cn
T(n)=3T(n/2)+cn
在处理代码的细节的过程中,并未有想到长度对齐,因此我参考了这个同学的代码,并且学习他的思路进行了代码的手动实现:https://www.jianshu.com/p/b5af56d676b2
#include <bits/stdc++.h>
using namespace std;
// big number a and b
string a, b;
string res;
bool check(string &x, string &y);
void add_pre_zero(string &x, int n);
void add_last_zero(string &x, int n);
void del_zero(string &x);
string add(string x, string y);
string subtract(string x, string y);
int string2int(string x);
int binary_align(string &x, string &y);
string recursive_mutiply(string x, string y);
int main() {
cin >> a;
cin >> b;
cout << recursive_mutiply(a, b) << endl;
return 0;
}
/*
* check positive or negative
* true: positive
* false: negative
*/
bool check(string &x, string &y) {
bool flag_x = true;
bool flag_y = true;
if (x.at(0) == '-') {
flag_x = false;
x = x.substr(1);
}
if (y.at(0) == '-') {
flag_y = false;
y = y.substr(1);
}
if ((flag_x && flag_y) || (!flag_x && !flag_y)) {
return true;
}
return false;
}
void add_pre_zero(string &x, int n) {
for (int i = 0; i < n; i++) {
x.insert(0, "0");
}
}
void add_last_zero(string &x, int n) {
for (int i = 0; i < n; i++) {
x = x + "0";
}
}
int string2int(string x) {
int result;
stringstream instr(x);
instr >> result;
return result;
}
/*
* delete the zero, like 0003574, the pre zeros
*/
void del_zero(string &x) {
int len = x.length();
if (len == 1) return;
int k = 0;
for (int i = 0; i < len; i++) {
if (x.at(i) == '0') {
k++;
} else {
break;
}
}
if (k == len) {
x = "0";
} else {
x = x.substr(k);
}
}
/*
* two big num add
* using string to simulate
*/
string add(string x, string y) {
string result;
del_zero(x);
del_zero(y);
reverse(x.begin(), x.end());
reverse(y.begin(), y.end());
int len_x = x.length();
int len_y = y.length();
int len_res = max(len_y, len_x);
int i = 0;
int tmp = 0;
while (i < len_res || tmp) {
if (i < len_x) tmp = tmp + (x[i] - '0');
if (i < len_y) tmp = tmp + (y[i] - '0');
result.insert(0, to_string(tmp % 10));
tmp = tmp / 10;
i++;
}
return result;
}
string subtract(string x, string y) {
string result = "";
del_zero(x);
del_zero(y);
int borrow = 0;
int i = x.size() - 1;
int j = y.size() - 1;
while (i >= 0 || j >= 0) {
int xx = i >= 0 ? x[i] - '0' : 0;
int yy = j >= 0 ? y[j] - '0' : 0;
int zz = (xx - borrow - yy + 10) % 10;
result = result + to_string(zz);
borrow = xx - borrow - yy < 0 ? 1 : 0;
i--, j--;
}
reverse(result.begin(), result.end());
int pos;
int res_len = result.size();
for (pos = 0; pos < res_len; pos++) {
if (result[pos] != '0') break;
}
result = result.substr(pos);
return result;
}
int binary_align(string &x, string &y) {
int init_len = 4;
int len_x = x.length();
int len_y = y.length();
if (len_x > 2 || len_y > 2) {
if (len_x >= len_y) {
while (init_len < len_x) init_len = init_len * 2;
if (len_x != init_len) {
add_pre_zero(x, init_len - len_x);
}
add_pre_zero(y, init_len - len_y);
} else {
while (init_len < len_y) init_len = init_len * 2;
if (len_y != init_len) {
add_pre_zero(y, init_len - len_y);
}
add_pre_zero(x, init_len - len_x);
}
}
if (x.length() == 1) add_pre_zero(x, 1);
if (y.length() == 1) add_pre_zero(y, 1);
return x.length();
}
string recursive_mutiply(string x, string y) {
string result;
string x1, x0, y1, y0;
bool sym;
sym = check(x, y);
int n = binary_align(x, y);
if (n > 1) {
x1 = x.substr(0, n / 2);
x0 = x.substr(n / 2, n);
y1 = y.substr(0, n / 2);
y0 = y.substr(n / 2, n);
}
if (n == 2) {
int a1 = string2int(x1);
int a0 = string2int(x0);
int b1 = string2int(y1);
int b0 = string2int(y0);
int z = (a1 * 10 + a0) * (b1 * 10 + b0);
result = to_string(z);
} else {
string x1y1 = recursive_mutiply(x1, y1);
string x0y0 = recursive_mutiply(x0, y0);
string x1andx0 = add(x1, x0);
string y1andy0 = add(y1, y0);
string p = recursive_mutiply(x1andx0, y1andy0);
string tmp = add(x1y1, x0y0);
string tmp_1 = subtract(p, tmp);
add_last_zero(tmp_1, n / 2);
add_last_zero(x1y1, n);
result = add(add(x1y1, tmp_1), x0y0);
}
if (!sym)
result.insert(0, "-");
return result;
}
上述代码是实现10进制的大数乘法,其中有很多细节的地方:
x
∗
y
=
(
x
1
∗
1
0
n
/
2
+
x
0
)
(
y
1
∗
1
0
n
/
2
+
y
0
)
x*y = (x1*10^{n/2}+x0)(y1*10^{n/2}+y0)
x∗y=(x1∗10n/2+x0)(y1∗10n/2+y0)
其中,令
c
0
=
x
1
y
1
,
c
1
=
x
0
y
0
,
p
=
(
x
1
+
x
0
)
(
y
1
+
y
0
)
c0=x1y1,c1=x0y0,p=(x1+x0)(y1+y0)
c0=x1y1,c1=x0y0,p=(x1+x0)(y1+y0)原式子化简为
x
∗
y
=
c
0
∗
1
0
n
+
(
p
−
c
0
−
c
1
)
∗
1
0
n
/
2
+
c
1
x*y=c0*10^{n}+(p-c0-c1)*10^{n/2}+c1
x∗y=c0∗10n+(p−c0−c1)∗10n/2+c1,这样下来,就可以达到分治的时候,递归树的叶子节点只有3,达到了降低计算规模的目的。
- 首先就是对于数字的位数划分,应该化为2的整数倍
- 然后就是需要实现大数加减法、正负检测
- 同时需要将两个数进行长度对齐