剑指offer刷题宝典--第3节

五、动态规划

三种时间复杂度算法求解斐波那契数列

在这里插入图片描述

JZ85 连续子数组的最大和(二) 【细节 ??】

要求: 时间复杂度O(n),空间复杂度O(n)

进阶: 时间复杂度O(n),空间复杂度O(1)

​ 但是题目要求需要返回长度最长的一个,我们则每次用left、right记录该子数组的起始,需要更新最大值的时候(要么子数组和更大,要么子数组和相等的情况下区间要更长)顺便更新最终的区间首尾,最后根据区间首尾获取子数组。

在这里插入图片描述

import java.util.*;
public class Solution {
    public int[] FindGreatestSumOfSubArray (int[] array) {
        int n = array.length;
        int[] dp = new int[n];
        dp[0] = array[0];
        int left = 0;
        int start = 0, end = 1;
        int max = -102;
        for (int i = 1; i < n; i++) {
            if (dp[i - 1] >= 0) {
                dp[i] = dp[i - 1] + array[i];
            } else {
                dp[i] = array[i];
                left = i;
            }
            if (dp[i] >= max) {
                max = dp[i];
                start = left;
                end = i + 1;
            }
        }
        return Arrays.copyOfRange(array, start, end );
    }
}


import java.util.*;
public class Solution {
    public int[] FindGreatestSumOfSubArray (int[] array) {
        int n = array.length;
        int cur = array[0];
        int left = 0;
        int start = 0, end = 1;
        int max = -102;
        for (int i = 1; i < n; i++) {
            if (cur >= 0) {
                cur += array[i];
            } else {
                cur = array[i];
                left = i;
            }
            if (cur >= max) {
                max = cur;
                start = left;
                end = i + 1;
            }
        }
        return Arrays.copyOfRange(array, start, end );
    }
}

JZ19 正则表达式匹配 【× 难!!!】

f[i] [j] 代表 str 的前 i 个和 pattern 的前 j 个能否匹配

转移方程

对于前面两个情况,可以合并成一种情况 f[i] [j]=f[i−1] [j−1]
对于第三种情况,对于 c* 分为看和不看两种情况
不看:直接砍掉正则串pattern 的后面两个, f[i] [j]=f[i] [j−2]
看:正则串pattern 不动,主串str前移一个,f[i] [j]=f[i−1] [j]

时间复杂度:O(mn),其中 m 和 n分别是字符串 s和 p的长度。我们需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1)

空间复杂度:O(mn),即为存储所有状态使用的空间。

在这里插入图片描述
在这里插入图片描述

| 是字符*要么出现0次,要么出现多次,所以结果带或!!

//注意此处dp和s[]和p[]的下标!!

import java.util.*;
public class Solution {
    public boolean match (String str, String pattern) {
        int m = str.length();
        int n = pattern.length();
        char[] s = str.toCharArray();
        char[] p = pattern.toCharArray();
        boolean[][] dp = new boolean[m + 1][n + 1];

        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                //p是空串
                if (j == 0) {
                    if (i == 0)
                        dp[i][j] = true;
                }
                //p是非空串,需要讨论分析
                else {
                    //p是字母或者.
                    if (p[j - 1] != '*') {
                        //注意要加括号!!
                        if (i>=1 && (s[i - 1] == p[j - 1] || p[j - 1] == '.'))
                            dp[i][j] = dp[i - 1][j - 1];
                    }
                    //p是*
                    else {
                        //*前字符重复0次,不出现
                        if (j >= 2)
                            dp[i][j] |= dp[i][j - 2];
                        //*前字符重复多次,出现
                        if (i>=1 && j >= 2 && (s[i-1] == p[j - 2] || p[j - 2] == '.'))
                            dp[i][j] |= dp[i - 1][j];
                    }
                }
            }
        }
        return dp[m][n];
    }
}

JZ71 跳台阶扩展问题

题解一:递归
题解思路:考虑最后一步是跳几阶到达目标位置的。
主要分析:
1.令f(n)表示n阶台阶总的跳法
2.假设最后只跳一步,那么f(n) = f(n-1); 最后跳两步,那么f(n) = f(n-2);以此类推,可知总的跳法为f(n) = f(n-1) + f(n-2) +…+f(0)

f(n)=f(n-1)+f(n-2)+f(n-3)+…+f(n-n)

复杂度分析:
时间复杂度:O(N^N)
空间复杂度:O(N) : 递归栈深度

题解三: 动态规划+
题解思路:延续题解一中的公式f(n) = f(n-1) + f(n-2) +…+f(0)
分析:
1.知道f(n) = f(n-1) + f(n-2) +…+f(0),那么f(n-1) = f(n-2) + f(n-3) +…+f(0);
2.可知 f(n) = 2 * f(n-1);
3.初始ans = 1;
复杂度分析:
时间复杂度:O(N),从1~n依次遍历了台阶数
空间复杂度:O(1),没有申请其他空间存放数据

