四大常规算法
第 1 节分治法
分治法——见名思义,即分而治之,从而得到我们想要的最终结果。分治法的思想是将一个规模为 N 的问题分解为 k 个较小的子问题,这些子问题遵循的处理方式就是互相独立且与原问题相同。
两部分组成
-
分(divide):递归解决较小的问题
-
治(conquer):然后从子问题的解构建原问题的解三个步骤
1、分解(Divide):将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
2、解决(Conquer):若子问题规模较小而容易被解决则直接解决,否则递归地解各个子问题;
3、合并(Combine):将各个子问题的解合并为原问题的解。
例程: 二分查找算法实现查找数字在数组中对应的索引
- 二分法一般用于有序的数组,所以下面的数组是有序的
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
/*递归实现二分查找参数:
arr - 有序数组地址arr
minSub - 查找范围的最小下标minSub maxSub - 查找范围的最大下标maxSub
num - 带查找数字
返回:找到则返回所在数组下标,找不到则返回-1
*/
//利用递归实现分治法
int binarySearch(int * arr, int minSub, int maxSub, int findNum)
{
if (minSub > maxSub)return -1;
int mid = (minSub + maxSub) / 2; //求取中间下标
if (findNum < arr[mid])
{
return binarySearch(arr, minSub, mid - 1 , findNum);
}
else if (findNum > arr[mid])
{
return binarySearch(arr, mid + 1, maxSub , findNum);
}
return mid;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
int index = binarySearch(arr, 0, sizeof(arr) / sizeof(arr[0]), 3);
cout << arr[index] << endl;
system("pause");
return 0;
}
第 2 节动态规划算法
- 你在这个项目中要求实现一个统计算法:如果美女机器人一次可以上 1 级台阶,也可以一次上 2 级台阶。求美女机器人走一个 n 级台阶总共有多少种走法。
-
分治法核心思想: 从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
-
比如总共有 5 级台阶,求有多少种走法;由于机器人一次可以走两级台阶,也可以走一级台阶,所以我们可以分成两个情况
-
机器人最后一次走了两级台阶,问题变成了“走上一个 3 级台阶,有多少种走法?”
-
机器人最后一步走了一级台阶,问题变成了“走一个 4 级台阶,有多少种走法?” 我们将求 n 级台阶的共有多少种走法用 f(n) 来表示,则 f(n) = f(n-1) + f(n-2);
-
由上可得 f(5) = f(4) + f(3); f(4) = f(3) + f(2); f(3) = f(2) + f(1);
边界情况分析走一步台阶时,只有一种走法,所以 f(1)=1 。走两步台阶时,有两种走法,直接走 2 个台阶,分两次每次走 1 个台阶,所以 f(2)=2 。走两个台阶以上可以分解成上面的情况这符合我们讲解的分治法的思想: 分而治之代码实现
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//利用递归实现分治法----实现走楼梯
int binarySearch(int n)
{
if (n == 0)return 0;
if (n == 1)return 1;
if (n == 2)return 2;
return (binarySearch(n - 1) + binarySearch(n - 2));
}
int main()
{
cout << binarySearch(20) << endl;
system("pause");
return 0;
}
- 但是,如果细心的同学是否会注意到,上面的代码中存在很多重复的计算?
- 比如: f(5) = f(4) + f(3) 计算分成两个分支:
f(4) = f(3)+f(2) = f(2) + f(1) + f(2);
f(3) = f(2) + f(1)
上面加粗的部分就是重复计算的一部分!
有没有办法避免重复计算的部分?其实我们可以从下向上分析推断问题。
实现代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//利用动态规划----实现走楼梯
int workOut(int n)
{
if (n == 0)return 0;
if (n == 1)return 1;
if (n == 2)return 2;
int * arr = new int[n + 1];
arr[0] = 0;
arr[1] = 1;
arr[2] = 2;
for (int i = 3; i <= n; i++)
{
arr[i] = arr[i - 1] + arr[i - 2];
}
int ret = arr[n];
delete[] arr;
return ret;
}
int main()
{
cout << workOut(3) << endl;
system("pause");
return 0;
}
- 这就是动态规划法 !!!
-
动态规划也是一种分治思想,但与分治算法不同的是,分治算法是把原问题分解为若干子问题,自顶向下,求解各子问题,合并子问题的解从而得到原问题的解。
-
动态规划也是自顶向下把原问题分解为若干子问题,不同的是,然后自底向上,先求解最小的子问题,把结果存储在表格中,在求解大的子问题时,直接从表格中查询小的子问题的解,避免重复计算,从而提高算法效率。
-
什么时候要用动态规划?
如果要求一个问题的最优解(通常是最大值或者最小值),而且该问题能够分解成若干个子问题,并且小问题之间也存在重叠的子问题,则考虑采用动态规划。
怎么使用动态规划?五步曲解决:
-
判断题意是否为找出一个问题的最优解
-
从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
-
从下往上分析问题,找出这些问题之间的关联(状态转移方程)
-
讨论底层的边界问题
-
解决问题(通常使用数组进行迭代求出最优解)
第 3 节 回溯算法
回溯的基本原理
- 在问题的解空间中,按深度优先遍历策略(不撞南墙不会回头),从根节点出发搜索解空间树。算法搜索至解空间的任意一个节点时,先判断该节点是否包含问题的解。如果确定不包含,跳过对以该节点为根的子树的搜索,逐层向其祖先节点回溯,否则进入该子树,继续深度优先搜索。回溯法解问题的所有解时,必须回溯到根节点,且根节点的所有子树都被搜索后才结束。回溯法解问题的一个解时,只要搜索到问题的一个解就可结束。
回溯的基本步骤
-
定义问题的解空间
-
确定易于搜索的解空间结构
-
以深度优先搜索的策略搜索解空间,并在搜索过程中尽可能避免无效搜索
名企面试题:
- 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如在下面的 3×4 的矩阵中包含一条字符串“bfce”的路径(路径中的字母用下划线标出)。但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符 b 占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
解题思路:
首先,在矩阵中任选一个格子作为路径的起点。如果路径上的第 i 个字符不是待搜索的目标字符 ch,那么这个格子不可能处在路径上的第 i 个位置。如果路径上的第 i 个字符正好是 ch,那么往相邻的格子寻找路径上的第 i+1 个字符。除在矩阵边界上的格子之外,其他格子都有 4 个相邻的格子。重复这个过程直到路径上的所有字符都在矩阵中找到相应的位置。
由于路径不能重复进入矩阵的格子,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。 当矩阵中坐标为(row, col)的格子和路径字符串中相应的字符一样时,从 4 个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符, 如果 4 个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。
#include<iostream>
#include<stack>
using namespace std;
bool toFindStr(int rows, int cols, int row, int col, int strIndex, char arr[4][4], bool ** brr, const char * str);
//回溯法查找数组中有用户所提供的字符串吗?
bool findStr(int rows, int cols, char arr[4][4], const char * str)
{
if (rows <= 0 || cols <= 0 || arr == NULL || str == NULL)return false;
//判定数组是否被访问
bool** brr = new bool*[rows];
for (int i = 0; i < rows; i++)
{
brr[i] = new bool[cols];
}
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
brr[i][j] = false;
}
}
int strIndex = 0; //每一次都从起始点进行搜索
//以数组中的每一个字符为起点,进行搜索
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (toFindStr(rows, cols, i, j, strIndex, arr, brr, str))
{
return true;
}
}
}
return false;
}
bool toFindStr(int rows, int cols, int row, int col, int strIndex, char arr[4][4], bool ** brr, const char * str)
{
if (str[strIndex] == 0)return true;
//参数合法性
if (rows <= 0 || cols <= 0 || arr == NULL || str == NULL
|| row > rows || row < 0 || col > cols || col < 0 || strIndex < 0)
return false;
bool ret = false;
//元素相同且没有被访问
if (arr[row][col] == str[strIndex] && brr[row][col] != true)
{
brr[row][col] = true;
strIndex++;
/*关键:
下方代码很好的利用了||的特性,下方会先判断第一个递归,直到第一个表达式返回(true/false)
若返回的是true,后续的递归就不会被调用,若为false还会调用后续递归
*/
//最后一层返回true,则之前的每一层都返回true
ret = toFindStr(rows, cols, row - 1, col, strIndex, arr, brr, str)//向上
|| toFindStr(rows, cols, row + 1, col, strIndex, arr, brr, str)//向下
|| toFindStr(rows, cols, row, col - 1, strIndex, arr, brr, str)//向左
|| toFindStr(rows, cols, row, col + 1, strIndex, arr, brr, str);//向右
/*
若这点的上下左右都不符合的话,那么以这个点为起点就行不通则回溯和设为false
这样就不影响下次使用,
*/
if (ret == false)
{
brr[row][col] = false;
strIndex--;//回溯
}
}
return ret;
}
int main()
{
char arr[4][4] =
{
'A','H','D','I',
'W','D','Y','O',
'Q','Y','D','W',
'H','S','F','L'
};
if (findStr(4, 4, arr, "HDYO"))
{
cout << "找到了" <<endl;
}
else
{
cout << "没找到" << endl;
}
system("pause");
}
第 4 节贪心算法
- 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
基本思路
其基本的解题思路为:
-
建立数学模型来描述问题
-
把求解的问题分成若干个子问题
-
对每一子问题求解,得到子问题的局部最优解
-
把子问题对应的局部最优解合成原来整个问题的一个近似最优解
- 这里钱的数组是按照金额从大到小排序的
#include<iostream>
using namespace std;
#define N 7
//钱的金额以及个数
int value[N] = { 100 , 50 , 20 , 10 , 5 , 2 , 1 };
int mcount[N] = { 10, 2, 3, 1 ,2 ,3 , 5 };
int getMomey(int momey)
{
if (momey <= 0)return -1;
int i = 0;
int sheetNum = 0;
while (1)
{
int m = momey / value[i];
int num = mcount[i] < m ? mcount[i] : m;
if (num == 0)
{
i++;
continue;
}
sheetNum += num;
momey -= value[i];
cout << value[i] << " " << num << endl;
i++;
if (momey == 0 )break;
if (i == N - 1)return -1;
}
return sheetNum;
}
int main()
{
int momey;
cout << "请输入钱数 :";
cin >> momey;
int sheetNum = getMomey(momey);
if (sheetNum == -1)
{
cout << "没有这么多钱" << endl;
}
else
{
cout << "一共用了" << sheetNum << "张钱" << endl;
}
system("pause");
}