算法复习
递归和分治是计算机科学中的两个重要概念,它们都用于解决复杂的问题。
1. 递归(Recursion):
递归是一种解决问题的方法,它通过解决一个问题来解决一个更小的实例。递归函数通常包含一个或多个基本情况(base cases),这些基本情况不需要递归就可以解决。递归函数通常包含两个部分:基准情况(base case)和递归情况(recursive case)。
在C++中,递归函数的基本结构如下:
void recursiveFunction(int parameter) {
// Base case
if (parameter satisfies some condition) {
// Do something and return
}
// Recursive case
else {
// Modify the parameter
recursiveFunction(modifiedParameter);
}
}
2. 分治(Divide and Conquer):
分治是一种递归算法,它将一个问题分解成多个小问题,然后解决这些小问题,最后将结果合并起来得到原问题的解。分治算法通常包含三个步骤:分解(Divide)、解决(Conquer)和合并(Combine)。
在C++中,分治算法的基本结构如下:
resultType divideAndConquer(problemType problem) {
// Base case
if (problem satisfies some condition) {
// Solve the problem directly and return the result
}
// Divide the problem into smaller subproblems
subproblemType subproblem1, subproblem2, ...;
// Conquer the subproblems
resultType result1 = divideAndConquer(subproblem1);
resultType result2 = divideAndConquer(subproblem2);
// Combine the results
resultType finalResult = combine(result1, result2, ...);
// Return the final result
return finalResult;
}
递归和分治是计算机科学中的重要概念,它们在解决许多复杂问题时都非常有用,如排序算法(如快速排序、归并排序)、搜索算法(如二分搜索)、图算法(如深度优先搜索、广度优先搜索)等。
3. 动态规划(Dynamic Programming,简称DP)
是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划的基本思想是:将一个复杂问题分解为若干个简单的子问题,通过解决这些子问题来求解原问题的解。动态规划通常用于优化问题,如求解最短路径、最小生成树、背包问题等。
在C++中,动态规划通常使用数组来存储中间结果,以避免重复计算。以下是一个简单的动态规划问题的例子,我们将使用它来解释动态规划的基本概念。
斐波那契数列的动态规划解法
斐波那契数列是一个经典的动态规划问题,它的定义如下:
F(0) = 0
F(1) = 1
F(n) = F(n - 1) + F(n - 2) for n > 1
我们可以使用动态规划来解决这个问题,避免重复计算。
#include <iostream>
#include <vector>
int fibonacci(int n) {
// 创建一个数组来存储斐波那契数列的值
std::vector<int> fib(n + 1, 0);
// 初始化基本情况
fib[0] = 0;
fib[1] = 1;
// 填充数组
for (int i = 2; i <= n; ++i) {
fib[i] = fib[i - 1] + fib[i - 2];
}
// 返回结果
return fib[n];
}
int main() {
int n = 10;
std::cout << "Fibonacci number at position " << n << " is: " << fibonacci(n) << std::endl;
return 0;
}
在这个例子中,我们首先创建了一个大小为n + 1
的数组fib
来存储斐波那契数列的值。然后,我们初始化了基本情况fib[0]
和fib[1]
。接着,我们使用一个循环从2
到n
填充数组,每个位置的值都是前两个位置的值之和。最后,我们返回fib[n]
作为结果。
动态规划的步骤
动态规划问题的解决通常包括以下步骤:
- 定义子问题:确定问题的子问题,即原问题可以分解为哪些子问题。
- 实现递归解法:编写递归函数来解决子问题。
- 存储中间结果:使用数组或哈希表等数据结构存储子问题的解,避免重复计算。
- 自底向上填表:根据子问题的解,构建原问题的解。
动态规划的适用条件
动态规划适用于满足以下条件的问题:
- 最优子结构:问题的最优解包含了子问题的最优解。
- 重叠子问题:问题的递归算法会多次求解相同的子问题。
动态规划的复杂性
动态规划的复杂性主要取决于子问题的数量和解决每个子问题的复杂性。通常,动态规划的时间复杂性为O(n),空间复杂性为O(n)。
总结
动态规划是一种有效的解决复杂问题的策略,它通过将问题分解为简单的子问题,并存储子问题的解来避免重复计算。动态规划适用于有最优子结构和重叠子问题的场景,并且可以显著提高效率。
4.回溯(Backtracking)
是一种通过深度优先搜索(DFS)策略来解决问题的算法。它在每一步都尝试各种可能的选择,如果在某一步发现当前的选择无法得到一个有效的解,那么它会回退到上一步并尝试其他的选择。这种策略通常用于寻找所有(或某个)解,而不是找到最优解。
回溯算法的基本思想是:从一个初始状态开始,不断地尝试各种可能的选择,如果发现当前的选择无法得到一个有效的解,那么就回退到上一步,并尝试其他的选择。这种策略通常用于解决组合问题,如排列组合、子集和组合总和等。
以下是一个简单的回溯算法的C++实现,用于生成一个给定长度的所有可能的字符串组合。
#include <iostream>
#include <vector>
#include <string>
void backtrack(std::string& current, int length, std::vector<std::string>& result) {
// 如果当前字符串的长度达到目标长度,则将其添加到结果中
if (current.length() == length) {
result.push_back(current);
return;
}
// 尝试在当前位置添加'a'到'd'中的任意字符
for (char c = 'a'; c <= 'd'; ++c) {
// 做选择
current.push_back(c);
// 递归进入下一个位置
backtrack(current, length, result);
// 撤销选择
current.pop_back();
}
}
int main() {
int length = 3; // 目标字符串长度
std::string current; // 当前正在构建的字符串
std::vector<std::string> result; // 存储所有可能的字符串组合
// 开始回溯
backtrack(current, length, result);
// 输出所有可能的字符串组合
for (const auto& str : result) {
std::cout << str << std::endl;
}
return 0;
}
在这个例子中,backtrack
函数是一个回溯函数,它尝试在当前字符串的末尾添加一个字符,然后递归地调用自己来处理下一个位置。如果当前字符串的长度达到目标长度,那么它就被添加到结果中。如果在某一步发现当前的选择无法得到一个有效的解,那么就会撤销上一步的选择并尝试其他的选择。
回溯算法的关键在于正确地定义和管理状态,以及在合适的时候进行回溯。它通常用于解决组合问题,如排列组合、子集和组合总和等。在实际应用中,回溯算法的效率取决于问题的特性和问题的规模。
5.分支限界法(Branch and Bound)
是一种启发式搜索算法,它在解空间树(解空间树是搜索树的一种,用于解决优化问题)上搜索解,通过在搜索过程中使用界限(bound)来排除不可能包含最优解的子树,从而提高搜索效率。分支限界法通常用于解决组合优化问题,如旅行商问题(TSP)、背包问题等。
分支限界法的基本思想是:
- 创建一个优先队列(通常是基于最小堆实现的)来存储活节点。
- 从根节点开始,生成所有可能的子节点。
- 对于每个子节点,计算其目标函数的值(对于旅行商问题,可能是路径长度;对于背包问题,可能是总价值)。
- 如果子节点的目标函数值优于当前最优解,则更新最优解。
- 如果子节点可能包含更好的解,则将其加入到优先队列中。
- 从优先队列中选择目标函数值最优的节点,重复步骤2-5。
- 当优先队列为空时,搜索结束,返回最优解。
以下是一个使用分支限界法解决旅行商问题的C++示例:
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
// 定义旅行商问题的节点结构
struct Node {
std::vector<int> path; // 当前路径
int cost; // 当前路径的成本
int level; // 当前节点在路径中的位置
int bound; // 当前节点的界限
};
// 比较函数,用于优先队列
struct CompareNodes {
bool operator()(const Node& a, const Node& b) {
return a.bound > b.bound;
}
};
// 计算旅行商问题的上界
int calculateBound(const std::vector<std::vector<int>>& graph, const Node& node) {
int bound = 0;
for (int i = node.level; i < graph.size(); ++i) {
int minCost = std::numeric_limits<int>::max();
for (int j = 0; j < graph.size(); ++j) {
if (graph[i][j] < minCost && graph[i][j] != 0) {
minCost = graph[i][j];
}
}
bound += minCost;
}
return bound;
}
// 旅行商问题的分支限界法
int branchAndBoundTSP(const std::vector<std::vector<int>>& graph) {
std::priority_queue<Node, std::vector<Node>, CompareNodes> pq;
Node root;
root.path.push_back(0); // 从节点0开始
root.cost = 0;
root.level = 0;
root.bound = calculateBound(graph, root);
pq.push(root);
int minCost = std::numeric_limits<int>::max();
while (!pq.empty()) {
Node current = pq.top();
pq.pop();
// 如果当前路径的成本已经超过最小成本,则停止搜索
if (current.cost > minCost) {
continue;
}
// 如果已经遍历了所有节点,则更新最小成本
if (current.level == graph.size() - 1) {
minCost = std::min(minCost, current.cost + graph[current.path.back()][0]);
continue;
}
// 生成子节点
for (int i = 0; i < graph.size(); ++i) {
if (graph[current.path.back()][i] != 0) {
Node child;
child.path = current.path;
child.path.push_back(i);
child.cost = current.cost + graph[current.path.back()][i];
child.level = current.level + 1;
child.bound = child.cost + calculateBound(graph, child);
pq.push(child);
}
}
}
return minCost;
}
int main() {
// 定义图的邻接矩阵
std::vector<std::vector<int>> graph = {
{0, 10, 15, 20},
{10, 0, 35, 25},
{15, 35, 0, 30},
{20, 25, 30, 0}
};
int minCost = branchAndBoundTSP(graph);
std::cout << "Minimum cost: " << minCost << std::endl;
return 0;
}
在这个例子中,我们定义了一个Node
结构来表示搜索树中的节点,并使用了一个优先队列来存储活节点。我们还定义了一个calculateBound
函数来计算每个节点的界限,即当前路径的成本加上剩余节点的最小成本。在branchAndBoundTSP
函数中,我们使用分支限界法来搜索最优解。
分支限界法的关键在于有效地计算界限,以便在搜索过程中排除不可能包含最优解的子树。这种方法通常用于解决组合优化问题,如旅行商问题和背包问题,其中问题的解空间非常大,使用回溯法或暴力搜索方法会导致计算量过大。