public class Solution {
    public int jumpFloorII(int target) {
        //为啥为1?
        //f(2)=f(1)+f(0)=2
//         if(target==0) return 1;
//         int res=0;
//         for (int i = 0; i < target; i++) {
//             res+=jumpFloorII(i);
//         }
//         return res;

        if (target == 0) return 1;
        if (target == 1) return 1;
        int res = 1;
        for (int i = 2; i <= target; i++)
            res = 2 * res;
        return res;
    }
}

JZ70 矩形覆盖

进阶:空间复杂度 O(1) ,时间复杂度 O(n)

时间复杂度:O(n)

空间复杂度:O(1)

f[n] = f[n-1] + f[n-2]

public class Solution {
    public int rectCover(int target) {
        //递归方式
//         if(target==0) return 0;
//         if(target==1) return 1;
//         if(target==2) return 2;

//         return rectCover(target-1)+rectCover(target-2);

        //迭代方式
        if (target <= 2) return target;
        int m = 1, n = 2, res = 0;
        for (int i = 3; i <= target; i++) {
            res = m + n;
            m = n;
            n = res;
        }
        return res;
    }
}

JZ63 买卖股票的最好时机(一)

dp[i] [0]表示卖出股票,不持有股票时的最大收益

dp[i] [1]表示买入股票,持有股票时的最大收益

推导状态转移方程:
dp[i] [0]:规定了今天不持股,有以下两种情况:
昨天不持股,今天什么都不做;
昨天持股,今天卖出股票(现金数增加),

状态转移方程:dp[i] [0] = Math.max(dp[i - 1] [0], dp[i - 1] [1] + prices[i]);
dp[i][1]:规定了今天持股,有以下两种情况:
昨天持股,今天什么都不做(现金数与昨天一样);
昨天不持股,今天买入股票(注意:只允许交易一次,因此手上的现金数就是当天的股价的相反数)

状态转移方程:dp[i] [1] = Math.max(dp[i - 1] [1], -prices[i]);

import java.util.*;
public class Solution {

    public int maxProfit (int[] prices) {
        int n = prices.length, minVal = prices[0];
        int[][] dp=new int[n][2];
        dp[0][0]=0;
        dp[0][1]=-prices[0];
        for(int i=1;i<n;i++){
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            //买入股票之前不能卖出股票
            dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
        }
        return dp[n-1][0];
    }
}

JZ47 礼物的最大价值

import java.util.*;
public class Solution {
    public int maxValue (int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0]=grid[0][0];
        for (int i = 1; i < m; i++) {
            dp[i][0]=dp[i-1][0]+grid[i][0];
        }
        for (int j = 1; j < n; j++) {
            dp[0][j]=dp[0][j-1]+grid[0][j];
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }
}

JZ48 最长不含重复字符的子字符串 【难!!!】

方法一:动态规划

  1. dp[i]表示i位置的不含重复字符的最长字符串长度

  2. 公式 【不懂??】

    总共两种情形:

    1. 字符没有出现过,长度加一;

    2. 字符出现过:此时又有两种情况:

      • 这个字符上一次出现的位置没有算在在当前最长不重复子串上,例如:dfafbsdas,遍历到第二个a时,第一个a不在最长不重复子串上,所以长度也是直接加一; f(n) = f(n - 1) + 1

      • 这个字符上一次出现的位置算在在当前最长不重复子串上,例如:fabsdas,遍历到第二个a时,当前位置的长度重新计算为两个重复字符之间的距离,就是bsda; f(n) = index - lastIndex

  3. 初始化

    dp[i]=1

    方法三:双指针 + 哈希表
    在这里插入图片描述
    在这里插入图片描述

import java.util.*;
public class Solution {
    HashMap<Character, Integer> map = new HashMap<>();
    public int lengthOfLongestSubstring (String s) {
        int n = s.length();
        int res = 0, left = -1;
        for (int right = 0; right < n; right++) {
            if (map.containsKey(s.charAt(right))) {
                left = Math.max(left, map.get(s.charAt(right)));
            }
            map.put(s.charAt(right), right);
            res = Math.max(res, right - left);
        }
        return res;
    }
}

