1.定义
分治算法(Divide and Conquer Algorithm)是一种通过将复杂问题分解为多个较小的子问题来解决的方法。这些子问题的形式与原问题相同或相似,且彼此独立。通过递归地解决这些子问题,并将它们的解合并起来,最终得到原问题的解。这种方法在排序(如快速排序、归并排序)、查找(如二分查找)和数学计算(如大整数乘法)等领域有广泛的应用。
2.基本思路
1.分解:
- 目标:将原问题分解为若干个规模较小、相互独立的子问题。
- 方法:
- 确定分解的标准或条件。
- 根据标准或条件将原问题划分为多个子问题。
- 每个子问题的形式与原问题相同或相似。
2.解决:
- 目标:递归地求解这些子问题。
- 方法:
- 如果子问题的规模足够小,可以直接求解,不再继续分解。
- 否则,继续递归地应用分治算法,将子问题进一步分解,直到达到可以直接求解的规模。
3.合并:
- 目标:将子问题的解合并起来,形成原问题的解。
- 方法:
- 设计合适的合并策略,将子问题的解组合成原问题的解。
- 合并过程中可能涉及一些额外的计算或操作。
3.操作步骤
1. 分解
目标:将原问题分解为若干个规模较小、相互独立的子问题。
操作步骤:1.确定分解标准:根据问题的特点,选择合适的分解标准或条件。
2.划分子问题:按照分解标准将原问题划分为多个子问题。
3.确保子问题形式相同:确保每个子问题的形式与原问题相同或相似。
2. 解决
目标:递归地求解这些子问题。
操作步骤:1.检查子问题规模:判断子问题的规模是否足够小,可以直接求解。
2.直接求解小规模问题:如果子问题的规模足够小,直接求解,不再继续分解。
3.递归求解大规模问题:如果子问题的规模仍然较大,继续递归地应用分治算法,将子问题进一步分解,直到达到可以直接求解的规模。
3. 合并
目标:将子问题的解合并起来,形成原问题的解。
操作步骤:1.设计合并策略:根据问题的特点,设计合适的合并策略。
2.执行合并操作:将子问题的解组合成原问题的解。
3.处理合并过程中的额外计算:合并过程中可能涉及一些额外的计算或操作,确保这些操作正确无误。
4.代码模板
1.通用分治算法模板(C++)
#include <iostream>
#include <vector>
using namespace std;
// 基本情况:如果问题规模足够小,直接求解
bool is(const vector<int>& p) {
// 根据问题的具体情况判断
return p.size() <= 1;
}
// 直接求解基本情况
vector<int> solve(const vector<int>& p) {
// 根据问题的具体情况求解
return p;
}
// 分解问题
vector<vector<int>> di(const vector<int>& p) {
// 根据问题的具体情况分解
int n = p.size(),mid = n / 2;
vector<int> left(p.begin(), p.begin() + mid);
vector<int> right(p.begin() + mid, p.end());
return {left, right};
}
// 合并子问题的解
vector<int> com(const vector<vector<int>>& s) {
// 根据问题的具体情况合并
vector<int> r;
for (const auto& sub : s) {
r.insert(r.end(), s.begin(), s.end());
}
return r;
}
// 分治算法主函数
vector<int> d(const vector<int>& p) {
if (is(p))
return solve(p);
vector<vector<int>> su = di(p);
vector<vector<int>> s;
for (const auto& subproblem : subproblems) {
su.push_back(di(s));
}
return com(s);
}
2.快速排序的具体实现(C++)
#include <iostream>
#include <vector>
using namespace std;
// 分区操作
int pa(vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++)
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]);
}
swap(arr[i + 1], arr[high]);
return i + 1;
}
// 快速排序主函数
void qu(vector<int>& arr, int low, int high) {
if (low < high) {
int pi = pa(arr, low, high);
qu(arr, low, pi - 1); // 递归排序左半部分
qu(arr, pi + 1, high); // 递归排序右半部分
}
}
int main() {
vector<int> arr = {10, 7, 8, 9, 1, 5};
int n = arr.size();
qu(arr, 0, n - 1);
cout << "Sorted array: ";
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << endl;
return 0;
}
5.经典例题
1. [NOIP1998 普及组] 幂次方
题目描述
任何一个正整数都可以用 2 的幂次方表示。例如 137=2^7+2^3+2^0 。同时约定次方用括号来表示,即 a^b 可表示为 a(b)。由此可知,137 可表示为 2(7)+2(3)+2(0)
进一步:7= 2^2+2+2^0 ( 2^1 用 2 表示),并且 3=2+2^0。所以最后 137 可表示为 2(2(2)+2+2(0))+2(2+2(0))+2(0)。又如 1315=2^10 +2^8 +2^5 +2+1
所以 1315 最后可表示为 2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)。
输入格式
一行一个正整数 n。
输出格式
符合约定的 n 的 0, 2 表示(在表示中不能有空格)。
样例 #1
样例输入 #1
1315
样例输出 #1
2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)
提示
【数据范围】对于 100% 的数据,1 < n < 2 *10^4。
NOIP1998 普及组 第三题
参考代码:
#include <iostream>
using namespace std;
// 定义全局变量 n
int n;
// 计算 m 的二进制表示中最高位 1 的位置(即 log2(m))
int log(int m) {
int cnt = 0;
// 不断右移 m,直到 m 变为 1
while (m != 1) {
m >>= 1; // 右移一位
cnt++; // 计数器加一
}
return cnt; // 返回计数器的值
}
// 递归解决将 n 表示为 2 的幂次和的形式
void solve(int n) {
if (n == 0) {
cout << "0"; // 如果 n 为 0,直接输出 0
} else if (n == 2) {
cout << "2"; // 如果 n 为 2,直接输出 2
} else if (n == 3) {
cout << "2+2(0)"; // 如果 n 为 3,输出 2+2(0)
} else {
int l = log(n); // 计算 n 的最高位 1 的位置
cout << "2("; // 输出 2(
solve(l); // 递归处理 l
cout << ")"; // 输出 )
n -= (1 << l); // 减去 2^l
if (n) {
cout << "+"; // 如果 n 还有剩余,输出 +
solve(n); // 递归处理剩余的 n
}
}
return;
}
int main() {
cin >> n; // 读取输入的 n
solve(n); // 调用 solve 函数处理 n
return 0;
}
2. [NOIP2001 提高组] 一元三次方程求解
题目描述
有形如:a x^3 + b x^2 + c x + d = 0 这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在 -100 至 100 之间),且根与根之差的绝对值 >1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后 2 位。
提示:记方程 f(x) = 0,若存在 2 个数 x1 和 x2,且 x1<x2,f(x1) f(x2) < 0,则在 (x1, x2) 之间一定有一个根。
输入格式
一行,4 个实数 a, b, c, d。
输出格式
一行,3 个实根,从小到大输出,并精确到小数点后 2 位。
样例 #1
样例输入 #1
1 -5 -4 20
样例输出 #1
-2.00 2.00 5.00
提示
【题目来源】NOIP 2001 提高组第一题
#include <bits/stdc++.h>
using namespace std;
// 定义全局变量 a, b, c, d
double a, b, c, d;
int main() {
// 从标准输入读取四个双精度浮点数 a, b, c, d
cin >> a >> b >> c >> d;
// 使用 for 循环在区间 [-100, 100] 内以 0.001 的步长遍历所有可能的 i 值
for (double i = -100; i <= 100; i += 0.001) {
// 计算多项式 f(i) = a*i^3 + b*i^2 + c*i + d 的值
double value = i * i * i * a + i * i * b + i * c + d;
// 判断 f(i) 是否接近于 0(误差小于 0.0001)
if (fabs(value) < 0.0001) {
// 如果 f(i) 接近于 0,输出当前的 i 值
cout << i << endl;
}
}
return 0;
}
3.最大子段和
题目描述
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
输入格式
第一行是一个整数,表示序列的长度 n。
第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 ai。
输出格式
输出一行一个整数表示答案。
样例 #1
样例输入 #1
7
2 -4 3 -1 2 -4 3
样例输出 #1
4
提示
样例 1 解释
选取 [3, 5] 子段 {3, -1, 2},其和为 4。
数据规模与约定
- 对于 40% 的数据,保证 n <= 2 *10^3。
- 对于 100% 的数据,保证 1 <= n <= 2 *10^5,-10^4 <= ai<=10^4。
#include <bits/stdc++.h>
using namespace std;
// 定义两个数组 a 和 s,用于存储输入数据和前缀和
long long a[200005], s[200005];
int main() {
// 读取输入的整数 n,表示数组的长度
long long n;
cin >> n;
// 初始化最大子数组和为一个很小的负数
long long sum = -1000000;
// 读取数组 a 的元素
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// 计算前缀和数组 s,并更新最大子数组和
for (int i = 1; i <= n; i++) {
// 计算当前前缀和 s[i]
s[i] = s[i - 1] + a[i];
// 如果当前元素 a[i] 大于当前前缀和 s[i],则更新 s[i] 为 a[i]
if (a[i] > s[i]) {
s[i] = a[i];
}
// 更新最大子数组和 sum
if (sum < s[i]) {
sum = s[i];
}
}
// 输出最大子数组和
cout << sum;
return 0;
}