1、动态规划
动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤:
- 第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
- 第二步骤:状态转移方程,找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]……dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步。
- 第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。
有了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。
- 案例一
class Solution {
//解题思路:动态规划
//状态转移方程:比如,如果想指导amout为11时的最少硬币数,如果你凑出amout=10的最少硬币数,你只需要加一就可以得到amount=11的最少硬币数。
public int coinChange(int[] coins, int amount) {
//自底而上求
int max=amount+1;
//dp[n]:当amout为n时,凑成的硬币最少个数
int[] dp=new int[amount+1];
//把数组中的元素都变为max
Arrays.fill(dp,max);
dp[0]=0;
for(int i=1;i<=amount;i++){
for(int j=0;j<coins.length;j++){
//判断条件
if(coins[j]<=i){
dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
}
}
}
return dp[amount]==amount+1?-1:dp[amount];
}
}
2、回溯法
解决⼀个回溯问题,实际上就是⼀个决策树的遍历过程。你只需要思考 3 个问题:
- 路径:也就是已经做出的选择。
- 选择列表:也就是你当前可以做的选择。
- 结束条件:也就是到达决策树底层,无法再做选择的条件。
回溯算法的框架:
- 案例一
class Solution {
private List<List<Integer>> list1=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates.length==0){
return list1;
}
int sum=0;
int index=0;
Arrays.sort(candidates);
List<Integer> list=new LinkedList<>();
backtrace(candidates,target,list,sum,index);
return list1;
}
public void backtrace(int[] arr,int target,List<Integer> list,int sum,int index){
//结束条件
if(sum==target){
list1.add(new LinkedList(list));
return;
}
for(int i=index;i<arr.length;i++){
if(target-sum<arr[i]){
break;
}
list.add(arr[i]);
sum=sum+arr[i];
backtrace(arr,target,list,sum,i);
list.remove(list.size()-1);
sum=sum-arr[i];
}
}
}
- 案例二
class Solution {
private List<List<Integer>> list=new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
if(nums.length<1){
return list;
}
int[] flag=new int[nums.length];
backtrace(nums,flag,nums.length,new LinkedList<>());
return list;
}
private void backtrace(int[] num,int[] flag,int len,List<Integer> list2){
if(list2.size()==len){
list.add(new ArrayList<>(list2));
return;
}
for(int i=0;i<num.length;i++){
if(flag[i]==1){
continue;
}
flag[i]=1;
list2.add(num[i]);
backtrace(num,flag,len,list2);
flag[i]=0;
list2.remove(list2.size()-1);
}
}
}
- 案例三
class Solution {
// 每步要么增加一个左括号,要么增加一个右括号,是一个二叉的选择,所以暴搜很容易写出来,就是dfs(left - 1, right, curStr + "("); dfs(left, right - 1, curStr + ")");
//但是并不是每个分支都是符合要求的(正确的括号匹配),比如如果right使用的比left多的话就已经不是正确括号了,没必须继续dfs这个分支了,所以加上if来剪枝哈~
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
dfs(n, n, "");
return res;
}
private void dfs(int left, int right, String curStr) {
if (left == 0 && right == 0) {
// 左右括号都不剩余了,递归终止
res.add(curStr);
return;
}
if (left > 0) {
// 如果左括号还剩余的话,可以拼接左括号
dfs(left - 1, right, curStr + "(");
}
if (right > left) {
// 如果右括号剩余多于左括号剩余的话,可以拼接右括号
dfs(left, right - 1, curStr + ")");
}
}
}
3、深度优先遍历算法
class Solution {
//深度优先遍历
private int[][] nums=new int[][]{
{
1,0},{
0,1},{
-1,0},{
0,-1}};
public int numIslands(char[][] grid) {
if(grid.length==0){
return 0;
}
int row=grid.length;
int col=grid[0].length;
//岛屿的个数
int count=0;
//记录被遍历过的位置
boolean[][] arr=new boolean[row][col];
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]=='1'&&arr[i][j]==false){
count++;
dfs(arr,grid,i,j,row,col);
}
}
}
return count;
}
public void dfs(boolean[][] arr,char[][] grid,int x,int y,int row,int col){
arr[x][y]=true;
for(int[] num:nums){
int curRow=num[0]+x;
int curCol=num[1]+y;
if(judgeRange(curRow,curCol,row,col)&&arr[curRow][curCol]==false&&grid[curRow][curCol]=='1'){
dfs(arr,grid,curRow,curCol,row,col);
}
}
}
public boolean judgeRange(int x,int y,int row,int col){
if(x>=0&&x<row&&y>=0&&y<col){
return true;
}
return false;
}
}
4、广度优先遍历算法
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
//解题思路:广度优先遍历
//广度优先遍历是按层层推进的方式,遍历每一层的节点;题目要求的是返回每一层的节点值,所以这题用广度优先来做非常合适
//广度优先需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> list=new LinkedList<>();
if(root==null){
return list;
}
LinkedList<TreeNode> deque=new LinkedList<>();
//将根节点放入队列中,然后不断遍历队列
deque.add(root);
while(deque.size()>0){
//获取当前队列的长度,这个长度相当于 当前这一层的节点个数
int len=deque.size();
List<Integer> list1=new LinkedList<>();
//将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
//如果节点的左/右子树不为空,也放入队列中
for(int i=0;i<len;i++){
TreeNode node=deque.poll();
list1.add(node.val);
if(node.left!=null){
deque.add(node.left);
}