import java.util.*;
public class Solution {
    public int lengthOfLongestSubstring (String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int n = s.length();
        int[] dp = new int[n];
        dp[0] = 1;
        int res = 1;
        map.put(s.charAt(0),0);
        for (int i = 1; i < n; i++) {
            char ch = s.charAt(i);
            if (map.containsKey(ch)) {
                dp[i] = Math.min(dp[i - 1] + 1, i - map.get(ch));
            } else {
                dp[i] = dp[i - 1] + 1;
            }
            map.put(ch, i);
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

JZ46 把数字翻译成字符串

1、dp[i]:表示长度为i的字符串有多少种不同的翻译方法

2、dp公式:

两位子串>11 && 两位子串>25:

dp[i] = dp[i - 1] + dp[i - 2];

否则:

dp[i] = dp[i - 1];

class Solution {
    public int translateNum(int num) {
        String strs = String.valueOf(num);
        int n = strs.length();
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i < n + 1; i++) {
            String number = strs.substring(i - 2, i);
            if (number.compareTo("10") >= 0 && number.compareTo("25") <= 0) {
                dp[i] = dp[i - 1] + dp[i - 2];
            } else
                dp[i] = dp[i - 1];
        }
        return dp[n];
    }
}

剑指 Offer 49. 丑数!!!

剑指 Offer 49. 丑数

求按从小到大的顺序的第 n 个丑数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
习惯上我们把1当做是第一个丑数。

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if (index == 0) return 0;
        int[] dp = new int[index + 1];
        dp[1] = 1;
        int p2 = 1, p3 = 1, p5 = 1;
        for (int i = 2; i <= index; i++) {
            int num2 = dp[p2] * 2;
            int num3 = dp[p3] * 3;
            int num5 = dp[p5] * 5;
            dp[i] = Math.min(Math.min(num2, num3), num5);
            if (dp[i] == num2) p2++;
            if (dp[i] == num3) p3++;
            if (dp[i] == num5) p5++;
        }
        return dp[index];
    }
}

剑指 Offer 60. n个骰子的点数

剑指 Offer 60. n个骰子的点数

dp五部曲:假设投掷n个骰子总的投的次数为6^n

1.状态定义:dp[i][j]为投掷i个骰子点数和为j的次数

2.状态转移:dp[i][j]dp[i-1][j-1],dp[i-1][j-2],...,dp[i-1][j-6]相加得到

因此dp[i][j]=∑dp[i-1][j-k],其中1<=k<=6 && j-k>=i-1 (j-k<i-1时dp[i][j]=0)

举例:dp[2][4]=dp[1][3]+dp[1][2]+dp[1][1]dp[1][0]=0,投1次点数和不可能为0

最本质的就是每当n+1,游戏规模会扩大6倍,也就是在6^n-1次的基础上重复玩了6个6^n-1

因此dp[i][j]的次数可以为dp[i-1][j-k]次数的和,而第n次玩的点数k为1-6概率相等

因此对应dp[i-1][j-k]中重复的6次6^ n-1每次选出dp[i-1][j-1],dp[i-1][j-2],进行相加即就是转移过来的玩6^n次的dp[i][j]的次数

3.初始化:只需要初始化dp[1][i]=1即可(1<=i<=6)

4.遍历顺序:显然dp[i]是需要dp[i-1]推导的,而dp[i][j]是需要dp[i-1][j-k]推导,因此j的遍历顺序没关系

5.返回形式:将这5*n+1种出现的次数/6^n就是答案

class Solution {
    public double[] dicesProbability(int n) {
        // i的范围是[1,n],j的范围是[n,6n]
        int[][] dp = new int[n + 1][6 * n + 1];

        // 初始化dp[i][1]=1,因为投1次出现的次数都为1
        for(int i = 1; i <= 6; i++) {
            dp[1][i] = 1;
        }

        // 遍历每个状态,i还需遍历[2,n]
        for(int i = 2; i <= n; i++) {
            // j的范围是[i,6i]
            for(int j = i; j <= 6 * i; j++) {
                // k的范围为1 <= k <= 6 && j - k >= i - 1
                for(int k = 1; k <= 6; k++) {
                    if(j - k < i - 1) break;
                    // dp[i][j]的值为∑dp[i-1][j-k]
                    dp[i][j] += dp[i - 1][j - k];
                }
            }
        }
        
        // 总的次数为6^n
        double all = Math.pow(6, n);
        // 可能出现的点数为5*n+1种
        double[] res = new double[5 * n + 1];
        for(int j = n; j <= 6 * n; j++) {
            // 向左偏移n位输出
            res[j - n] = dp[n][j] / all;
        }
        return res;
    }
}

JZ62 孩子们的游戏(圆圈中最后剩下的数)

JZ62 孩子们的游戏(圆圈中最后剩下的数)

时间复杂度:O(n),需要求解的函数值有 n 个。

空间复杂度:O(n),函数的递归深度为 n,需要使用 O(n) 的栈空间。

法一:递归(推荐使用)

实际上,本题是著名的 “约瑟夫环” 问题,可使用动态规划解决。
在这里插入图片描述
在这里插入图片描述

方式二:迭代

