剑指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 最长不含重复字符的子字符串 【难!!!】
方法一:动态规划
-
dp[i]表示i位置的不含重复字符的最长字符串长度
-
公式 【不懂??】
总共两种情形:
-
字符没有出现过,长度加一;
-
字符出现过:此时又有两种情况:
-
这个字符上一次出现的位置没有算在在当前最长不重复子串上,例如:dfafbsdas,遍历到第二个a时,第一个a不在最长不重复子串上,所以长度也是直接加一; f(n) = f(n - 1) + 1
-
这个字符上一次出现的位置算在在当前最长不重复子串上,例如:fabsdas,遍历到第二个a时,当前位置的长度重新计算为两个重复字符之间的距离,就是bsda; f(n) = index - lastIndex
-
-
-
初始化
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. 丑数!!!
求按从小到大的顺序的第 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个骰子的点数
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 孩子们的游戏(圆圈中最后剩下的数)
时间复杂度: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. 字符串的排列
注意去重的方式!!
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