八大算法基础思想
八大算法基础思想
前言
算法:对特定问题求解步骤的一种描述,他是指令的有限序列,其中每一条指令表示一个或多个操作
一个算法具有五个重要特性:
- 有穷性: 一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成
- 确定性:算法的每一个步骤都具有确定的含义,不会出现二义性
- 可行性:算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的
- 输入:一个算法有零个或多个输入,这些输入取自某个特定的对象集合
- 输出:一个算法有一个或多个输出,这些输出是与输入有着某些特定关系的量
算法设计的四个要求:
-
正确性:算法应当满足具体问题的需求
-
可读性:算法主要为了人的阅读与交流,其次才是机器执行
-
健壮性:当输入数据非法时,算法也能适当的作出反应或进行处理,而不会产生莫名其妙的输出结果
-
高效率与低存储量:时间效率指的是算法的执行时间,对于同一个问题,如果有多个算法能够解决,执行时间短的算法效率高,执行时间长的效率低。存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间
算法是一种思维模式,常见算法种类很多,就基础算法思想而言,我认为有八种,他们分别是:枚举、递推、递归、分治、动态规划、贪心、回溯、模拟
1.枚举
枚举也称穷举,是将问题的可能解依次全部列举,然后一一带入问题内检验,从而找到正确解,它是最常见的算法,比如:1~100中有多少个数是3的倍数,这个问题就可以通过枚举算法来解决
int n=0;
for(int i=1;i<=100;++i)
if(i%3==0) ++n;
2.递推
递推,递次推导,核心就是从已知的条件出发,逐步推算出问题的解,比如今天是2号,问后天是几号,可以通过明天是3号,后天就是4号来解决,更经典的例子就是斐波那契数列,斐波那契的递推公式就是f(n)=f(n-1)+f(n-2)其中,f(1)=1,f(2)=1
int f[20]={0,1};
for(int i=2;i<20;++i)
f[i]=f[i-1]+f[i-2];
3.递归
递归:重复调用函数自身实现循环称为递归;
递归实际上不断地深层调用函数,直到函数有返回才会逐层的返回,递归是用栈机制实现的,每深入一层,都要占去一块栈数据区域,因此,递归涉及到运行时的堆栈开销(参数必须压入堆栈保存,直到该层函数调用返回为止),所以有可能导致堆栈溢出的错误;但是递归编程所体现的思想正是人们追求简洁、将问题交给计算机,以及将大问题分解为相同小问题从而解决大问题的动机。
如求斐波那契数列的第n项:
#include <iostream>
int fib(int n) {
if (n < 0) exit(-1);
if (n == 1 || n == 0) return 1;
return fib(n - 1) + fib(n - 2);
}
int main() {
int n;
std::cin >> n;
std::cout << fib(n);
}
递归与迭代思想十分相似,只是进行循环的方式不同而已
迭代:利用变量的原值推出新值称为迭代,或着说迭代是函数内某段代码实现循环;
两者关系:所有的迭代可以转换为递归,但递归不一定可以转换成迭代。
总结如下:
定义 | 优点 | 缺点 | |
---|---|---|---|
递归 | 重复调用函数自身实现循环 | a.用有限的循环语句实现无限集合;b.代码易读;c.大问题转化成小问题,减少了代码量。 | a.递归不断调用函数,浪费空间b.容易造成堆栈溢出 |
迭代 | 利用变量的原值推出新值;函数内某段代码实现循环。 | a.效率高,运行时间只随循环的增加而增加;b.无额外开销。 | a.代码难理解;b.代码不如递归代码简洁;c.编写复杂问题时,代码逻辑不易想出 |
相对来说,能用迭代不用递归(因为递归不断调用函数,浪费空间,容易造成堆栈溢出
4.分治
分治:分而治之;
核心思想:先分再治,从上而下分,从下而上治,将一个问题分解成许多个子问题,通过解决子问题逐步解决总问题;
最经典的例子归并排序:
#include <iostream>
constexpr int N = 2e5 + 5;
int a[N], t[N], n;
void merge_sort(int l, int r) {
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(l, mid), merge_sort(mid + 1, r);
int k = 0, left = l, right = mid + 1;
while (left <= mid && right <= r)
t[k++] = a[left] <= a[right] ? a[left++] : a[right++];
while (left <= mid) t[k++] = a[left++];
while (right <= r) t[k++] = a[right++];
for (int i = l, j = 0; i <= r; i++, j++) a[i] = t[j];
}
int main() {
std::cin >> n;
for (int i = 0; i < n; ++i) std::cin >> a[i];
merge_sort(0, n - 1);
for (int i = 0; i < n; ++i) std::cout << a[i] << " ";
}
5.动态规划
动态规划又与分治很像,都是划分成多个子问题来解决问题,但是动态规划的子问题之间不是相互独立的
,当前子问题的解课看作是前多个阶段问题的完整总结。
动态规划是在目前看来非常不接近人类思维方式一种算法,主要原因是在于人脑在演算的过程中很难对每一次决策的结果进行记忆。动态规划在实际的操作中,往往需要额外的空间对每个阶段的状态数据进行保存,以便下次决策的使用。
经典的背包问题
#include <bits/stdc++.h>
using namespace std;
int t, n, v[105], dp[1005], w[105];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
cin >> t >> n;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
for (int i = 1; i <= n; i++) {
for (int j = t; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
cout << dp[t] << endl;
return 0;
}
6.贪心
从贪心二字就可得知,这个算法的目的就是为了「贪图更多」。但是这种贪心是「目光短浅」的,这就导致贪心算法无法从长远出发,只看重眼前的利益。
具体点说,贪心算法在执行的过程中,每一次都会选择最大的收益,但是总收益却不一定最大。
贪心算法的实现过程就是从问题的一个初始解出发,每一次都作出「当前最优」的选择,直至遇到局部极值点。贪心所带来的局限性很明显,就是无法保证最后的解是最优的,很容易陷入局部最优的情况。
如找零钱问题
假设你开了间小店,不能电子支付,钱柜里的货币只有 25 分、10 分、5 分和 1 分四种硬币,如果你是售货员且要找给客户 41 分钱的硬币,如何安排才能找给客人的钱既正确且硬币的个数又最少?
这里需要明确的几个点:
1.货币只有 25 分、10 分、5 分和 1 分四种硬币;
2.找给客户 41 分钱的硬币;
3.硬币最少化
要求硬币最少肯定就需要选择面值能大就大的硬币
#include <iostream>
int main() {
int n;
std::cin >> n;
int n25 = n / 25;
n -= 25 * n25;
int n10 = n / 10;
n -= 10 * n10;
int n5 = n / 5;
n -= 5 * n5;
std::cout << "\n1分硬币个数:\t" << n
<< "\n5分硬币个数:\t" << n5
<< "\n10分硬币个数:\t" << n10
<< "\n25分硬币个数:\t" << n25 << std::endl;
return 0;
}
7.回溯
回溯算法也可称作试探算法,简单来说,回溯的过程就是在做出下一步选择之前,先对每一种可能进行试探;只有当可能性存在时才会向前迈进,倘若所有选择都不可能,那么则向后退回原来的位置,重新选择。dfs(深度优先搜索)就是应用回溯算法
经典问题
P1219 USACO1.5八皇后 Checker Challenge - 洛谷
#include <iostream>
using namespace std;
int n, num;
int map[15][15];
int ans[15];
void dfs(int);
int check(int, int);
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
map[i][j] = 1; // 0为限制 1为可行区域
}
}
dfs(1);
cout << num;
return 0;
}
int check(int x, int y) {
for (int i = 1; i < x; i++) // 列
if (map[i][y] == 0) // 已有皇后
return 0;
for (int i = x - 1, j = y + 1; i > 0 && j <= n; i--, j++) // ↗对角线
if (map[i][j] == 0)
return 0;
for (int i = x - 1, j = y - 1; i > 0 && j > 0; i--, j--) // ↖对角线
if (map[i][j] == 0)
return 0;
return 1;
}
void dfs(int sum) // sum为皇后个数也代表行
{
if (sum > n) { // 皇后个数足够
num++; // 解的个数
if (num <= 3) { // 输出前三个解
for (int i = 1; i <= n; i++) {
cout << ans[i] << " ";
}
cout << endl;
}
} else {
for (int j = 1; j <= n; j++) { // 列
if (check(sum, j)) {
map[sum][j] = 0; // 标记
ans[sum] = j;
dfs(sum + 1); // 寻找下一个皇后
map[sum][j] = 1; // 还原
}
}
}
}
8.模拟
许多真实场景下,由于问题规模过大、变量过多等因素,很难将具体的问题抽象出来,也就无法针对抽象问题的特征来进行算法的设计。这个时候,模拟思想或许是最佳的解题策略。
模拟的过程就是对真实场景尽可能的模拟,然后通过计算机强大的计算能力对结果进行预测。这相较于上述的算法是一种更为宏大的思想。在进行现实场景的模拟中,可能系统部件的实现都需要上述几个算法思想的参与。
模拟说起来是一种很玄幻的思想,没有具体的实现思路,也没有具体的优化策略。只能说,具体问题具体分析。