提交 34eb5b92 编写于 作者: qq_36480062's avatar qq_36480062

commit

上级 79b1c61b
......@@ -3,7 +3,9 @@ package dp;
import java.util.Arrays;
/**
* LCS最优实现:
* LeetCode 第 300 号问题
* 最长递增子序列(最长上升子序列)
* LIS最优实现:
* 设dp为动态规划辅助数组,dp[0]不存值,也没有任何意义
* dp[i]代表:长度为i的最长递增子序列末尾的数(源数组的数)
* 4, 2, 3, 1, 5, 6
......@@ -11,16 +13,17 @@ import java.util.Arrays;
*/
public class LIS {
public static void main(String[] args) {
int[] arr = {4, 2, 3, 1, 5, 6};
System.out.println(Arrays.toString(arr));
// System.out.println(Baoli(arr));
System.out.println(dp(arr, arr.length));
System.out.println(Arrays.toString(dp));
System.out.println();
// int[] arr = {4, 2, 3, 1, 5, 2, 6};
int i=1;
System.out.println(-i);
// for (int i = 0; i < 1000000; i++) {
// int[] arr = random(200);
// if (dpBest(arr) != dp(arr, arr.length))
// System.out.println("no");
// }
}
public static int[] random(int n) {
int arr[] = new int[n];
for (int i = 0; i < n; i++) {
......@@ -33,27 +36,43 @@ public class LIS {
public static int[] dp;
/**
* O(N logN)最优实现
* @param arr
* O(N logN)最优实现,LeetCode官方题解非常牛逼
*
* @param nums
* @return
*/
public static int dpBest(int[] arr) {
dp = new int[arr.length + 1];
dp[1] = arr[0];//结合数组语义初始化:长度为1的最长递增子序列,为源数组第一个元素
int p = 1;//记录dp更新的最后位置
for (int i = 1; i < arr.length; i++) {
if (arr[i] > dp[p]) {
dp[p + 1] = arr[i];
p++;
} else {//扫描dp数组:用arr[i]的值,替换为在dp数组中扫到的第一个比arr[i]大的值
for (int j = 0; j <= p; j++) {//可以优化为二分搜索
if (dp[j] < arr[i]) {
dp[j] = arr[i];
}
}
public static int dpBest(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
int len = 0;
for (int num : nums) {
int i = Arrays.binarySearch(dp, 0, len, num);
if (i < 0) {
i = -(i + 1);
}
dp[i] = num;
if (i == len) {
len++;
}
}
return p;
return len;
}
public static int indexOfFirstBigger(int[] dp, int v, int l, int r) {
while (l <= r) {
int mid = (l + r) >> 1;
if (dp[mid] > v) {
r = mid; //保留大于v的下标以防这是第一个
} else {
l = mid + 1;
}
if (l == r && dp[l] > v)
return l;
}
return -1;
}
/**
......@@ -80,17 +99,20 @@ public class LIS {
* <p>
* dp[0]初始化为1,有一堆候选值选最大的
*
* @param arr 需要求的源数组
* @param n 数组的长度
* @return 最长递增子序列
* @param nums 需要求的源数组
* @param n 数组的长度
* @return 最长递增子序列的长度
*/
public static int dp(int[] arr, int n) {
dp = new int[n];
public static int dp(int[] nums, int n) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[n];
dp[0] = 1;
for (int i = 1; i < dp.length; i++) {//以i为结尾
int cnt = 1;
for (int j = i - 1; j >= 0; j--) {//从当前索引-1往前找
if (arr[i] > arr[j]) {//从源数组中找到比当前元素还小的值,就把在dp数组中求出的值+1与cnt作比较,并更新cnt
if (nums[i] > nums[j]) {//从源数组中找到比当前元素还小的值,就把在dp数组中求出的值+1与cnt作比较,并更新cnt
//for结束后,最后cnt表示以arr[i]结尾,之前的最长递增子序列的长度
cnt = Math.max(cnt, dp[j] + 1);
}
......@@ -104,6 +126,45 @@ public class LIS {
return ans;
}
/**
* :最长上升子序列。与LIS相同,另一种实现,思路
* 给定一个无序的整数数组,找到其中最长上升子序列的长度。
* 示例:
* 输入: [10,9,2,5,3,7,101,18]
* 输出: 4
* 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
* 说明:
* 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
* 你算法的时间复杂度应该为 O(n2) 。
* 进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
* <p>
* 思路与上一题类似:
* dp数组语义,dp[i] 表示以位置 i 结尾的子序列的最大长度
*
* @param nums
* @return
*/
public static int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// dp[i] -> the longest length sequence from 0 - i, and must include nums[i]
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);//初始化dp数组为1,因为每个子序列可以只有1个元素
int max = 0;//记录最长的序列
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {//寻找i之前的元素
if (nums[i] > nums[j])//基准元素,arr[i],找在它之前的比它小的元素
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
max = Math.max(max, dp[i]);//寻找dp数组中最大的元素,因为不一定最长递增子序列在最后
}
return max;
}
/**
* 暴力解法,找到打头的数字,i就是打头开始,p就是每次找到的第二个,该算法本身就是bug,这个算法没啥用
* 没有参考价值,测试一半通不过,AC看脸
......
package dp;
/**
* 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,
* 影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
* 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
* 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
* 示例 1:
* 输入: [1,2,3,1]
* 输出: 4
* 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
* 偷窃到的最高金额 = 1 + 3 = 4 。
* 示例 2:
* 输入: [2,7,9,3,1]
* 输出: 12
* 解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
* 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
*/
public class 打家劫舍 {
public static void main(String[] args) {
}
}
package dp;
import java.util.Arrays;
/**
* https://mp.weixin.qq.com/s?__biz=MzUyNjQxNjYyMg==&mid=2247486923&idx=2&sn=6c1c8aeb4db68522e67ddf8c1e933660&chksm=fa0e624acd79eb5cdb410808921609a830b9b9221e813e4eb89cf551ca48f317668d44b095d2&scene=21#wechat_redirect
* LeetCode 第 221 号问题:最大正方形。
* 在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
* 示例:
......@@ -11,10 +14,106 @@ package dp;
* 1 0 0 1 0
* <p>
* 输出: 4
* 题目给定一个字符矩阵,字符矩阵中只有两种字符,分别是 ‘0’ 和 ‘1’
* 题目要在矩阵中找全为 ‘1’ 的,面积最大的正方形。
* <p>
* 因为我们考虑的是正方形的右下方的顶点,
* 因此状态可以定义成 “当前点为正方形的右下方的顶点时,正方形的最大面积”
*/
public class 最大正方形 {
public static void main(String[] args) {
int[][] arr = new int[3][4];
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
if (Math.random() * 15 < 12)
arr[i][j] = 1;
}
}
for (int[] ints : arr) {
System.out.println(Arrays.toString(ints));
}
System.out.println(maximalSquare(arr));
}
/**
* 非常巧妙,注意思路,非常牛逼
* 因为我们考虑的是正方形的右下方的顶点,因此状态可以定义成 “当前点为正方形的右下方的顶点时,正方形的最大面积”
* 有了状态,我们再来看看递推方程如何写,前面说到我们可以从当前点向三个方向延伸,我们看相邻的位置的状态,
* <p>
* 这里我们需要取三个方向的状态的最小值才能确保我们延伸的是全为 ‘1’ 的正方形,也就是
* dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
* <p>
* 两个例子
* [1, 1, 1, 1]
* [1, 1, 1, 0] 源数组
* [1, 1, 1, 0]
* ------------
* [1, 1, 1, 1]
* [1, 2, 2, 0] 动态规划数组
* [1, 2, 3, 0]
* <p>
* [1, 1, 1, 1]
* [1, 1, 0, 1] 源数组
* [1, 0, 1, 1]
* ------------
* [1, 1, 1, 1]
* [1, 2, 0, 1] 动态规划数组
* [1, 0, 1, 1]
* 首先看状态定义:把dp[i][j]语义定义为,把(i,j)作为正方形的右下角顶点时,这个正方形的最大面积(直接得出的是边长,不是面积)
* 则直接找上面,左边,左上角来组成正方形,则在源数组中必须要这三个位置都为1,才能组成正方形
* 所以寻找这三个值中最小的+1,就符合(i,j)作为正方形的右下角顶点时,这个正方形的边长
* 寻找状态转移方程,
*
* @param matrix 矩阵
* @return 最大面积
*/
public static int maximalSquare(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
int m = matrix.length, n = matrix[0].length;
int[][] dp = new int[m][n];
int maxLength = 0;//边长
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 1) {
if (i == 0 || j == 0)
dp[i][j] = matrix[i][j] == 1 ? 1 : 0;//第一排,第一列,没有上面,左边,左上角来组成大正方形,直接给1
else
dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
maxLength = Math.max(dp[i][j], maxLength);
}
}
}
System.out.println("-----");
for (int[] ints : dp) {
System.out.println(Arrays.toString(ints));
}
return maxLength * maxLength;
}
}
/**
* 题目给定一个字符矩阵,字符矩阵中只有两种字符,分别是 ‘0’ 和 ‘1’,题目要在矩阵中找全为 ‘1’ 的,面积最大的正方形。
* 首先一个正方形是由四个顶点构成的,如果说我们在矩阵中随机找四个点,然后判断该四个点组成的是不是正方形,如果是正方形,然后看组成正方形的每个位置的元素是不是都是 ‘1’,这种方式也是可行的,但是比较暴力,这么弄下来,时间复杂度是 O((m*n)^4)。
* 那我们就会思考,组成一个正方形是不是必须要四个点都找到?如果我们找出其中的三个点,甚至说两个点,能不能确定这个正方形呢?
* 你会发现,这里我们只需要考虑 正方形对角线的两个点 即可,这两个点确定了,另外的两个点也就确定了,因此我们可以把时间复杂度降为 O((m*n)^2)。
* 但是这里还是会有一些重复计算在里面,我们和之前一样,本质还是在做暴力枚举,只是说枚举的个数变少了,我们能不能记录我们之前得到过的答案,通过牺牲空间换取时间呢,这里正是动态规划所要做的事情!
*
* 我们可以思考,如果我们从左到右,然后从上到下遍历矩阵,假设我们遍历到的当前位置是正方形的右下方的点,那其实我们可以看之前我们遍历过的点有没有可能和当前点组成符合条件的正方形,除了这个点以外,无非是要找另外三个点,这三个点分别在当前点的上方,左方,以及左上方,也就是从这个点往这三个方向去做延伸,具体延伸的距离是和其相邻的三个点中的状态有关
*
* 状态定义
*
* 因为我们考虑的是正方形的右下方的顶点,因此状态可以定义成 “当前点为正方形的右下方的顶点时,正方形的最大面积”
*
* 递推方程
*
* 有了状态,我们再来看看递推方程如何写,前面说到我们可以从当前点向三个方向延伸,我们看相邻的位置的状态,这里我们需要取三个方向的状态的最小值才能确保我们延伸的是全为 ‘1’ 的正方形,也就是
*
* dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
*
* 在实现上,我们需要单独考虑两种情况,就是当前位置是 ‘1’,还有就是当前位置是 ‘0’,如果是 ‘0’ 的话,状态就是 0,表示不能组成正方形,如果是 ‘1’ 的话,我们也需要考虑位置,如果是第一行的元素,以及第一列的元素,表明该位置无法同时向三个方向延伸,状态直接给为 1 即可,其他情况就按我们上面得出的递推方程来计算当前状态。
**/
\ No newline at end of file
......@@ -25,7 +25,8 @@ public class 最小路径和 {
/**
* dp[i][j]代表走到第i行,第j列,走过来的最小路径和,
* dp[i][j] = Math.min(dp[i - 1][j] + dp[i][j - 1]) + arr[i][j]
* 该递推方程代表,走到(i,j)的值,应当是(i-1,j)和(i,j-1)之间的较小值
* 该递推方程代表,走到(i,j)的值,应当是(i-1,j)和(i,j-1)之间的较小值,
* 再加上该位置的值,就是到(i,j)的最小路径和
*
* @param arr 该数组
* @return 最小路径和
......@@ -53,4 +54,4 @@ public class 最小路径和 {
}
return dp[m - 1][n - 1];
}
}
}
\ No newline at end of file
package dp;
/**
* LeetCode 第 256 号问题:粉刷房子,非常精妙,
* https://mp.weixin.qq.com/s?__biz=MzUyNjQxNjYyMg==&mid=2247486936&idx=1&sn=27ec53c1463384edeeee138db23c1c7d&chksm=fa0e6259cd79eb4f240786ad7c00dcd0ed39ad68fd62b72e15d6a8ee0ecbd26b6250ab3b1171&scene=21#wechat_redirect
* 假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
* 当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的矩阵来表示的。
* 例如,costs[0][0]表示第 0 号房子粉刷成红色的成本花费;costs[1][2]表示第 1 号房子粉刷成绿色的花费,以此类推。请你计算出粉刷完所有房子最少的花费成本。
* 所有花费均为正整数。
* 示例:
* 输入: [[17,2,17],[16,16,5],[14,3,19]] //顺序红色、蓝色或者绿色
* 输出: 10
* 解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
* 最少花费: 2 + 5 + 3 = 10。
*/
public class 粉刷房子 {
public static void main(String[] args) {
int[][] arr={{17,2,17},{16,16,5},{14,3,19}};
System.out.println(Fen(arr));
}
public static int[][] dp;
/**
* 问题拆解
* <p>
* 对于每个房子来说,都可以使用三种油漆当中的一种,如果说不需要保证相邻的房子的颜色必须不同,
* 那么整个题目会变得非常简单,每个房子直接用最便宜的油漆刷就好了,但是加上这个限制条件,
* 你会发现刷第 i 个房子的花费其实是和前面 i - 1 个房子的花费以及选择相关,如果说我们需要知道第 i 个房子使用第 k 种油漆的最小花费,
* 那么你其实可以思考第 i - 1 个房子如果不用该油漆的最小花费,这个最小花费是考虑从 0 到当前位置所有的房子的。
* 状态定义
* <p>
* 通过之前的问题拆解步骤,状态可以定义成 dp[i][k],表示如果第 i 个房子选择第 k 个颜色,
* 那么从 0 到 i 个房子的最小花费
* 递推方程
* <p>
* 基于之前的状态定义,以及相邻的房子不能使用相同的油漆,那么递推方程可以表示成:
* <p>
* dp[i][k] = Math.min(dp[i - 1][l], ..., dp[i - 1][r]) + costs[i][k], l != k, r != k
* 实现
* <p>
* 因为我们要考虑 i - 1 的情况,但是第 0 个房子并不存在 i - 1 的情况,因此我们可以把第 0 个房子的最小花费存在状态数组中,当然你也可以多开一格 dp 状态,其实都是一样的。
* 对于这道题目,你可能会问这不是和矩阵类动态规划类似吗?
* 如果单从房子来考虑的确是,但是对于颜色的话,我们必须考虑考虑相邻房子的所有颜色,这就有点序列的意思在里面了。
*/
public static int Fen(int[][] nums) {
if (nums.length == 0 || nums[0].length == 0) {
return 0;
}//没有东西,直接返回0
int n = nums.length;
dp = new int[n][3];
for (int i = 0; i < nums[0].length; i++) {
dp[0][i] = nums[0][i];
}//先初始化第一行,要理解dp数组语义
for (int i = 1; i < n; i++) {
dp[i][0] = Math.min(dp[i - 1][1], dp[i - 1][2]) + nums[i][0];
dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][2]) + nums[i][1];
dp[i][2] = Math.min(dp[i - 1][0], dp[i - 1][1]) + nums[i][2];
}
return Math.min(dp[n - 1][0], Math.min(dp[n - 1][2], dp[n - 1][1]));
}
}
......@@ -36,6 +36,17 @@ dp[i]语义就是走i阶楼梯能有多少种选择
大白话:把已知的数据存在数组里,作为基本条件,然后再根据数组语义找出dp方程,
也就是前面得到的答案可以推出后面的答案
解释一下子数组(子串)和子序列的区别,你可以看看下面这个例子:
输入数组:[1,2,3,4,5,6,7,8,9]
子数组:[2,3,4], [5,6,7], [6,7,8,9], ...
子序列:[1,5,9], [2,3,6], [1,8,9], [7,8,9], ...
可以看到的是,子数组必须是数组中的一个连续的区间,而子序列并没有这样一个要求。
你只需要保证子序列中的元素的顺序和原数组中元素的顺序一致即可,例如,在原数组中,元素 1 出现在元素 9 之前,那么在子序列中,如果这两个元素同时出现,那么 1 也必须在 9 之前。
不知道你有没有发现,这里的子数组的问题和我们前面提到的矩阵类动态规划的分析思路很类似,只需要考虑当前位置,以及当前位置和相邻位置的关系。
通过这样的分析就可以把之前讲的内容和今天要介绍的内容关联起来了,相比矩阵类动态规划,序列类动态规划最大的不同在于,对于第 i 个位置的状态分析,它不仅仅需要考虑当前位置的状态,还需要考虑前面 i - 1 个位置的状态,这样的分析思路其实可以从子序列的性质中得出。
对于这类问题的问题拆解,有时并不是那么好发现问题与子问题之间的联系,但是通常来说思考的方向其实在于 寻找当前状态和之前所有状态的关系,我们通过几个非常经典的动态规划问题来一起看看。
https://mp.weixin.qq.com/s?__biz=MzUyNjQxNjYyMg==&mid=2247486936&idx=1&sn=27ec53c1463384edeeee138db23c1c7d&chksm=fa0e6259cd79eb4f240786ad7c00dcd0ed39ad68fd62b72e15d6a8ee0ecbd26b6250ab3b1171&scene=21#wechat_redirect
矩阵类动态规划,也可以叫做坐标类动态规划,一般这类问题都会给你一个矩阵,矩阵里面有着一些信息,然后你需要根据这些信息求解问题。
其实 矩阵可以看作是图的一种,怎么说?你可以把整个矩阵当成一个图,
矩阵里面的每个位置上的元素当成是图上的节点,然后每个节点的邻居就是其相邻的上下左右的位置,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册