分治策略解题报告
分治法的原理
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
简单的说,分治就是分而治之,把一个问题拆分成几个小问题,最后再汇总解决的办法。
有两点需要记住:
(1) 分治法基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同。
(2)递归的解这些子问题,然后将各子问题的解合并得到原问题的解。
题目1.求两个n位的(二进制、十进制)长整数乘积的高效算法
(1)两个1位数相乘视为基本运算
(2)两个1位数相乘运算量远高于其相加
算法思路
这里我们假设有两个大整数X、Y,分别设X=1234、Y=5678。现在要求X*Y的乘积,暴力的算法就是把X与Y中的每一项去乘,但是这样的乘法所需的时间复杂度为,效率比较低下。
那我们可以采用分治的算法,将X、Y分别拆分为A与B、C与D,如下图:
X
∗
Y
=
(
A
∗
1
0
n
/
2
+
B
)
∗
(
C
∗
1
0
n
/
2
+
D
)
=
A
C
∗
1
0
n
+
(
A
D
+
B
C
)
∗
1
0
n
/
2
+
B
D
…
…
(
1
)
X * Y = (A * 10^{n/2} + B) * (C * 10^{n/2} + D) = AC * 10^n + (AD + BC) * 10^{n/2} + BD……(1)
X∗Y=(A∗10n/2+B)∗(C∗10n/2+D)=AC∗10n+(AD+BC)∗10n/2+BD……(1)
此处的n为X、Y的位数。
算法分析
- 首先将X和Y分成A,B,C,D
- 此时将X和Y的乘积转化为(1)式,把问题转化为求解因式分解的值
在(1)式中,我们一共需要进行4次n/2的乘法(AC2次,AD、BC各一次)和3次加法,因而该算法的时间复杂度为:
T
(
n
)
=
4
∗
T
(
n
/
2
)
+
θ
(
n
)
T(n) = 4 * T(n/2) + θ(n)
T(n)=4∗T(n/2)+θ(n)
计算求得:
T
(
n
)
=
θ
(
n
2
)
T(n) = θ(n^2)
T(n)=θ(n2)
相当于暴力求解。
但是我们再来看看,我们是否可以用加法来换取乘法?因为多一个加法操作,也是常数项,对时间复杂度没有影响,如果减少一个乘法则不同。
(1)式可以化为:
X
∗
Y
=
A
C
∗
1
0
n
+
(
(
A
+
B
)
∗
(
C
+
D
)
−
A
C
−
B
D
)
∗
1
0
n
/
2
+
B
D
X * Y = AC * 10^n + ((A + B) * (C + D) - AC - BD) * 10^{n/2} + BD
X∗Y=AC∗10n+((A+B)∗(C+D)−AC−BD)∗10n/2+BD
或者
X
∗
Y
=
A
C
∗
1
0
n
+
(
(
A
−
B
)
∗
(
D
−
C
)
+
A
C
+
B
D
)
∗
1
0
n
/
2
+
B
D
或者 X * Y = AC * 10^n + ((A - B) * (D - C) + AC + BD) * 10^{n/2} + BD
或者X∗Y=AC∗10n+((A−B)∗(D−C)+AC+BD)∗10n/2+BD
此处的主要变换为:
A
D
+
B
C
=
(
A
+
B
)
∗
(
C
+
D
)
−
A
C
−
B
D
=
(
A
−
B
)
∗
(
D
−
C
)
+
A
C
+
B
D
AD + BC = (A + B) * (C + D) - AC - BD = (A - B) * (D - C) + AC + BD
AD+BC=(A+B)∗(C+D)−AC−BD=(A−B)∗(D−C)+AC+BD
省去了一步乘法,因为乘法的耗时比加法高很多。
现在的时间复杂度为:
T
(
n
)
=
3
∗
T
(
n
/
2
)
+
θ
(
n
)
T(n) = 3 * T(n/2) + θ(n)
T(n)=3∗T(n/2)+θ(n)
计算求得:
T
(
n
)
=
O
(
n
l
o
g
2
(
3
)
)
=
O
(
n
1.585
)
T(n) = O(n^{log2(3)}) = O(n^{1.585})
T(n)=O(nlog2(3))=O(n1.585)
时间复杂度有较明显的降低。
C++代码实现
#include <iostream>
#include <cmath>
using namespace std;
#define LL long long
int SIGN(LL A)
{
return (A > 0) - (A < 0); // 处理 A = 0 的情况
}
LL IntegerMultiply(LL x, LL y, int n)
{
if (x == 0 || y == 0) // 乘数有一个为 0,两数相乘为 0
return 0;
LL sign = SIGN(x) * SIGN(y); // 确定结果的符号
x = abs(x);
y = abs(y);
if (n == 1) // 只剩一位时,返回简单运算
return sign * (x * y);
int half = n / 2;
// 计算 10^half 的值
LL base = 1;
for (int i = 0; i < half; ++i)
{
base *= 10;
}
// 分割 x 和 y
LL a = x / base;
LL b = x % base;
LL c = y / base;
LL d = y % base;
LL ac = IntegerMultiply(a, c, half); // 计算 AC
LL bd = IntegerMultiply(b, d, half); // 计算 BD
LL abcd = IntegerMultiply(a + b, c + d, half) - ac - bd; // 计算 AD + BC
// 组合结果
return sign * (ac * (base * base) + abcd * base + bd);
}
int main()
{
LL x, y;
cin >> x >> y;
// 计算位数 n
LL n = 0;
LL maxVal = max(abs(x), abs(y));
while (maxVal != 0)
{
maxVal /= 10;
n++;
}
if (n == 0)
n = 1; // 确保 n 至少为 1
cout << "直接相乘 X * Y = " << x * y << endl;
cout << "分治相乘 X * Y = " << IntegerMultiply(x, y, n) << endl;
return 0;
}
测试结果
1234 5678
直接相乘 X * Y = 7006652
分治相乘 X * Y = 7006652
123456 456789
直接相乘 X * Y = 56393342784
分治相乘 X * Y = 56393342784
1234 -5678
直接相乘 X * Y = -7006652
分治相乘 X * Y = -7006652
-123456 -456789
直接相乘 X * Y = 56393342784
分治相乘 X * Y = 56393342784
可见得出结果是正确的。
题目2.直线x上分散有n个点(n个点的位置记为x1,x2,…,xn),求距离最近的点对
算法思路
这个算法使用分治法来解决寻找一维坐标系中最近点对的问题。其基本思路可以概括为以下几个步骤:
- 排序:
- 首先对所有点进行排序,以便于后续的分治操作。
- 递归分治:
- 将点集分为左右两部分,分别计算每部分中的最近点对。
- 递归的基准情况是,当点的数量小于或等于1时,返回一个无穷大的距离。
- 合并结果:
- 在左半部分和右半部分中找到最近的点对后,取这两个点对中的最小距离
d
。 - 然后,创建一个带状区域(strip),该区域包含与中点距离小于
d
的点。 - 调用
stripClosest
函数来查找带状区域内的最近点对,并更新最小距离及对应的点对。
- 在左半部分和右半部分中找到最近的点对后,取这两个点对中的最小距离
- 返回结果:
- 最终返回最小的距离及相应的点对。
算法分析
-
排序:初始的排序操作需要 O(nlogn) 的时间复杂度。
-
递归调用:
- 每次分治将点集分为两个部分,因此递归的深度是 O(nlogn)。
- 在每一层递归中,需要处理带状区域的点。
- 创建带状区域的时间复杂度是 O(n),因为要遍历所有点。
- 在带状区域内寻找最近点对的时间复杂度是 O(n),因为可能需要比较所有点。
-
总时间复杂度:
-
因此,整个算法的时间复杂度可以表示为:
T ( n ) = 2 T ( n / 2 ) + O ( n ) T(n) = 2T(n/2)+O(n) T(n)=2T(n/2)+O(n) -
根据master定理,这个递归关系的解为:
T ( n ) = O ( n l o g n ) T(n) = O(nlogn) T(n)=O(nlogn)
-
总结
因此,该算法可以在 O(nlogn) 的时间复杂度下高效地找到一维坐标系中最近的点对。这种方法相较于简单的 O(n^2)的暴力搜索方法具有显著的性能优势。
代码实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <limits>
using namespace std;
// 检查带状区域内的点对
double stripClosest(const vector<double> &strip, double d, pair<double, double> &closestPair)
{
double minDist = d;
vector<double> sortedStrip = strip; // 创建副本进行排序
sort(sortedStrip.begin(), sortedStrip.end());
for (size_t i = 0; i < sortedStrip.size(); ++i)
{
for (size_t j = i + 1; j < sortedStrip.size() && (sortedStrip[j] - sortedStrip[i]) < minDist; ++j)
{
if (double dist = sortedStrip[j] - sortedStrip[i]; dist < minDist)
{
minDist = dist;
closestPair = {sortedStrip[i], sortedStrip[j]};
}
}
}
return minDist;
}
// 分治法求解最近点对
pair<double, pair<double, double>> closestPairRecursive(vector<double> &points, int left, int right)
{
if (right - left <= 1)
return {numeric_limits<double>::infinity(), {0, 0}};
int mid = left + (right - left) / 2;
double midPoint = points[mid];
auto leftPair = closestPairRecursive(points, left, mid);
auto rightPair = closestPairRecursive(points, mid, right);
double d = min(leftPair.first, rightPair.first);
pair<double, double> closestPair = (d == leftPair.first) ? leftPair.second : rightPair.second;
vector<double> strip;
for (int i = left; i < right; ++i)
if (abs(points[i] - midPoint) < d)
strip.push_back(points[i]);
double dStrip = stripClosest(strip, d, closestPair);
return {min(d, dStrip), closestPair};
}
// 主函数
int main()
{
int n;
cout << "请输入点的数量: ";
cin >> n;
if (n < 2)
{
cout << "没有足够的点进行计算。" << endl;
return 0;
}
vector<double> points(n);
cout << "请输入点的坐标: ";
for (double &point : points)
cin >> point;
sort(points.begin(), points.end());
auto result = closestPairRecursive(points, 0, n);
if (result.first == numeric_limits<double>::infinity())
{
cout << "没有足够的点进行计算。" << endl;
}
else
{
cout << "最小距离为: " << result.first << endl;
cout << "最近的点对是: (" << result.second.first << ", " << result.second.second << ")" << endl;
}
return 0;
}
测试结果
请输入点的数量: 10 1 3 5 -2 -5 1.4 1.6 9 -0.6 1.3
请输入点的坐标: 最小距离为: 0.1
最近的点对是: (1.3, 1.4)
请输入点的数量: 15 0 1 -2 1.55 12 -4 22 4 6 -22 43 -0.4 1.39 11 -3
请输入点的坐标: 最小距离为: 0.16
最近的点对是: (1.39, 1.55)
可见测试结果是比较正确的。