五大经典算法是指:分治法、动态规划算法、回溯算法、贪心算法、分支限界法。下面依次讲解各大算法并以leetcode对应题目进行说明。
一、分治法
1.1 基本概念
分治法是指:把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
1.2 适用情况
-
该问题的规模缩小到一定的程度就可以容易地解决;
-
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
-
利用该问题分解出的子问题的解可以合并为该问题的解;
-
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
**第二条特征是应用分治法的前提。**它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
1.3 基本步骤
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
step3 合并:将各个子问题的解合并为原问题的解。
1.4 经典问题
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔
1.5 经典题目
leetcode 21.合并两个有序链表
// TODO
二、动态规划算法
2.1 基本概念
动态规划是指:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
2.2 适用情况
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3) 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
2.3 基本步骤
(1) 确定dp[i]的含义
(2) 写出状态转移方程
2.4 经典题目
leetcode 70.爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
此题有多种解法,动态规划只是其中的一种方法,因此并不必拘泥于必须要用某种解法去解决,只要能够得到正确答案即是正确解法。
1、递归方法
使用递归方法比较简单,F(n) = F(n-1) + F(n-2),但该方法由于多次重复计算,时间复杂度会比较高。
2、递归方法的变形
public int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
int a = 1, b = 2, temp;
for (int i = 3; i <= n; i++) {
int temp = a;
a = b;
b = temp + b;
}
return b;}
3、动态规划
首先确定dp[]的含义,然后为其赋值,之后确定状态转移方程
public int climbStairs(int n) {
int[] nums = new int[45]; //45为题目中给定的n的最大值
nums[0] = 1;
nums[1] = 2;
for (int i = 2; i < n; i++) {
nums[i] = nums[i - 1] + nums[i - 2];
}
return nums[n - 1];
}
三、回溯算法
3.1 基本概念
回溯算法实际上是一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回(也就是递归返回),尝试别的路径。
3.2 算法思想
(1) 选择。对于每个特定的解,肯定是由一步步构建而来的,而每一步怎么构建,肯定都是有限个选择,要怎么选择,这个要知道;同时,在编程时候要定下,优先或合法的每一步选择的顺序,一般是通过多个if或者for循环来排列。
(2) 条件。对于每个特定的解的某一步,他必然要符合某个解要求符合的条件,如果不符合条件,就要回溯,其实回溯也就是递归调用的返回。
(3) 结束。当到达一个特定结束条件时候,就认为这个一步步构建的解是符合要求的解了。把解存下来或者打印出来。对于这一步来说,有时候也可以另外写一个issolution函数来进行判断。注意,当到达第三步后,有时候还需要构建一个数据结构,把符合要求的解存起来,便于当得到所有解后,把解空间输出来。这个数据结构必须是全局的,作为参数之一传递给递归函数。
3.3 算法原则
(1) 必须要有一个临时变量(可以就直接传递一个字面量或者常量进去)传递不完整的解,因为每一步选择后,暂时还没构成完整的解,这个时候这个选择的不完整解,也要想办法传递给递归函数。也就是,把每次递归的不同情况传递给递归调用的函数。
(2) 可以有一个全局变量,用来存储完整的每个解,一般是个集合容器(也不一定要有这样一个变量,因为每次符合结束条件,不完整解就是完整解了,直接打印即可)。
(3) 最重要的一点,一定要在参数设计中,可以得到结束条件。一个选择是可以传递一个量n,也许是数组的长度,也许是数量,等等。
(4) 要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。
3.4 经典题目
leetcode 39.组合总数
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
递归树如下:
但是由于题目要求返回和为target的不同组合,因此[2,2,3]、[2,3,2]、[3,2,2]为重复方案。为了避免搜索过程中的重复方案,我们要去定义一个搜索起点,已经考虑过的数,以后的搜索中就不能出现,让我们的每次搜索都从当前起点往后搜索(包含当前起点),直到搜索到数组末尾。这样我们人为规定了一个搜索顺序,就可以避免重复方案。 如下图所示,处于黄色虚线矩形内的分支都不再去搜索了,这样我们就完成了去重操作。
class Solution {
//回溯算法
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>(); //记录答案
if (candidates.length == 0) {
return ans;
}
Arrays.sort(candidates); //排序操作实现了结果的去重
backTrack(candidates, target, ans, 0, new ArrayList<Integer>()); //0值代表第一次从下标0开始遍历
return ans;
}
private void backTrack(int[] candidates, int target, List<List<Integer>> ans, int i, ArrayList<Integer> integers) {
if (target < 0) //退出条件
return;
if (target == 0) { //target==0时表示该路径符合要求,将路径下的值添加到ans中
ans.add(new ArrayList<>(integers));
return;
}
for (int start = i; start < candidates.length; start++) {
integers.add(candidates[start]);
// 因为同一个数字可以重复使用,所以第四个参数仍为start
backTrack(candidates, target - candidates[start], ans, start, integers);
integers.remove(integers.size() - 1); //回溯,恢复现场
}
}
}
leetcode 40.组合总数II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用 一次 。
**注意:**解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
**分析:**这道题目和 39.组合总和 的区别:
1、本题 candidates 中的每个数字在每个组合中只能使用一次。
2、本题数组candidates的元素是有重复的,而39.组合总和是无重复元素的数组candidates
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(candidates);
backTrace(candidates, target, 0, ans, new ArrayList<Integer>());
return ans;
}
private void backTrace(int[] candidates, int target, int i, List<List<Integer>> ans, ArrayList<Integer> integers) {
if (target < 0) {
return;
}
if (target == 0) {
ans.add(new ArrayList<>(integers));
return;
}
for (int start = i; start < candidates.length; start++) {
if (start > i && candidates[start] == candidates[start - 1]) {
continue; //出现重复节点,由于已经排序,同层的第一个节点已经被访问过,所以直接跳过
}
integers.add(candidates[start]);
// 因为同一个数字只能使用一次,所以第三个参数需要做加1操作
backTrace(candidates, target - candidates[start], start + 1, ans, integers);
integers.remove(integers.size() - 1);
}
}
}
leetcode 78.子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
**输入:**nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) {
result.add(new ArrayList<>());
return result;
}
backTrace(nums, 0, result, new ArrayList<>());
return result;
}
public void backTrace(int[] nums, int startIndex, List<List<Integer>> result, List<Integer> temp) {
result.add(new ArrayList<>(temp)); //第一次添加空串
for (int i = startIndex; i < nums.length; i++) {
temp.add(nums[i]);
backTrace(nums, i + 1, result, temp);
temp.remove(temp.size() - 1);
}
}
}