算法
- 常用算法
- 进阶算法
- 动态规划
- 滑动窗口
- 单调栈(ppt-25节)
- 数列矩阵
- KMP算法
- 线段树 && IndexTree
- 数据结构
常用算法
进阶算法
动态规划
1. 基础知识
1.1 4大尝试模型
> 1)从左往右的尝试模型
> 2)范围上的尝试模型
> 3)多样本位置全对应的尝试模型
> 4)寻找业务限制的尝试模型
2. 经典题型
问题1:机器人走路
问题描述
假设有排成一行的N个位置,记为1~N,N 一定大于或等于 2
开始时机器人在其中的M位置上(M 一定是 1~N 中的一个)
如果机器人来到1位置,那么下一步只能往右来到2位置;
如果机器人来到N位置,那么下一步只能往左来到 N-1 位置;
如果机器人来到中间位置,那么下一步可以往左走或者往右走;
规定机器人必须走 K 步,最终能来到P位置(P也是1~N中的一个)的方法有多少种
给定四个参数 N、M、K、P,返回方法数。
解题思路:范围尝试模型
1. 准备一个指针,表示机器人当前位置。
2. 根据机器人当前的位置,判断并确定机器人下一步能走到的位置。(左走或者右走)
3. 一直重复第 2 步,直到剩余步数为零。
4. 判断是否是目标地址。如果是+1,否则+0。
代码逻辑
public class ClassicRobotWalk {
@Test
public void test01() {
System.out.println(Solution01.getAllCount(5, 2, 4, 6));
System.out.println(Solution02.getAllCount(5, 2, 4, 6));
}
/**
* 暴力递归版本
*/
private static class Solution01 {
/**
* @param n 总共的位置
* @param m 起始位置
* @param p 目标位置
* @param k 需要走 K 步
* @return 符合要求的方案数量
*/
public static int getAllCount(int n, int m, int p, int k) {
if (n < 2) {
return 0;
}
return f(n, m, k, p);
}
private static int f(int n, int cur, int rest, int target) {
if (rest == 0) {
return cur == target ? 1 : 0;
}
if (cur < 1 || cur > n) {
return 0;
}
// 向左走
int p1 = f(n, cur - 1, rest - 1, target);
// 向右走
int p2 = f(n, cur + 1, rest - 1, target);
return p1 + p2;
}
}
/**
* 动态规划版本
*/
private static class Solution02 {
/**
* @param n 总共的位置
* @param m 起始位置
* @param p 目标位置
* @param k 需要走 K 步
* @return 符合要求的方案数量
*/
public static int getAllCount(int n, int m, int p, int k) {
if (n < 2) {
return 0;
}
int[][] dp = new int[k + 1][n + 1];
dp[0][p] = 1;
for (int cur = 1; cur <= k; cur++) {
dp[cur][1] = dp[cur - 1][2];
dp[cur][n] = dp[cur - 1][n - 1];
for (int i = 2; i < n; i++) {
dp[cur][i] = dp[cur - 1][i - 1] + dp[cur - 1][i + 1];
}
}
return dp[k][m];
}
}
}
总结
1. 根据指针所指向的位置,判断机器人能走哪些位置。 (拓展:马走日,有8中走法,就是一种范围尝试模型)
问题2:两人拿卡牌
问题描述
给定一个整型数组arr,代表数值不同的纸牌排成一条线
玩家A和玩家B依次拿走每张纸牌
规定玩家A先拿,玩家B后拿
但是每个玩家每次只能拿走最左或最右的纸牌
玩家A和玩家B都绝顶聪明
请返回最后获胜者的分数。
解题思路:从左往右尝试模型
代码逻辑
总结
问题3:最长回文子序列长度
问题描述
给定一个字符串str,返回这个字符串的最长回文子序列长度
比如 : str = “a12b3c43def2ghi1kpm”
最长回文子序列是“1234321”或者“123c321”,返回长度7
解题思路:从左往右尝试模型
第一步:准备两个指针(左右指针) 第二步:左右指针开始移动,并分析存在的情况。 第三步:存在四种情况: 1. 要左要右 2. 不要左不要要右 3. 不要左要右 4. 要左不要右 第四步:取这四种情况的最大值,并返回
第五步:跳转至第二步执行,直到左右指针相碰...
代码逻辑
leetcode:最长回文序列(可中断,即去除中间部分字符)
/**
* 最长回文序列(可中断,即去除中间部分字符)
* https://leetcode.com/problems/longest-palindromic-subsequence/
*
* @author zzt
* @since 2021/5/18 22:46
*/
public class ClassicPalindromeStrMaxLen {
public static void main(String[] args) {
String s = "aaaaaabbbaaaaaaa";
System.out.println(Solution01.getStrPalindromeMaxLen1(s));
System.out.println(Solution02.getStrPalindromeMaxLen1(s));
System.out.println(Solution03.getStrPalindromeMaxLen1(s));
}
/**
* 暴力递归
*/
private static class Solution01 {
/**
* @param s 求的字符串
* @return 最长回文子序列长度
*/
public static int getStrPalindromeMaxLen1(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] cs = s.toCharArray();
return process(cs, 0, cs.length - 1);
}
private static int process(char[] cs, int l, int r) {
if (l == r) {
return 1;
}
if (l == r - 1) {
return cs[l] == cs[r] ? 2 : 1;
}
int p1 = process(cs, l + 1, r - 1);
int p2 = process(cs, l, r - 1);
int p3 = process(cs, l + 1, r);
int p4 = (cs[l] == cs[r] ? 2 : 0)
+ process(cs, l + 1, r - 1);
return Math.max(Math.max(p1, p2), Math.max(p3, p4));
}
}
/**
* 动态规划
*/
private static class Solution02 {
/**
* @param s 求的字符串
* @return 最长回文子序列长度
*/
public static int getStrPalindromeMaxLen1(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] cs = s.toCharArray();
int n = cs.length;
int[][] dp = new int[n][n];
dp[n - 1][n - 1] = 1;
for (int i = 0; i < n - 1; i++) {
dp[i][i] = 1;
dp[i][i + 1] = cs[i] == cs[i + 1] ? 2 : 1;
}
for (int l = n - 3; l >= 0; l--) {
for (int r = l + 2; r < n; r++) {
int p1 = dp[l + 1][r - 1];
int p2 = dp[l][r - 1];
int p3 = dp[l + 1][r];
int p4 = (cs[l] == cs[r] ? 2 : 0) + dp[l + 1][r - 1];
dp[l][r] = Math.max(Math.max(p1, p2), Math.max(p3, p4));
}
}
return dp[0][n - 1];
}
}
/**
* 动态规划(优化)
*/
private static class Solution03 {
/**
* @param s 求的字符串
* @return 最长回文子序列长度
*/
public static int getStrPalindromeMaxLen1(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] cs = s.toCharArray();
int n = cs.length;
int[][] dp = new int[n][n];
dp[n - 1][n - 1] = 1;
for (int i = 0; i < n - 1; i++) {
dp[i][i] = 1;
dp[i][i + 1] = cs[i] == cs[i + 1] ? 2 : 1;
}
for (int l = n - 3; l >= 0; l--) {
for (int r = l + 2; r < n; r++) {
int p2 = dp[l][r - 1];
int p3 = dp[l + 1][r];
int p4 = (cs[l] == cs[r] ? 2 : 0) + dp[l + 1][r - 1];
dp[l][r] = Math.max(p2, Math.max(p3, p4));
}
}
return dp[0][n - 1];
}
}
}
总结
解题思路:从左往右尝试模型
代码逻辑
总结
问题4:0-1背包问题
问题描述
给定两个长度都为N的数组weights和values,
weights[i]和values[i]分别代表 i号物品的重量和价值。
给定一个正数bag,表示一个载重bag的袋子,
你装的物品不能超过这个重量。
返回你能装下最多的价值是多少?
问题升级:
达到 target 价值的方案有多少种?
解题思路:从左往右尝试模型
> 1. 准备两个指针,一个用于指定数组,另一个用于指定当前背包的容量大小
> 2.
代码逻辑
public class ClassicBackpack0Or1 {
@Test
public void test01() {
int[] weights = {3, 2, 4, 7, 4, 1, 2, 2, 4};
int[] values = {5, 6, 3, 19, 8, 12, 32, 1, 4};
int bag = 18;
System.out.println(Solution01.getMaxValue(weights, values, bag));
System.out.println(Solution02.getMaxValue(weights, values, bag));
}
/**
* 暴力求解
*/
private static class Solution01 {
public static int getMaxValue(int[] w, int[] v, int bag) {
return process(w, v, 0, bag);
}
private static int process(int[] w, int[] v, int i, int curBag) {
if (i == w.length) {
return 0;
}
int p1 = process(w, v, i + 1, curBag);
int p2 = 0;
if (w[i] <= curBag) {
p2 = v[i] + process(w, v, i + 1, curBag - w[i]);
}
return Math.max(p1, p2);
}
}
/**
* 动态规划
*/
private static class Solution02 {
public static int getMaxValue(int[] w, int[] v, int bag) {
int len = w.length;
int k = bag + 1;
int[][] dp = new int[len + 1][k];
for (int i = len - 1; i >= 0; i--) {
for (int curBag = 0; curBag < k; curBag++) {
int p1 = dp[i + 1][curBag];
int p2 = 0;
if (w[i] <= curBag) {
p2 = v[i] + dp[i + 1][curBag - w[i]];
}
dp[i][curBag] = Math.max(p1, p2);
}
}
return dp[0][bag];
}
}
}
总结
先写好暴力递归,然后找到位置依赖。。。
问题5:马走日
问题描述
请同学们自行搜索或者想象一个象棋的棋盘,
然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置
那么整个棋盘就是横坐标上9条线、纵坐标上10条线的区域
给你三个 参数 x,y,k
返回“马”从(0,0)位置出发,必须走k步
最后落在(x,y)上的方法数有多少种?
解题思路:范围尝试模型
> 1. 准备两个指针,表示马所在的位置(x,y)
> 2. 根据马所在的位置(x,y),得到下一步能够到达的位置(8种走法,期间要判断是否走出了棋盘)
> 3. 判断是否达到预期效果,如果没有达到,则重复第2步,直到达到预期(预期:走完k步;或者走出了棋盘)
> 4. 将所有的情况相加得到最终的结果
代码逻辑
public class ClassicHorseWalkingWay {
public static void main(String[] args) {
System.out.println(Solution01.getAllWayCount(4, 2, 4));
System.out.println(Solution02.getAllWayCount(4, 2, 4));
}
/**
* 暴力递归
*/
private static class Solution01 {
/**
* @param x 目标坐标x
* @param y 目标坐标y
* @param k 必须要走的步数
* @return 总的方法数
*/
public static int getAllWayCount(int x, int y, int k) {
return f(x, y, 0, 0, k);
}
private static int f(int x, int y, int i, int j, int res) {
if (i < 0 || i > 10 || j < 0 || j > 9) {
return 0;
}
if (res == 0) {
return x == i && y == j ? 1 : 0;
}
int way = f(x, y, i - 1, j - 2, res - 1);
way += f(x, y, i - 1, j + 2, res - 1);
way += f(x, y, i + 1, j - 2, res - 1);
way += f(x, y, i + 1, j + 2, res - 1);
way += f(x, y, i - 2, j - 1, res - 1);
way += f(x, y, i + 2, j + 1, res - 1);
way += f(x, y, i - 2, j + 1, res - 1);
way += f(x, y, i + 2, j - 1, res - 1);
return way;
}
}
/**
* 动态规划
*/
private static class Solution02 {
/**
* @param x 坐标x
* @param y 坐标y
* @param k 必须要走的步数
* @return 总的方法数
*/
public static int getAllWayCount(int x, int y, int k) {
int[][][] dp = new int[k + 1][10][9];
dp[0][x][y] = 1;
for (int res = 1; res <= k; res++) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 9; j++) {
dp[res][i][j] = pick(dp, i + 1, j + 2, res)
+ pick(dp, i - 1, j + 2, res)
+ pick(dp, i + 1, j - 2, res)
+ pick(dp, i - 1, j - 2, res)
+ pick(dp, i - 2, j + 1, res)
+ pick(dp, i + 2, j - 1, res)
+ pick(dp, i - 2, j - 1, res)
+ pick(dp, i + 2, j + 1, res);
}
}
}
return dp[k][0][0];
}
private static int pick(int[][][] dp, int i, int j, int res) {
if (i < 0 || i > 9 || j < 0 || j > 8) {
return 0;
}
return dp[res - 1][i][j];
}
}
}
总结
问题6:求最短路径
暴力递归->动态规划4:22分钟
问题描述
给定一个二维数组matrix,一个人必须从左上角出发,最后到达右下角
沿途只可以向下或者向右走,沿途的数字都累加就是距离累加和
返回最小距离累加和
解题思路:范围尝试模型
> 1. 准备两个指针,表示人所在的位置(x,y)
> 2. 根据人所在的位置(x,y),得到下一步能够到达的位置(向下或者向右)
> 3. 判断是否达到预期效果,如果没有达到,则重复第2步,直到达到预期(预期:走完k步;或者走出了棋盘)
> 4. 将所有的情况获取最小值,并返回最终的结果
代码逻辑
public class ClassicMinPath {
public static void main(String[] args) {
int[][] matrix = {{1, 3, 1}, {1, 5, 1}, {4, 2, 1}};
System.out.println(Solution01.getMinPath(matrix));
System.out.println(Solution02.getMinPath(matrix));
}
/**
* 暴力递归
*/
private static class Solution01 {
public static int getMinPath(int[][] matrix) {
return f(matrix, 0, 0);
}
private static int f(int[][] matrix, int i, int j) {
if (i == matrix.length - 1 && j == matrix[0].length - 1) {
return matrix[i][j];
}
if (i == matrix.length || j == matrix[0].length) {
return Integer.MAX_VALUE;
}
int val = matrix[i][j];
int p1 = f(matrix, i + 1, j);
int p2 = f(matrix, i, j + 1);
return val + Math.min(p1, p2);
}
}
/**
* 动态规划
*/
private static class Solution02 {
public static int getMinPath(int[][] matrix) {
int y = matrix.length, x = matrix[0].length;
int[][] dp = new int[y][x];
int endY = y - 1;
int endX = x - 1;
dp[endY][endX] = matrix[endY][endX];
for (int i = endX - 1; i >= 0; i--) {
dp[endY][i] = dp[endY][i + 1] + matrix[endY][i];
}
for (int i = endY - 1; i >= 0; i--) {
dp[i][endX] = dp[i + 1][endX] + matrix[i][endX];
}
for (int i = endY - 1; i >= 0; i--) {
for (int j = endX - 1; j >= 0; j--) {
dp[i][j] = matrix[i][j] + Math.min(dp[i][j + 1], dp[i + 1][j]);
}
}
return dp[0][0];
}
}
}
总结
问题7:货币问题(目标值,第21节ppt)
暴力->动态规划4:42:47
问题描述
arr是货币数组,其中的值都是正数。再给定一个正数aim。
每个值都认为是一张货币,
即便是值相同的货币也认为每一张都是不同的,
返回组成aim的方法数
例如:arr = {1,1,1},aim = 2
第0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2
一共就3种方法,所以返回3
问题扩展1:
arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。
返回组成aim的方法数
例如:arr = {1,2},aim = 4
方法如下:1+1+1+1、1+1+2、2+2
一共就3种方法,所以返回3
问题扩展2:
arr是货币数组,其中的值都是正数。再给定一个正数aim。
每个值都认为是一张货币,
认为值相同的货币没有任何不同,
返回组成aim的方法数
例如:arr = {1,2,1,1,2,1,2},aim = 4
方法:1+1+1+1、1+1+2、2+2
一共就3种方法,所以返回3
解题思路:从左往右尝试模型
> 1. 问题的理解:从左往右尝试(要或不要)。并判断是否达到了目标值
> 2. 准备好两个指针,index 指向货币数组,rest 代表还差下多少(离目标值)。
> 3. 直到`index`指针==货币数组长度 或者 `rest`=0为止。并计算过程中所有的方法数之和
代码逻辑
public class ClassicCurrency {
@Test
public void test01() {
int[] arr = {1, 1, 1};
int target = 2;
System.out.println(Solution01.getAllCount(arr, target));
System.out.println(Solution02.getAllCount(arr, target));
}
/**
* 暴力递归
*/
private static class Solution01 {
/**
* @param arr 货币数组
* @param target 目标值
* @return 所有=目标值 方案数量
*/
public static int getAllCount(int[] arr, int target) {
return f(arr, 0, target);
}
private static int f(int[] arr, int index, int rest) {
if (rest == 0) {
return 1;
}
if (index == arr.length) {
return 0;
}
int p1 = f(arr, index + 1, rest);
int p2 = 0;
if (arr[index] <= rest) {
p2 = f(arr, index + 1, rest - arr[index]);
}
return p1 + p2;
}
}
/**
* 动态规划
*/
private static class Solution02 {
/**
* @param arr 货币数组
* @param target 目标值
* @return 所有=目标值 方案数量
*/
public static int getAllCount(int[] arr, int target) {
int[][] dp = new int[arr.length + 1][target + 1];
for (int i = 0; i <= arr.length; i++) {
dp[i][0] = 1;
}
for (int index = arr.length - 1; index >= 0; index--) {
for (int rest = 0; rest <= target; rest++) {
int p1 = f(arr, index + 1, rest);
int p2 = 0;
if (arr[index] <= rest) {
p2 = f(arr, index + 1, rest - arr[index]);
}
dp[index][rest] = p1 + p2;
}
}
return dp[0][target];
}
private static int f(int[] arr, int index, int rest) {
if (rest == 0) {
return 1;
}
if (index == arr.length) {
return 0;
}
int p1 = f(arr, index + 1, rest);
int p2 = 0;
if (arr[index] <= rest) {
p2 = f(arr, index + 1, rest - arr[index]);
}
return p1 + p2;
}
}
/**
* 问题扩展1:【暴力递归】
* arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
* 每个值都认为是一种面值,且认为张数是无限的。
* 返回组成aim的方法数
* 例如:arr = {1,2},aim = 4
* 方法如下:1+1+1+1、1+1+2、2+2
* 一共就3种方法,所以返回3
*/
private static class Solution11 {
/**
* @param arr 货币数组
* @param target 目标值
* @return 所有=目标值 方案数量
*/
public static int getAllCount(int[] arr, int target) {
return process(arr, 0, target);
}
private static int process(int[] arr, int index, int rest) {
if (index == arr.length) {
return rest == 0 ? 1 : 0;
}
int way = 0;
for (int i = 0; (i * arr[index]) <= rest; i++) {
way += process(arr, index + 1, rest - (i * arr[index]));
}
return way;
}
}
/**
* 问题扩展1:动态规划
*/
private static class Solution12 {
/**
* @param arr 货币数组
* @param target 目标值
* @return 所有=目标值 方案数量
*/
public static int getAllCount(int[] arr, int target) {
int[][] dp = new int[arr.length + 1][target + 1];
dp[arr.length][0] = 1;
for (int index = arr.length - 1; index >= 0; index--) {
for (int rest = 0; rest <= target; rest++) {
int way = 0;
for (int i = 0; (i * arr[index]) <= rest; i++) {
way += dp[index + 1][rest - (i * arr[index])];
}
dp[index][rest] = way;
}
}
return dp[0][target];
}
}
/**
* 问题扩展2:【暴力递归】
* arr是货币数组,其中的值都是正数。再给定一个正数aim。
* 每个值都认为是一张货币,
* 认为值相同的货币没有任何不同,
* 返回组成aim的方法数
* 例如:arr = {1,2,1,1,2,1,2},aim = 4
* 方法:1+1+1+1、1+1+2、2+2
* 一共就3种方法,所以返回3
*/
private static class Solution21 {
/**
* @param arr 货币数组
* @param target 目标值
* @return 所有=目标值 方案数量
*/
public static int getAllCount(int[] arr, int target) {
Map<Integer, Integer> coinCountMap = new HashMap<>();
int maxVal = 0;
for (int a : arr) {
if (!coinCountMap.containsKey(a)) {
coinCountMap.put(a, 1);
maxVal = Math.max(maxVal, a);
} else {
coinCountMap.put(a, coinCountMap.get(a) + 1);
}
}
int[] coinsCount = new int[maxVal + 1];
int[] coins = new int[coinCountMap.size()];
AtomicInteger i = new AtomicInteger(0);
coinCountMap.forEach((k, v) -> {
coinsCount[k] = v;
coins[i.getAndIncrement()] = k;
});
return process(coins, coinsCount, 0, target);
}
private static int process(int[] coins, int[] coinsCount, int index, int rest) {
if (index == coins.length) {
return rest == 0 ? 1 : 0;
}
int way = 0;
for (int i = 0; (i * coins[index]) <= rest && coinsCount[coins[index]] >= i; i++) {
way += process(coins, coinsCount, index + 1, rest - (i * coins[index]));
}
return way;
}
}
/**
* 问题扩展2:动态规划
*/
private static class Solution22 {
/**
* @param arr 货币数组
* @param target 目标值
* @return 所有=目标值 方案数量
*/
public static int getAllCount(int[] arr, int target) {
Map<Integer, Integer> coinCountMap = new HashMap<>();
int maxVal = 0;
for (int a : arr) {
if (!coinCountMap.containsKey(a)) {
coinCountMap.put(a, 1);
maxVal = Math.max(maxVal, a);
} else {
coinCountMap.put(a, coinCountMap.get(a) + 1);
}
}
int[] coinsCount = new int[maxVal + 1];
int[] coins = new int[coinCountMap.size()];
AtomicInteger a = new AtomicInteger(0);
coinCountMap.forEach((k, v) -> {
coinsCount[k] = v;
coins[a.getAndIncrement()] = k;
});
int[][] dp = new int[coins.length + 1][target + 1];
dp[coins.length][0] = 1;
for (int index = coins.length - 1; index >= 0; index--) {
for (int rest = 0; rest <= target; rest++) {
int way = 0;
for (int i = 0
; (i * coins[index]) <= rest && coinsCount[coins[index]] >= i
; i++) {
way += dp[index + 1][rest - (i * coins[index])];
}
dp[index][rest] = way;
}
}
return dp[0][target];
}
}
}
总结
问题8:咖啡机(京东笔试题,难)
问题描述
给定一个数组arr,arr[i]代表第i号咖啡机泡一杯咖啡的时间
给定一个正数N,表示N个人等着咖啡机泡咖啡,每台咖啡机只能轮流泡咖啡
只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯
每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发
假设所有人拿到咖啡之后立刻喝干净,
返回从开始等到所有咖啡机变干净的最短时间
三个参数:int[] arr、int N,int a、int b
解题思路:寻找业务限制的尝试模型
> 1.
代码逻辑
总结
问题9:砍怪兽(概率问题)
问题描述
给定3个参数,N,M,K
怪兽有N滴血,等着英雄来砍自己
英雄每一次打击,都会让怪兽流失[0~M]的血量
到底流失多少?每一次在[0~M]上等概率的获得一个值
求K次打击之后,英雄把怪兽砍死的概率
解题思路:(多样本位置全对应的尝试模型)
> 1.
代码逻辑
总结
问题10:醉汉走路(概率问题)
问题描述
给定5个参数,N,M,row,col,k
表示在NM的区域上,醉汉Bob初始在(row,col)位置
Bob一共要迈出k步,且每步都会等概率向上下左右四个方向走一个单位
任何时候Bob只要离开NM的区域,就直接死亡
返回k步之后,Bob还在N*M的区域的概率
解题思路:寻找业务限制的尝试模型
> 1.
代码逻辑
总结
问题11:货币问题+目标值(最值问题)
问题描述
arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。
返回组成aim的最少货币数
难点:斜率优化
:优化模型中的枚举行为
解题思路:从左往右的尝试模型 + 枚举行为
- 各种货币面值*张数 =
aim
- 准备那个两个指针:指针
index
表示面值数组的索引;rest
表示需要的目标价值
;并枚举出所有可能的张数。- 开始从左往右尝试,并枚举出所有的张数可能(总共的面值不能超过需要的最大值)
代码逻辑
public class ClassicCurrency3 {
@Test
public void test01() {
int[] arr = {1, 2, 3};
int aim = 6;
System.out.println(Solution01.getMinCurrencyCount(arr, aim));
}
private static class Solution01 {
public static int getMinCurrencyCount(int[] arr, int aim) {
// return dp(arr, 0, aim);
int n = arr.length;
int[][] dp = new int[n + 1][aim + 1];
for (int i = 1; i <= aim; i++) {
dp[n][i] = Integer.MAX_VALUE;
}
for (int index = n - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int count = Integer.MAX_VALUE;
for (int i = 0; i * arr[index] <= rest; i++) {
int next = dp[index + 1][rest - i * arr[index]];
if (next != Integer.MAX_VALUE) {
count = Math.min(count, i + next);
}
}
dp[index][rest] = count;
}
}
return dp[0][aim];
}
private static int dp(int[] arr, int index, int rest) {
if (index == arr.length) {
return rest == 0 ? 0 : -1;
}
int count = -1;
for (int i = 0; i * arr[index] <= rest; i++) {
int next = dp(arr, index + 1, rest - i * arr[index]);
if (next != -1) {
count = Math.min(count, i + next);
}
}
return count;
}
}
}
总结
问题12:货币问题+目标值+逆向分裂
问题描述
给定一个正数n,求n的裂开方法数,
规定:后面的数不能比前面的数小
比如4的裂开方法有:
1+1+1+1、1+1+2、1+3、2+2、4
5种,所以返回5
优化:有枚举行为,可以使用斜率优化
解题思路:寻找业务限制的尝试模型
1.
代码逻辑
public class ClassicCurrency4 {
// 正确性测试
@Test
public void test00() {
int count = 100000;
List<Integer> testData = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
testData.add(DataGenerator.getRandomVal(0, 20));
}
for (Integer data : testData) {
int a1 = Solution01.getAimFromArr(data);
int a2 = Solution01.dp1(data);
int a3 = Solution01.dp2(data);
if (a1 != a2 || a1 != a3) {
System.err.println("程序出错了!!!");
System.out.println("测试数据:" + data);
System.out.printf("测试结果:%d - %d - %d", a1, a2, a3);
break;
}
}
System.out.println("程序测试完成!!!");
}
/**
* 性能测试
* 结论:目标值 `aim` 不大的情况下,暴力递归更有优势
* 但在目标值比较大时(比如>20),动态规划优势就明显了,斜率优化更加明显
*/
@Test
@SuppressWarnings("all")
public void test01() throws IOException {
int count = 1;
List<Integer> testData = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
testData.add(DataGenerator.getRandomVal(500, 1000));
}
CyclicBarrier barrier = new CyclicBarrier(3);
CountDownLatch latch = new CountDownLatch(3);
Thread t1 = new Thread(() -> {
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
for (Integer aim : testData) {
Solution01.getAimFromArr(aim);
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "执行的时间:" + (end - start));
latch.countDown();
}, "暴力递归");
Thread t2 = new Thread(() -> {
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
for (Integer aim : testData) {
Solution01.dp1(aim);
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "执行的时间:" + (end - start));
latch.countDown();
}, "动态规划");
Thread t3 = new Thread(() -> {
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
for (Integer aim : testData) {
Solution01.dp2(aim);
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "执行的时间:" + (end - start));
latch.countDown();
}, "动态规划+斜率优化");
t1.start();
t2.start();
t3.start();
latch.await();
System.out.println("程序执行完毕!!!");
}
private static class Solution01 {
public static int getAimFromArr(int aim) {
return process(1, aim);
// return dp1(aim);
// return dp2(aim);
}
private static int process(int index, int rest) {
if (rest == 0) {
return 1;
}
if (index > rest) {
return 0;
}
int ans = 0;
for (int count = index; count <= rest; count++) {
ans += process(count, rest - count);
}
return ans;
}
private static int dp1(int aim) {
int n = aim + 1;
int[][] dp = new int[n][n];
dp[aim][0] = 1;
dp[aim][aim] = 1;
for (int index = aim - 1; index > 0; index--) {
dp[index][0] = 1;
dp[index][index] = 1;
for (int rest = 1; rest <= aim; rest++) {
int ans = 0;
for (int count = index; count <= rest; count++) {
ans += dp[count][rest - count];
}
dp[index][rest] = ans;
}
}
return dp[1][aim];
}
// 动态规划 + 斜率优化
private static int dp2(int aim) {
int n = aim + 1;
int[][] dp = new int[n][n];
dp[aim][0] = 1;
dp[aim][aim] = 1;
for (int index = aim - 1; index > 0; index--) {
dp[index][0] = 1;
dp[index][index] = 1;
for (int rest = index; rest <= aim; rest++) {
int ans = dp[index + 1][rest] + dp[index][rest - index];
dp[index][rest] = ans;
}
}
return dp[1][aim];
}
}
}
总结
问题13:醉汉走路(概率问题)
问题描述
给定5个参数,N,M,row,col,k
表示在NM的区域上,醉汉Bob初始在(row,col)位置
Bob一共要迈出k步,且每步都会等概率向上下左右四个方向走一个单位
任何时候Bob只要离开NM的区域,就直接死亡
返回k步之后,Bob还在N*M的区域的概率
解题思路:寻找业务限制的尝试模型
> 1.
代码逻辑
总结
问题14:数组尽可能平分(值)
问题描述
给定一个正数数组arr,
请把arr中所有的数分成两个集合,尽量让两个集合的累加和接近
返回:最接近的情况下,较小集合的累加和
解题思路:寻找业务限制的尝试模型
> 1.
代码逻辑
总结
问题15:数组尽可能平分(数量+值)
问题描述
给定一个正数数组arr,请把arr中所有的数分成两个集合
如果arr长度为偶数,两个集合包含数的个数要一样多
如果arr长度为奇数,两个集合包含数的个数必须只差一个
请尽量让两个集合的累加和接近
返回:最接近的情况下,较小集合的累加和
解题思路:寻找业务限制的尝试模型
> 1.
代码逻辑
总结
问题16:N皇后问题
问题描述
N皇后问题是指在N*N的棋盘上要摆N个皇后,
要求任何两个皇后不同行、不同列, 也不在同一条斜线上
给定一个整数n,返回n皇后的摆法有多少种。n=1,返回1
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
n=8,返回92
解题思路:寻找业务限制的尝试模型
> 1.
代码逻辑
总结
问题16:最长回文子串(字符串)
实现逻辑
class Solution {
public String longestPalindrome(String s) {
if (s.length() <= 1) {
return s;
}
char[] chars = s.toCharArray();
int len = chars.length;
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
int maxLen = 1;
int begin = 0;
// 对角线
for (int l = len - 2; l >= 0; l--) {
for (int r = l + 1; r < len; r++) {
if (chars[r] == chars[l]) {
if (r - l < 3) {
dp[l][r] = true;
} else {
dp[l][r] = dp[l + 1][r - 1];
}
}
int temp;
if (dp[l][r] && (temp = r - l + 1) > maxLen) {
maxLen = temp;
begin = l;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
3. 动态规划优化
常见优化模型
- 斜率优化
- 空间压缩优化
- 四边形不等式
- 其他优化技巧
经典案例
4. LeetCode原题
问题1:664. 奇怪的打印机(难)
5. 专题
5.1. 4种尝试模型
5.1.1 从左往右的尝试模型
问题1:0-1 背包问题
5.1.2 范围尝试模型
5.1.3 多样本位置全对应的尝试模型
5.1.4 寻找业务限制的尝试模型
5.2. 最值问题
5.3. 字符串问题
5.4. 目标值问题
5.4.1 升级版背包问题(阿里21春招)
5.4.2 货币问题 + 目标值(求最值)
5.5. 概率问题
5.5.1 砍怪兽(概率问题)
5.5.2 醉汉走路(概率问题)
5.6. 枚举行为问题
5.6.1 货币问题 + 目标值(求最值)[+逆袭分解]
5.6. 目标值问题
滑动窗口
1. 基础知识
1.1 滑动窗口是什么?
滑动窗口是一种想象出来的数据结构:
滑动窗口有左边界L和有边界R
在数组或者字符串或者一个序列上,记为S,窗口就是S[L..R]这一部分
L往右滑意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口
L和R都只能往右滑
1.2 滑动内最大值和最小值的更新结构
窗口不管L还是R滑动之后,都会让窗口呈现新状况,
如何能够更快的得到窗口当前状况下的最大值和最小值?
最好平均下来复杂度能做到O(1)
利用单调双端队列!
1.3 窗口内所有数之和(<=sum)的问题
1. 给定一个数组 `arr`、`sum`值;
2. 求区间 `(l, r)` 内之和,不大于`sum`值。
2. 经典题型
问题1:指定大小内的最大值
问题描述
假设一个固定大小为W的窗口,依次划过arr,
返回每一次滑出状况的最大值
例如,arr = [4,3,5,4,3,3,6,7], W = 3
返回:[5,5,5,4,6,7]
问题分析
1. 创建并初始化滑动窗口(大小为 w)。
2. 声明左右指针:`l`、`r`。
3. 同时移动左右指针(都+1),然后淘汰掉最小的值
解题代码
public class Classic_01_MaxVal{
public int[] getAllMax(int[] arr, int w){
if (arr.length<w){
return null;
}
int[] res = new int[arr.legnth-w];
LinkedList<Integer> win = new LinkedList<>();
for(int i = 0; i < w; i++) {
while(!win.isEmpty&&arr[i]<arr[win.peekLast()]){
win.pollLast();
}
win.addLast(i);
}
int index = 0;
res[index++] = arr[win.peekFirst()];
for(int i = w; i < arr.length; i++) {
while(!win.isEmpty&&arr[i]<arr[win.peekLast()]){
win.pollLast();
}
win.addLast(i);
if(win.peekFirst() <= i-w) {
win.pollFirst();
}
res[index++] = arr[win.peekFirst()];
}
return res;
}
}
问题2:区间内的最大值和最小值问题
问题描述
给定一个整型数组arr,和一个整数num
某个arr中的子数组sub,如果想达标,必须满足:
sub中最大值 – sub中最小值 <= num,
返回arr中达标子数组的数量
问题分析
- 问题中需要用到窗口的最大值和最小值。
- 准备两个窗口,一个最大值窗口,一个最小值窗口
解题代码
private static class Solution02 {
public static int getMaxDiscrepancy(int[] arr, int num) {
int count = 0;
LinkedList<Integer> max = new LinkedList<>();
LinkedList<Integer> min = new LinkedList<>();
int r = 0;
for (int l = 0; l < arr.length; l++) {
while (r < arr.length) {
while (!max.isEmpty() && arr[r] > arr[max.peekLast()]) {
max.pollLast();
}
max.addLast(r);
while (!min.isEmpty() && arr[r] < arr[min.peekLast()]) {
min.pollLast();
}
min.addLast(r);
if (arr[max.peekFirst()] - arr[min.peekFirst()] <= num) {
r++;
} else {
break;
}
}
count += r - l;
// 淘汰过期数据
if (max.peekFirst() == l) {
max.pollFirst();
}
if (min.peekFirst() == l) {
min.pollFirst();
}
}
return count;
}
}
问题3:货币分裂问题
问题描述
arr是货币数组,其中的值都是正数。再给定一个正数aim。
每个值都认为是一张货币,
返回组成aim的最少货币数
注意:因为是求最少货币数,所以每一张货币认为是相同或者不同就不重要了
问题分析
- 问题中需要用到窗口的最大值和最小值。
- 准备两个窗口,一个最大值窗口,一个最小值窗口
解题代码
总结
问题4:加油站问题
问题描述
问题分析 & 解题思路
1. 声明一个`2n+1`长度的前缀和数组,并求出所有的前缀之和(`gas[i]-cost[i]`),
2. 声明一个滑动窗口(前缀和数组最小值),并求出范围为`0 ~ n`的滑动窗口
注意:判断特殊值(右边最初是 0)
3. 然后求出范围为`n+1 ~ 2n+1`内的前缀和数组值,`目标值 = 最小值-当前值`(注意有淘汰过期数据)
解题代码
总结
3. LeetCode原题
问题1:加油站问题
单调栈(ppt-25节)
基础知识
案例分析
一种特别设计的栈结构,为了解决如下的问题:
给定一个可能含有重复值的数组arr
,i
位置的数一定存在如下两个信息
1)arr[i]
的左侧离i最近并且小于(或者大于)arr[i]
的数在哪?
2)arr[i]
的右侧离i最近并且小于(或者大于)arr[i]
的数在哪?
如果想得到arr
中所有位置的两个信息,怎么能让得到信息的过程尽量快。
那么到底怎么设计呢?
基本思想
(求最近小于的数为例)
1. 准备一个指针`index`,表示数组的索引位置,还有一个栈结构(记录`索引位置`)
(如果有重复值,可以考虑用数组解决,但不一定都要,根据实际情况而定)。
2. 将所有比栈顶**大**的值放入栈中,反之将栈顶索引位置(这是目标位置),两侧索引就是最近的小于`index`位置的值(如果左侧没有则为`-1`)。
3. 直到`index`遍历结束,然后再依次弹出栈顶中的索引位置。
难点(重点)
:
- 目标索引位置左右两边的含义
- 目标索引位置已经最近的
小于
的数之间的数字又代表什么含义
经典题型
题目一:单调栈的实现
代码实现
无重复值版本
public class Demo {
/**
* @param arr 求解数组
* @return [ lSmall rSmall
* 0: -1 3
* 1: 0 4
* ]
*/
public static int[][] monotonicStack(int[] arr) {
int[][] rest = new int[arr.length][2];
Stack<Integer> stackIndex = new Stack<>();
for (int large = 0; large < arr.length; large++) {
while (!stackIndex.isEmpty()
&& arr[stackIndex.peek()] > arr[large]) {
Integer targetIndex = stackIndex.pop();
int small = stackIndex.isEmpty() ? -1 : stackIndex.peek();
rest[targetIndex][0] = small;
rest[targetIndex][1] = large;
}
stackIndex.push(large);
}
while (!stackIndex.isEmpty()) {
int targetIndex = stackIndex.pop();
int small = stackIndex.isEmpty() ? -1 : stackIndex.peek();
rest[targetIndex][0] = small;
rest[targetIndex][1] = -1;
}
return rest;
}
}
有重复值版本
public class Demo{
/**
* @param arr 求解数组
* @return [ small large
* 0: -1 3
* 1: 0 4
* ]
*/
public static int[][] monotonicStack(int[] arr) {
int[][] rest = new int[arr.length][2];
Stack<List<Integer>> stack = new Stack<>();
for (int large = 0; large < arr.length; large++) {
while (!stack.isEmpty() && arr[large] < arr[stack.peek().get(0)]) {
List<Integer> targetIndexList = stack.pop();
int smallIndex = stack.isEmpty() ? -1 : stack.peek().get(0);
for (Integer targetIndex : targetIndexList) {
rest[targetIndex][0] = smallIndex;
rest[targetIndex][1] = large;
}
}
if (stack.isEmpty() || arr[stack.peek().get(0)] != arr[large]) {
LinkedList<Integer> list = new LinkedList<>();
list.addLast(large);
stack.push(list);
} else {
List<Integer> peek = stack.peek();
peek.add(large);
}
}
while (!stack.isEmpty()) {
List<Integer> targetIndexList = stack.pop();
int smallIndex = stack.isEmpty() ? -1 : stack.peek().get(0);
for (Integer targetIndex : targetIndexList) {
rest[targetIndex][0] = smallIndex;
rest[targetIndex][1] = -1;
}
}
return rest;
}
}
题目二:窗口内最值问题
问题描述
给定一个只包含正数的数组arr,arr中任何一个子数组sub
一定都可以算出(sub累加和 )* (sub中的最小值)是什么
那么所有子数组中,这个值最大是多少?
问题分析
1. 找到问题核心:sub数组*内部最小值(两边取小的单调栈)
2. 理解单调栈中的取值特点(两边取小,中间就是较大值)
public class Classic_02_SubSumAndMin {
public static void main(String[] args) {
int[] arr = {3, 2, 4, 1, 5, 8, 7, 9};
System.out.println(7 * ( 7 + 8 + 9));
System.out.println(max1(arr));
System.out.println(getMax(arr));
}
public static int max1(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
int minNum = Integer.MAX_VALUE;
int sum = 0;
for (int k = i; k <= j; k++) {
sum += arr[k];
minNum = Math.min(minNum, arr[k]);
}
max = Math.max(max, minNum * sum);
}
}
return max;
}
public static int getMax(int[] arr) {
int[] sums = getSums(arr);
return monotonic(arr, sums);
}
private static int monotonic(int[] arr, int[] sums) {
int size = arr.length;
Stack<Integer> stack = new Stack<>();
int max = 0;
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[i] <= arr[stack.peek()]) {
Integer large = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[large]);
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
}
return max;
}
private static int[] getSums(int[] arr) {
int[] sums = new int[arr.length];
sums[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
sums[i] = arr[i] + sums[i - 1];
}
return sums;
}
}
题目三:柱状图中最大的矩形
问题描述
给定一个非负数组arr,代表直方图
返回直方图的最大长方形面积
问题分析(木桶原理)
1. 本质:区间最小值的高度 * 宽度。问题在于`最小值的高度`求解
2. 单调栈中(两边取小),可以完美的解决此类问题。
题目四:最大矩形
问题分析
1. 求`子数组最小值之和`子数组最小值之和问题,可以转化为求单调栈(两边最近小于的栈结构)
2. 求arr[i]左边,离arr[i]最近,<=arr[i],位置在x
3. 求arr[i]右边,离arr[i]最近,< arr[i],的数,位置在y
4. 两边距离 `i` 位置的个数就是`(i-x)*(y-i)`,
所有以`i`位置作为最小值的子数组,值之和就是`(i-x)*(y-i)*arr[i]`
题目五:
题目六:子数组的最小值之和
问题分析
1. 求`子数组最小值之和`子数组最小值之和问题,可以转化为求单调栈(两边最近小于的栈结构)
2. 求arr[i]左边,离arr[i]最近,<=arr[i],位置在x
3. 求arr[i]右边,离arr[i]最近,< arr[i],的数,位置在y
4. 两边距离 `i` 位置的个数就是`(i-x)*(y-i)`,
所有以`i`位置作为最小值的子数组,值之和就是`(i-x)*(y-i)*arr[i]`
代码逻辑
LeetCoded原题训练
专题训练
数列矩阵
基础知识
经典题型
1. 斐波那契数列
问题描述
问题分析
逻辑代码
总结
2. 爬楼梯
3. 牛生牛问题
问题描述:
第一年农场有1只成熟的母牛A,往后的每年:
1)每一只成熟的母牛都会生一只母牛
2)每一只新出生的母牛都在出生的第三年成熟
3)每一只母牛永远不会死
返回N年后牛的数量
问题分析 & 解题思路
解题思路:F(n) = F(N-1) + F(N-3)
KMP算法
基础知识
使用解题范围:
- 字符串
s
中是否包含 连续子字符串m
问题分析:
- 常见思路:每个字符依次进行判断(思路很简单,不多叙述)。
- 如果存在字符串
s
=aaaaaaaaaaaaaab
,并判断是否存在子字符串m
=aaaaaab
。
这样的话,用普通的方法进行求解,时间复杂度太高。- 上述的问题,在于
a
字符重复很多,无效判断太多了。
常见解题思路:
- 处理子字符串
m
,并得到其中的重复情况。
经典题型
问题1:KMP经典实现
问题描述
给定两个字符串
s
和match
,
求s
是否存在match
子串,并返回第一个字符的位置
解题思路
1. 创建好 `match` 的 `next` 数组(记录上一个匹配的记录)
代码逻辑
class Kmp{
public static int getIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
return -1;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int x = 0;
int y = 0;
// O(M) m <= n
int[] next = getNextArray(str2);
// O(N)
while (x < str1.length && y < str2.length) {
if (str1[x] == str2[y]) {
x++;
y++;
} else if (next[y] == -1) { // y == 0
x++;
} else {
y = next[y];
}
}
return y == str2.length ? x - y : -1;
}
public static int[] getNextArray(char[] str2) {
if (str2.length == 1) {
return new int[] { -1 };
}
int[] next = new int[str2.length];
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组的值
int cn = 0; // 当前是哪个位置的值再和i-1位置的字符比较
while (i < next.length) {
if (str2[i - 1] == str2[cn]) { // 配成功的时候
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0; //此时的 cn 屹然是0(第一个位置,所以必然是 1 )
}
}
return next;
}
}
问题2:子树相等
问题描述
给定两棵二叉树的头节点head1和head2
想知道head1中是否有某个子树的结构和head2完全一样
解题思路
代码逻辑
总结
算法优化
LeetCoded原题训练
专题训练
线段树 && IndexTree
基础知识
线段树
解决的问题
- 区间相同操作
例如:
1. 区间内`3~200`之内的数字全部`+6`
2. 区间内`7~565`之内的数字全部更新为`7`。
3. 区间内`3~990`之内的数字累加和是多少?
(上述所有的操作,都是O(n)级别的 )
IndexTree
解决的问题
1. 前缀数之和,问题的基础上,更改某个位置的值,求`某区间范围内之和`?
经典题型
1. 线段树结构
问题描述
问题分析
逻辑代码
总结
2. IndexTree结构
问题描述
问题分析
逻辑代码
总结