复杂度分析

  • 时间复杂度:O(n),需要求解的函数值有 n 个。
  • 空间复杂度:O(1),只使用常数个变量。
public class Solution {
    public int LastRemaining_Solution(int n, int m) {
		//if (n == 1)
		//	return 0;
		//return (LastRemaining_Solution(n - 1, m) + m) % n;

        int f = 0;
        for (int i = 2; i <= n; i++) {
            f = (f + m) % i;
        }
        return f;
    }
}

六、回溯

JZ12 矩阵中的路径

import java.util.*;
public class Solution {
    public boolean hasPath (char[][] matrix, String word) {
        boolean[][] visited = new boolean[matrix.length][matrix[0].length];
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                boolean flag = backTracking(matrix, visited, i, j, word, 0);
                if (flag)
                    return true;
            }
        }
        return false;
    }
    //右 下  左  上
    int[][] directions = {{0, 1},  {1, 0}, {0, -1}, {-1, 0}};
    public boolean backTracking(char[][] matrix, boolean[][] visited, int i, int j,
                                String s, int k) {
        if (matrix[i][j] != s.charAt(k))
            return false;
        else if (k == s.length() - 1)
            return true;

        boolean res = false;
        visited[i][j] = true;
        for (int[] dir : directions) {
            int newi = i + dir[0], newj = j + dir[1];
            if (newi >= 0 && newi < matrix.length && newj >= 0 && newj < matrix[0].length) {
                if (!visited[newi][newj]) {
                    boolean flag = backTracking(matrix, visited, newi, newj, s, k + 1);
                    if (flag) {
                        res = true;
                        break;
                    }
                }
            }
        }
        visited[i][j] = false;
        return res;
    }
}



import java.util.*;

public class Solution {
    int m, n;
    public boolean hasPath (char[][] matrix, String word) {
        char[] words = word.toCharArray();
        m = matrix.length;
        n = matrix[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(matrix, words, i, j, 0))
                    return true;
            }
        }
        return false;
    }

    public boolean dfs(char[][] matrix, char[] words, int i, int j, int k) {
        if (i < 0 || j < 0 || i >= m || j >= n || matrix[i][j] != words[k])
            return false;
        if (k == words.length - 1) return true;

        matrix[i][j] = '0';
        boolean res = dfs(matrix, words, i, j + 1, k + 1) ||
                      dfs(matrix, words, i + 1, j, k + 1) || dfs(matrix, words, i, j - 1, k + 1)
                      || dfs(matrix, words, i-1, j, k + 1) ;
        matrix[i][j] = words[k];
        return res;



    }
}

DFS

JZ13 机器人的运动范围

方法一:深度优先遍历 DFS

可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。

剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝

算法解析:
递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 si, sj 。
终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,返回 00 ,代表不计入可达解。

递推工作
标记当前单元格 :将索引 (i, j) 存入 Set visited 中,代表此单元格已被访问过。
搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 。
回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数。

class Solution {
    int row;
    int col;
    boolean[][] visited;
    int res = 0;
    public int movingCount(int m, int n, int k) {
        row = m;
        col = n;
        visited = new boolean[row][col];
        // 不需要循环!!
        dfs(0, 0, k);
        return res;
    }

    private void dfs(int i, int j, int k) {
        int sum = i / 10 + i % 10 + j / 10 + j % 10;
        if (i < 0 || i >= row || j < 0 || j >= col || sum > k || visited[i][j]) return;

        visited[i][j] = true;
        res++;
        dfs(i, j + 1, k);
        dfs(i, j - 1, k);
        dfs(i + 1, j, k);
        dfs(i - 1, j, k);
    }
}

剑指 Offer 38. 字符串的排列

剑指 Offer 38. 字符串的排列

注意去重的方式!!

class Solution {
    boolean[] visited;
    ArrayList<String> res = new ArrayList<>();
    StringBuilder path = new StringBuilder();

    public String[] permutation(String s) {
        int n = s.length();
        visited = new boolean[n];
        char[] arr = s.toCharArray();
        Arrays.sort(arr);
        backTrack(arr);
        return res.toArray(new String[0]);
    }

    private void backTrack(char[] arr) {

        if (path.length() == arr.length) {
            res.add(path.toString());
        }

        for (int i = 0; i < arr.length; i++) {
            if (visited[i]) continue;
            // 去重!!
            if (i > 0 && visited[i - 1] && arr[i] == arr[i - 1]) continue;

            visited[i] = true;
            path.append(arr[i]);
            backTrack(arr);
            path.deleteCharAt(path.length() - 1);
            visited[i] = false;
        }
    }
}

整理不易🚀🚀,关注和收藏后拿走📌📌欢迎留言🧐👋📣
欢迎专注我的公众号AdaCoding 和 Github:AdaCoding123
在这里插入图片描述

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值