动态规划:不同的定义产生不同的解法
文章链接
收获
对状态机的定义不同,得到的状态转移方程就完全不同
经典面试题:最长回文子串(LeetCode第5题)
文章链接
收获
回文子串有两个做法,一个是从中间展开的算法,一个是两侧向中间靠齐的dp
单调栈
文章链接
收获
- 单调栈算法过程
- 处理Next Greater Number,从后向前遍历
- 先判断 栈是否为空 和 栈顶的值是不是比即将入栈的值小
- 如果栈空,则说明找不到比该数大的数,将其Next Greater Number置为-1
- 栈不为空,直到找到比该数大的数,将其置为栈顶,比其小的也就没有存在的意义了
- 然后将该数的Next Greater Number定为栈顶元素,并将该数加入栈中
- 处理循环数组
- 将该数组的双倍长度构造出来,这样的话就就不仅能比较右边的数,还能比较左边的数了
二分查找算法详解
文章链接
二分查找的框架
最基本的框架1(形式是[a,b))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int binarySearch(int nums[],int target){
int left = 0;
int right = nums.length;
while(left < right){
//防止计算mid时溢出
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if(nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid;
}
return -1;
}最基本的框架2 (形式是[a,b])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int binarySearch(int nums[],int target){
int left = 0;
int right = nums.length - 1;
while(left <= right){
//防止计算mid时溢出
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if(nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid - 1;
}
return -1;
}寻找左侧边界的二分搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* 返回的是比target小的数量,同时也是最左target的下标
* @param nums
* @param target
* @return
*/
int binarySearchLeft(int nums[],int target){
int left = 0;
int right = nums.length;
while(left < right){
//防止计算mid时溢出
int mid = left + (right - left) / 2;
if(nums[mid] == target)
right = mid;
else if(nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid;
}
//要时刻注意处理边界,如果整个数组都没有这个target,则left会到nums.length
// target 比所有数都大
if (left == nums.length) return -1;
// 类似之前算法的处理方式
return nums[left] == target ? left : -1;
}寻找右侧边界的二分搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/**
* 返回的是最右target的下标
* @param nums
* @param target
* @return
*/
int binarySearchRight(int nums[],int target){
int left = 0;
int right = nums.length;
while(left < right){
//防止计算mid时溢出
int mid = left + (right - left) / 2;
if(nums[mid] == target)
left = mid + 1;
else if(nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid;
}
//要时刻注意处理边界,如果整个数组都没有这个target,则left会到nums.length
// target 比所有数都大
// return left - 1;
if (left == 0) return -1;
// 类似之前算法的处理方式
return nums[left-1] == target ? (left-1) : -1;
}
收获
- 左侧边界的二分搜索,left即代表比target小的数的数量
- 右侧边界的二分搜索,left即代表比target大的第一个数的下标,比如是5,则代表索引为4是target的最右侧数
最长递归子序列(LeetCode第300题)
文章链接
两个方法:
- dp
- 扑克牌算法,运用二分查找
dp
注意事项
最重要的是先确定状态转移方程,并且明确dp数组的含义,这里要求的是最长递归子序列,所建立的dp数组是一个一维数组(为何选取的是一维数组呢?),并且dp[i]的含义是指以索引i结尾的最长递归子序列。
采用归纳法可知,若dp[0…i-1]可知,则
1 | for(int j = 0; j < i;j++){ |
即dp[i]是之前的dp[0…i-1],判断其是否小于nums[i],若小于则nums[i]可以接到其后面,最后选取一个最长的递归子序列即可。
还有要考虑的就是base case,由于是最短递归子序列至少为1,所以base case为1,所以dp数组全部初始化为1即可。
代码
1 | public int lengthOfLtsByDp(int nums[]){ |
扑克牌算法
最长递归子序列其实类似于我们平时玩的蜘蛛纸牌,eg:2,3,1,9,7,4,6,3 ,同一堆的牌只能是小的放在大的上面,如上,就分成2,1 3 9,7,6,3 4 一共4堆,则最长递归子序列数为 4 。
至于如何处理扑克牌放置问题,则可以采用二分查找,因为堆顶都是有序的。
1 | public int lengthOfLtsByBinarySearch(int[] nums){ |
子序列解题模板
文章链接
收获
两种思路
dp数组为一维数组。常见题型有
最长递增子序列
。在子数组array[0..i]
中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]
。模板:
1
2
3
4
5
6
7
8int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
dp数组为二维数组。二维数组又分为是对两字符串而言和一个字符串而言。
模板:
1
2
3
4
5
6
7
8
9
10
11int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 1; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}- 对一个字符串使用的dp为二维数组。常见题型有之前做过的
最长回文子串
,使用双指针,从两边向中间靠拢,二维分别指的是左侧指针和右侧指针的索引值。可以推广到最长回文子序列。 - 对两个字符串使用的dp为二维数组。例如
编辑距离
和最长公共子序列
- 对一个字符串使用的dp为二维数组。常见题型有之前做过的
回文子序列的求解(LeetCode第516题)
回文子序列的求解,是在回文子串的基础上进行的,dp[i][j] 的长度取决于s[i]和s[j]以及dp[i+1][j-1]
- 当s[i] == s[j]时,dp[i][j] = dp[i+1][j-1] + 2
- 当s[i] != s[j]时, dp[i][j] = Math.max(dp[i][j-1],dp[i+1][j])
当转移方程写好后,最大的难点就是如何遍历dp,建议采用表的形式,从方程中可以看到,我们必须得到左边、下边、左下,三个dp表达式才能得到下一个dp表达式,故可以采用斜着遍历的方式和逆循环从下往上遍历即可。
1 | /** |
1 | /** |
编辑距离(LeetCode第72题)
先总结一哈:从后往前,然后写递归函数,然后画DP Table,写非递归,注意Base case!
文章链接
题目
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
1 | 输入: word1 = "horse", word2 = "ros" |
示例 2:
1 | 输入: word1 = "intention", word2 = "execution" |
思路
个人觉得公众号说的有的啰嗦哈~ 我就稍微总结一下我的想法:
- 拿到题目,又是两个字符串之间的问题,毫无疑问第一想法想到使用双指针的dp,因为一般遇到两个字符串的问题都是采用dp二维数组解决(双指针);
- dp解决问题,首先就要搞清楚dp数组代表什么,其次就是状态转移有哪几种情况!
- 一般dp二维数组 dp[i][j] ,代表以s1[i-1]、s2[j-1]结尾的转换的最少操作数,为啥是i-1,j-1!因为dp是从dp[1][1]开始的,代表字符串的第一个字符!!
- 搞明白了dp数组是啥含义,现在就要搞清楚有哪几种状态转移,从公众号中可以看到,dp[i][j]想要往前移动,则可以进行删除、修改、插入操作,如果双指针指向的字符相同,则无需进行任何操作,直接向前移动就行,也就是此时 dp[i][j] = dp[i-1][j-1] ;
- 剩下的插入、删除、修改如何做呢…取三者最小值就好了嘛!!!
- 其实这个画个DP表就非常的清晰了,画DP表也能提醒我们注意Base Case,因为没有Base Case ,我们是肯定写不出来DP表中的值的;
- 有个注意的点,就是备忘录做法是从0开始的,所以当i或者j = -1时,此时的i、j代表的是数组的下标,是从0开始的,比如说此时j = 5的,说明还有6个字符需要删除,所以要记得+1 ,而dp是从dp[1][1]开始的(比如两个字符串’’abc””acd”,dp[1][1]指的是都指向a)!!!!这个很容易错!
三种做法
- 常规暴力递归(超时)
1 | class Solution { |
- 带备忘录的递归(这个貌似比dp更优秀)
1 | class Solution { |
- dp
1 | class Solution { |
最长公共子序列问题(LeetCode第1143题)
典型的dp二维数组解决,太简单了…
- 第一个要注意的就是该dp可以优化,当双指针指向的字符不等时,dp[i-1][j-1]可以不用考虑!!!!因为max(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])是一定轮不到最后这个的;
- 第二个要注意的点就是记得Base Case,记得要画DP表!
- 同样要注意这里的第一个字符串对应的就是dp[1][1]!!!!!
代码
1 | class Solution { |
滑动窗口算法(示例:LeetCode第1143:最小覆盖子串问题)
- Tip:最重要的是过最后一个用例的艰辛,因为需要考虑到Integer是一个对象,超过-127~128之后会new一个新的对象,导致就算是值相等,其也会显示不相等!!!!!!!
文章链接
代码
文章中使用C++实现的,我是在LeetCode用的Java实现的
1 | class Solution { |
最大子序列和(LeetCode第53题)
先总结一下,可以运用 dp 和 滑动窗口 两种方法!!!
题目
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
1 | 输入: [-2,1,-3,4,-1,2,1,-5,4], |
思路
dp
- 要的是子串,必然可以用dp,而且是单子串且非回文,则必然使用一维dp数组
- 只要定义好了dp数组,这道题就没有任何难度,dp[i]定义为以i为索引的字符结尾的最大和
- 则dp[i] = Math.max(dp[i-1],nums[i]),然后取dp[i]中最大的即可!
- 这题只有一个小难点,那就是找Base Case,好像并不存在Base Case,但是我们可以创造Base Case,可以令dp[0] = nums[0],这样就有Base Case了,同时注意哈,这里不需要将dp数组的大小变为nums.length+1,因为这个题目不存在Base Case,所以dp只会产生n个数,不需要n+1个存储空间,像上文的dp二维数组,就存在BaseCase,当有一个为0时,有相应的取值!总而言之,一定要特别注意dp的数组大小以及BaseCase。
滑动窗口
比较简单的思路。就是sum如果大于0了,就移动右指针就行,如果是小于0了,就比较sum和num[i],取最大值(相当于移动左指针),直到循环结束。
代码
- dp
1 | class Solution { |
- 滑动窗口
1 | class Solution { |
打家劫舍 I、II、III (LeetCode第198、213、337)
文章链接
I 的代码
- 自己写的(感觉写的有点啰嗦~)
1 | class Solution { |
- 高分题解
1 | class Solution { |
II 的代码
分两次dp就好了,一次[0,nums.length-2],一次[1,nums.length-1],取最大值就好~
1 | class Solution { |
III 的代码
这是一个树形的dp问题,解决方法是采用一个一维数组,之前的I II只用到了dp[nums.length-1],这里用到了两个,而且这里的dp竟然用了递归哟…
Tip:特殊做法记忆记忆记忆!!!
1 | /** |