动态规划
一、斐波那契数列
leetcode-70-爬楼梯
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
题解
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
class Solution
{
public int climbStairs(int n)
{
if(n == 1)
return 1;
if(n == 2)
return 2;
int[] dp = new int[n];
dp[0] = 1;
dp[1] = 2;
for(int i=2;i<n;i++)
{
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n-1];
}
}
leetcode-5457. 和为奇数的子数组数目
给你一个整数数组 arr 。请你返回和为 奇数 的子数组数目。
由于答案可能会很大,请你将结果对 10^9 + 7 取余后返回。
示例 1:
输入:arr = [1,3,5]
输出:4
解释:所有的子数组为 [[1],[1,3],[1,3,5],[3],[3,5],[5]] 。
所有子数组的和为 [1,4,9,3,8,5].
奇数和包括 [1,9,3,5] ,所以答案为 4 。
示例 2 :
输入:arr = [2,4,6]
输出:0
解释:所有子数组为 [[2],[2,4],[2,4,6],[4],[4,6],[6]] 。
所有子数组和为 [2,6,12,4,10,6] 。
所有子数组和都是偶数,所以答案为 0 。
示例 3:
输入:arr = [1,2,3,4,5,6,7]
输出:16
示例 4:
输入:arr = [100,100,99,99]
输出:4
示例 5:
输入:arr = [7]
输出:1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-sub-arrays-with-odd-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
dp使用二维数组的时候,时间超时,可以记录前缀和中和为奇数的数目和和为偶数的数目,sum[i]是偶数,因为偶数-奇数=奇数,所以res+前缀和为奇数的个数,同理如果sum[i]为奇数,那么先res+1,又因为奇数-偶数=奇数,所以res要加上前缀和为偶数的个数。
class Solution
{
public int numOfSubarrays(int[] arr)
{
int[] sum = new int[arr.length];
sum[0] = arr[0];
int jishu = 0;
int oushu=0;
int res = 0;
if(sum[0]%2 == 1)
{
jishu++;
res++;
}
else
{
oushu++;
}
for(int i = 1;i<arr.length;i++)
{
sum[i] = sum[i-1]+arr[i];
if(sum[i]%2 == 0)
{
oushu++;
res = res+jishu;
}
else
{
jishu++;
res = res+1;
res = res+oushu;
}
res = res%(int)(Math.pow(10,9)+7);
}
return res%(int)(Math.pow(10,9)+7);
}
}
leetcode-198-打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以
class Solution
{
public int rob(int[] nums)
{
int[] dp = new int[nums.length];
if(nums.length == 0)
return 0;
if(nums.length==1)
return nums[0];
if(nums.length==2)
return Math.max(nums[0],nums[1]);
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i=2;i<nums.length;i++)
{
dp[i] = Math.max(nums[i]+dp[i-2],dp[i-1]);
}
return dp[nums.length-1];
}
}
leetcode-213-打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
这道题目和第一道描述基本一样,强盗依然不能抢劫相邻的房子,输入依然是一个数组,但是告诉你这些房子不是一排,而是围成了一个圈。
也就是说,现在第一间房子和最后一间房子也相当于是相邻的,不能同时抢。比如说输入数组 nums=[2,3,2],算法返回的结果应该是 3 而不是 4,因为开头和结尾不能同时被抢。
这个约束条件看起来应该不难解决,我们前文「单调栈解决 Next Greater Number」说过一种解决环形数组的方案,那么在这个问题上怎么处理呢?
首先,首尾房间不能同时被抢,那么只可能有三种不同情况:要么都不被抢;要么第一间房子被抢最后一间不抢;要么最后一间房子被抢第一间不抢。
作者:labuladong
链接:https://leetcode-cn.com/problems/house-robber-ii/solution/tong-yong-si-lu-tuan-mie-da-jia-jie-she-wen-ti-by-/
那就简单了啊,这三种情况,那种的结果最大,就是最终答案呗!不过,其实我们不需要比较三种情况,只要比较情况二和情况三就行了,因为这两种情况对于房子的选择余地比情况一大呀,房子里的钱数都是非负数,所以选择余地大,最优决策结果肯定不会小。
class Solution
{
public int rob(int[] nums)
{
if(nums.length == 0)
return 0;
if(nums.length == 1)
return nums[0];
return Math.max(rob(nums,1,nums.length-1),rob(nums,0,nums.length-2));
}
//计算从left到right所能获得的最大结果
public int rob(int[] nums,int left,int right)
{
int left_1 = 0; //相当于存dp[i-1]的值
int left_2 = 0; //相当于存dp[i-2]的值
int res = 0;
for(int i=left;i<=right;i++)
{
res = Math.max(left_1,nums[i]+left_2);
left_2 = left_1;
left_1 = res;
}
return res;
}
}
二、矩阵路径
leetcode-64-最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
题解
到右下角只能从右下角的上一个或者右下角的左边移动得到,dp[i][j]状态表示从(0,0)到(i,j)能得到的最小路径和
class Solution
{
public int minPathSum(int[][] grid)
{
int rows = grid.length;
int cols = grid[0].length;
int[][] dp = new int[rows][cols];
int result = 0;
for(int i=0;i<rows;i++)
{
dp[i][0] = grid[i][0]+result;
result = dp[i][0] ;
}
result = 0;
for(int j=0;j<cols;j++)
{
dp[0][j] = grid[0][j] + result;
result = matrix[0][j];
}
for(int i=1;i<rows;i++)
{
for(int j=1;j<cols;j++)
{
dp[i][j] = Math.min(matrix[i-1][j]+grid[i][j],matrix[i][j-1]+grid[i][j]);
}
}
return dp[rows-1][cols-1];
}
}
leetcode-62-不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
题解
到达(i,j)位置可以从(i-1,j)或者(i,j-1)移动一步得到,所以只需计算出到达(i-1,j)和(i,j-1)位置的方式并求和即可。
class Solution {
public int uniquePaths(int m, int n)
{
//状态dp[i][j]表示到(i,j)共有多少种可能。
int[][] dp = new int[m][n];
for(int i=0;i<n;i++)
{
dp[0][i] = 1;
}
for(int i=0;i<m;i++)
{
dp[i][0] = 1;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
//到达(i,j)的方式等于到达(i-1,j)和(i,j-1)位置的方式求和
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
三、数组区间
leetcode-303-区域和检索 - 数组不可变
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
题解
private int[] sum;
public NumArray(int[] nums)
{
sum = new int[nums.length + 1];
for (int i = 0; i < nums.length; i++)
{
sum[i + 1] = sum[i] + nums[i];
}
}
public int sumRange(int i, int j) {
return sum[j + 1] - sum[i];
}
leetcode-413-等差数列划分
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,以下数列为等差数列:
1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9
以下数列不是等差数列。
1, 1, 2, 5, 7
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。
如果满足以下条件,则称子数组(P, Q)为等差数组:
元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。
函数要返回数组 A 中所有为等差数组的子数组个数。
示例:
A = [1, 2, 3, 4]
返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。
题解
动态规划
算法
在上一个方法中,我们开始是从最大区间 (0,n−1)开始的,其中 n为数组 A 中元素的个数。我们可以观察到区间 (0,i) 中等差数列的个数只和这个区间中的元素有关。因此,这个问题可以用动态规划来解决。
首先创建一个大小为 n 的一维数组 dp。dp[i] 用来存储在区间 (k,i), 而不在区间 (k,j)中等差数列的个数,其中 j<i。
与递归方法中后向推导不同,我们前向推导 dp中的值。其余的思路跟上一个方法几乎一样。对于第 i个元素,判断这个元素跟前一个元素的差值是否和等差数列中的差值相等。如果相等,那么新区间中等差数列的个数即为 1+dp[i−1]。sum同时也要加上这个值来更新全局的等差数列总数。
下面的动画描述了 dp 的推导过程。
public class Solution
{
public int numberOfArithmeticSlices(int[] A)
{
int[] dp = new int[A.length];
int sum = 0;
for (int i = 2; i < dp.length; i++)
{
if (A[i] - A[i - 1] == A[i - 1] - A[i - 2])
{
dp[i] = 1 + dp[i - 1];
sum += dp[i];
}
}
return sum;
}
}
公式计算
通过 dp 方法,我们观察到对于 k 个连续且满足等差条件的元素,每次 sum 值分别增加 1,2,3,…,k。因此,与其每次更新 sum 值,只需要用变量 count 来记录有多少个满足等差条件的连续元素,之后直接把 sum 增加 count∗(count+1)/2就可以了。
public class Solution
{
public int numberOfArithmeticSlices(int[] A)
{
int count = 0;
int sum = 0;
for (int i = 2; i < A.length; i++)
{
if (A[i] - A[i - 1] == A[i - 1] - A[i - 2])
{
count++;
}
else
{
sum += (count + 1) * (count) / 2;
count = 0;
}
}
return sum += count * (count + 1) / 2;
}
}
四、分割整数
leetcode-343-整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
题解
n-i分解的时候,如果出现n-i比F(n-i)大,那么显然就不需要再进行分解,所以我们还需要比较i*(n-i)和i*F(n-i)的大小关系
// 暴力解法
public int integerBreak(int n) {
if (n == 2) {
return 1;
}
int res = -1;
for (int i = 1; i <= n - 1; i++) {
res = Math.max(res, Math.max(i * (n - i), i * integerBreak1(n - i)));
}
return res;
}
作者:97wgl
链接:https://leetcode-cn.com/problems/integer-break/solution/bao-li-sou-suo-ji-yi-hua-sou-suo-dong-tai-gui-hua-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 记忆化搜索-自顶向下
int[] memory;
public int integerBreak(int n) {
memory = new int[n + 1];
return integerBreakHelper(n);
}
public int integerBreakHelper(int n) {
if (n == 2) {
return 1;
}
// memory的初始值为0,如果它不为0,说明已经计算过了,直接返回即可
if (memory[n] != 0) {
return memory[n];
}
int res = -1;
for (int i = 1; i <= n - 1; i++) {
res = Math.max(res, Math.max(i * integerBreakHelper(n - i), i * (n - i)));
}
// 将每次计算的结果保存到备忘录数组中
memory[n] = res;
return res;
}
作者:97wgl
链接:https://leetcode-cn.com/problems/integer-break/solution/bao-li-sou-suo-ji-yi-hua-sou-suo-dong-tai-gui-hua-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 动态规划
class Solution {
public int integerBreak(int n) {
if(n==1)
return 1;
if(n==2)
return 1;
if(n==3)
return 2;
//dp[i]表示分割数字i可以得到的最大乘积
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 1;
dp[3] = 2;
for(int i=2;i<=n;i++)
{
for(int j=1;j<i;j++)
{
//要比较j*(i-j)和j*dp[i-j]
//比如6,如果使用j*(i-j)得到的最大的是3*3,如果使用j*dp[i-j]得到的最大结果为8
dp[i] = Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
}
}
return dp[n];
}
}
leetcode-279-完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/perfect-squares
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
相当于暴力求解
//这个方法会超出时间限制
class Solution {
int res = Integer.MAX_VALUE;
public int getNumSqures(int n,int count)
{
for(int i=1;i<=n;i++)
{
if(n-(int)Math.pow(i,2)<0)
{
break;
}
if(n-(int)Math.pow(i,2)==0)
{
res = Math.min(res,count+1);
return count+1;
}
int ss = getNumSqures(n-(int)Math.pow(i,2),count+1);
res = Math.min(res,ss);
}
return res;
}
public int numSquares(int n)
{
getNumSqures(n,0);
return res;
}
}
动态规划
class Solution {
public static int numSquares(int n)
{
if(n==1)
return 1;
if(n==2)
return 2;
if(n==3)
return 3;
//状态dp[i]表示从组成i的最小的完全平方的个数
int[] dp = new int[n+1];
dp[1] = 1;
for(int i=2;i<=n;i++)
{
dp[i] = i;
for(int j=1;j<=i;j++)
{
if(i-j*j>=0)
{
//从上面的递归的结果中可以看出,这个题目的答案是所有分支的结果的最小值加1
dp[i] = Math.min(dp[i],1+dp[i-j*j]);
}
else
break;
}
}
return dp[n];
}
}
leetcode - 91-解码方法
一条包含字母 A-Z 的消息通过以下方式进行了编码:
‘A’ -> 1
‘B’ -> 2
…
‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
示例 1:
输入: “12”
输出: 2
解释: 它可以解码为 “AB”(1 2)或者 “L”(12)。
示例 2:
输入: “226”
输出: 3
解释: 它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/decode-ways
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
class Solution {
public int numDecodings(String s)
{
if(s.length() == 0)
return 0;
if(s.charAt(0) == '0')
return 0;
int[] dp = new int[s.length()];
dp[0] = 1;
for(int i=1;i<s.length();i++)
{
//无法解析的情况
if (s.charAt(i) == '0' && (s.charAt(i - 1) > '2' || s.charAt(i - 1) == '0'))
{
return 0;
}
//当前后2位是10或20
else if (s.charAt(i) == '0' && (s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2'))
{
if (i > 1)
{
dp[i] = dp[i - 2];
}
else
{
dp[i] = 1;
}
}
//当前后2位是21-26或11-19
else if ((s.charAt(i - 1) == '2' && s.charAt(i) > '0' && s.charAt(i) <= '6') || (s.charAt(i - 1) == '1'))
{
if (i > 1)
{
//最后一个单独算一个字符,最后两个算一个字符,两种情况求和
dp[i] = dp[i - 1] + dp[i - 2];
}
else
{
dp[i] = dp[i - 1] + 1;
}
}
else
{
dp[i] = dp[i - 1];
}
}
return dp[dp.length - 1];
}
}
五、最长递增子序列
leetcode-300-最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
状态转移方程:
class Solution {
public int lengthOfLIS(int[] nums)
{
if(nums.length == 0)
return 0;
int[] dp = new int[nums.length];
dp[0] = 1;
int res=1; //保存最终的结果
for(int i = 1;i<nums.length;i++)
{
int max = 1;
for(int j=0;j<i;j++)
{
if(nums[i]>nums[j])
max = Math.max(max,dp[j]+1);
if(nums[i] == nums[j])
max = Math.max(max,dp[j]);
}
dp[i] = max;
if(max>res)
res = max;
}
return res;
}
}
leetcode-646-最长数对链
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例 :
输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-length-of-pair-chain
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
根据数对的第一个数排序所有的数对,dp[i] 存储以 pairs[i] 结尾的最长链的长度。当 i < j 且 pairs[i][1] < pairs[j][0] 时,扩展数对链,更新 dp[j] = max(dp[j], dp[i] + 1)。
根据数对的第一个数排序所有的数对,dp[i] 存储以 pairs[i] 结尾的最长链的长度。当 i < j 且 pairs[i][1] < pairs[j][0] 时,扩展数对链,更新 dp[j] = max(dp[j], dp[i] + 1)。
class Solution
{
public void sort(int[][] pairs)
{
for(int i = pairs.length - 1;i>=0;i--)
{
int flag = 0;
for(int j = 0;j<i;j++)
{
if(pairs[j][0]> pairs[j+1][0])
{
flag = 1;
int[] temp = pairs[j];
pairs[j] = pairs[j+1];
pairs[j+1] = temp;
}
}
if(flag == 0)
return;
}
}
public int findLongestChain(int[][] pairs)
{
//先对二维数组进行排序,可以使用自己写的冒泡或者使用下面的函数进行排序
//sort(pairs);
Arrays.sort(pairs, (a, b) -> a[0] - b[0]);
int rows = pairs.length;
int cols = pairs[0].length;
int[] dp = new int[pairs.length];
Arrays.fill(dp, 1);
if(pairs.length == 0)
return 0;
int res = 1;
for(int i = 1;i<rows;i++)
{
int[] temp = pairs[i];
for(int j = 0;j<i;j++)
{
//用之前的最后一个数字,和这一次的第一个数字进行比较
if(pairs[j][1] < temp[0])
{
dp[i] = Math.max(dp[i],dp[j]+1);
if(dp[i]>res)
res = dp[i];
}
}
}
return res;
}
}
leetcode-376-摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。
示例 2:
输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
示例 3:
输入: [1,2,3,4,5,6,7,8,9]
输出: 2
官方题解
为了更好地理解这一方法,用两个数组来 dp ,分别记作 up和 down 。
每当我们选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小。
up[i]存的是目前为止最长的以第 i 个元素结尾的上升摆动序列的长度。
类似的, down[i] 记录的是目前为止最长的以第 i 个元素结尾的下降摆动序列的长度。
我们每当找到将第 i 个元素作为上升摆动序列的尾部的时候就更新 up[i] 。现在我们考虑如何更新 up[i],我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j],满足 j < i且 nums[i] >nums[j]。类似的, down[i] 也会被更新。
下面使用两个dp,因为最后一个数字,可能是向下摆的,也可能是向上摆的。
public class Solution
{
public int wiggleMaxLength(int[] nums)
{
if (nums.length < 2)
return nums.length;
//up[i]存的是目前为止最长的以第 i 个元素结尾的上升摆动序列的长度
int[] up = new int[nums.length];
//down[i] 记录的是目前为止最长的以第 i 个元素结尾的下降摆动序列的长度。
int[] down = new int[nums.length];
for (int i = 1; i < nums.length; i++)
{
for(int j = 0; j < i; j++)
{
//如果以i为结尾的值大于以j为结尾的元素的值,说明是上摆的,
if (nums[i] > nums[j])
{
//和以j为结尾的下摆的摆动序列个数+1进行比较
up[i] = Math.max(up[i],down[j] + 1);
}
else if (nums[i] < nums[j])
{
down[i] = Math.max(down[i],up[j] + 1);
}
}
}
return 1 + Math.max(down[nums.length - 1], up[nums.length - 1]);
}
}
自己题解
class Solution {
public int wiggleMaxLength(int[] nums) {
int len = nums.length;
//dp[i]表示到i位置的时候,摆动序列的长度
int[] dp = new int[len];
Arrays.fill(dp,1);
if(nums.length <= 1)
return nums.length;
//res[i]如果为1,表示nums[i]-nums[i-1]>0,如果为-1,表示nums[i]-nums[i-1]<0
int[] res = new int[len];
//temp之前res[loc] = -1;这里存放的就是最大值即只需要比最大的小就好了
//如果res[loc] = 1;这里存放的就是最小值即只需要比最大的大就好了
int temp = nums[0];
for(int i = 1;i<len;i++)
{
//如果和上一个数字相等
if(nums[i] - nums[i-1] == 0)
{
res[i] = res[i-1];
dp[i] = dp[i-1];
continue;
}
if(i == 1)
{
dp[1] = 2;
temp = nums[1];
if(nums[1] - nums[0] > 0)
{
res[1] = 1;
}
else
{
res[1] = -1;
}
continue;
}
else
{
if(dp[i-1] == 1)
{
temp = nums[i];
if(nums[i] - nums[i-1] > 0)
{
res[i] = 1;
}
else
{
res[i] = -1;
}
dp[i] = 2;
continue;
}
if(res[i-1] == -1 && nums[i] - temp > 0 )
{
temp = nums[i];
res[i] = 1;
dp[i] = dp[i-1]+1;
}
else if(res[i-1] == 1 && nums[i] - temp < 0 )
{
temp = nums[i];
res[i] = -1;
dp[i] = dp[i-1]+1;
}
else
{
if(res[i-1] == 1)
{
temp = Math.max(nums[i],temp);
}
else
temp = Math.min(nums[i],temp);
res[i] = res[i-1];
dp[i] = dp[i-1];
}
}
}
return dp[len-1];
}
}
六、最长公共子序列
leetcode-1143-最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace”,它的长度为 3。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc”,它的长度为 3。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
class Solution {
public int longestCommonSubsequence(String text1, String text2)
{
int len1 = text1.length();
int len2 = text2.length();
if(len1 == 0 || len2 == 0)
return 0;
//状态dp[i][j]描述的是字符串1前i个元素和字符串2的前j个元素中最长公共子序列长度。
int[][] dp = new int[len1][len2];
for(int i=0; i<len1;i++)
{
for(int j = 0;j<len2;j++)
{
//如果位置都为0的话,如果字符串1和字符串2的第0个位置的元素相等,那么dp[i][j]设置为1,否则设置为0
if(i==0 && j==0 )
{
if(text1.charAt(i) == text2.charAt(j))
dp[i][j] = 1;
continue;
}
if(i == 0 && j!=0)
{
if(dp[i][j-1]==1 || text1.charAt(i) == text2.charAt(j))
{
dp[i][j] = 1;
}
continue;
}
if(j == 0 && i!=0)
{
if(dp[i-1][j]==1 || text1.charAt(i) == text2.charAt(j))
{
dp[i][j] = 1;
}
continue;
}
//如果字符串1的第i个元素和字符串2的第j个元素相等的话,那么就可以通过dp[i-1][j-1]+1递推而来
//不相等的话,去上面和左边的最大值
if(text1.charAt(i) == text2.charAt(j))
{
dp[i][j] = dp[i-1][j-1]+1;
}
else
{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[len1-1][len2-1];
}
}
七、0-1背包
经典的0-1背包问题
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论(下面是选或者不选两种情况):
- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
// W 为背包总体积
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values)
{
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++)
{
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; j++)
{
if (j >= w)
{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
}
else
{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
public int knapsack(int W, int N, int[] weights, int[] values)
{
int[] dp = new int[W + 1];
for (int i = 1; i <= N; i++)
{
int w = weights[i - 1], v = values[i - 1];
for (int j = W; j >= 1; j--)
{
if (j >= w)
{
dp[j] = Math.max(dp[j], dp[j - w] + v);
}
}
}
return dp[W];
}
leetcode-416-分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
dfs题解(超时)
class Solution
{
public boolean dfs(int[] nums,int step,int sum,int newSum)
{
if(step>=nums.length)
return false;
if(sum==newSum)
return true;
if(newSum> sum)
return false;
if(dfs(nums,step+1,sum,newSum) == true)
return true;
if(dfs(nums,step+1,sum,newSum+nums[step])==true)
return true;
return false;
//return || dfs(nums,step+1,sum,newSum+nums[step]);
}
public boolean canPartition(int[] nums)
{
int sum = 0;
for(int i=0;i<nums.length;i++)
{
sum = sum + nums[i];
}
if(sum%2 == 1)
return false;
sum = sum/2;
return dfs(nums,0,sum,0);
}
}
动态规划
事实上,这是一个典型的“动态规划”问题,并且它的“原形”是“0-1 背包问题。使用“动态规划”解决问题的思路是“以空间换时间”,“规划”这个词在英文中就是“填表格”的意思,代码执行的过程,也可以称之为“填表格”。
做这道题需要做这样一个等价转换:是否可以从这个数组中挑选出一些正整数,使得这些数的和等于整个数组元素的和的一半。前提条件是:数组的和一定得是偶数,即数组的和一定得被 2 整除,这一点是特判。
本题与 0-1 背包问题有一个很大的不同,即:
- 1、0-1 背包问题选取的物品的容积总量不能超过规定的总量;
- 2、本题选取的数字之和需要恰恰好等于规定的和的一半。
这一点区别,决定了在初始化的时候,所有的值应该初始化为 false。 (《背包九讲》的作者在介绍 0-1 背包问题的时候,有强调过这点区别,我在这里也只是再重复一下。)
作为“0-1 背包问题”,它的特点是:“每个数只能用一次”。思路是:物品一个一个选,容量也一点一点放大考虑(这一点是“动态规划”的思想,特别重要)。
如果在实际生活中,其实我们也是这样做的,一个一个尝试把候选物品放入“背包”,看什么时候能容纳的价值最大。
具体做法是:画一个 len 行,target + 1 列的表格。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始,很多时候,我们需要考虑这个容量为 0 的数值。
状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j,如果可以dp[i][j]为true,否则为false。
状态转移方程:很多时候,状态转移方程思考的角度是“分类讨论”,对于“0-1 背包问题”而言就是“当前考虑到的数字选与不选”。
-
1、不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
-
2、选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。
状态转移方程是:
- dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
一般写出状态转移方程以后,就需要考虑边界条件(一般而言也是初始化条件)。
- 1、j - nums[i] 作为数组的下标,一定得保证大于等于 0 ,因此 nums[i] <= j;
- 2、注意到一种非常特殊的情况:j 恰好等于 nums[i],即单独 nums[j] 这个数恰好等于此时“背包的容积” j,这也是符合题意的。
因此完整的状态转移方程是:
说明:虽然写成花括号,但是它们的关系是或者。
- 初始化:dp[0][0] = false,因为是正整数,当然凑不出和为 0。
- 输出:dp[len - 1][target],这里 len 表示数组的长度,target 是数组的元素之和(必须是偶数)的一半。
public class Solution
{
public boolean canPartition(int[] nums)
{
int len = nums.length;
if (len == 0)
{
return false;
}
int sum = 0;
for (int num : nums)
{
sum += num;
}
// 特判:如果是奇数,就不符合要求
if ((sum & 1) == 1)
{
return false;
}
int target = sum / 2;
// 创建二维状态数组,行:物品索引,列:容量(包括 0)
boolean[][] dp = new boolean[len][target + 1];
// 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
if (nums[0] <= target)
{
dp[0][nums[0]] = true;
}
// 再填表格后面几行
for (int i = 1; i < len; i++)
{
for (int j = 0; j <= target; j++)
{
// 直接从上一行先把结果抄下来,然后再修正
dp[i][j] = dp[i - 1][j];
if (nums[i] == j)
{
dp[i][j] = true;
continue;
}
if (nums[i] < j)
{
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[len - 1][target];
}
}
自己按照答案写的
res[i][j]表示前i个数中,是否一部分数字之和等于j
如果通过树结构(dfs)做,每个节点会产生两个分支,会产生很多的分支,动态规划是如何保存这些分支的,动态规划通过数组保存这些分支,这些分支的数据其实都被保存了,比如res[i-1][j]、res[i][j-nums[i-1]]这两个其实表示的是树中一个节点的两个分支,其实在数组中都有保存。
class Solution
{
public boolean canPartition(int[] nums)
{
int rows = nums.length;
int sum = 0;
for(int i=0;i<nums.length;i++)
{
sum = sum + nums[i];
}
if(sum %2 == 1)
return false;
int target = sum/2;
//res[i][j]表示前i个数中,是否一部分数字之和等于j
boolean[][] res = new boolean[rows+1][target+1];
//res[i][j] = res[i-1][j] || res[i-1][j-nums[i]]
for(int i = 1;i<=rows;i++)
{
for(int j=1;j<=target;j++)
{
if(j==0)
res[i-1][j] = true;
if(j-nums[i-1] == 0)
res[i-1][j-nums[i-1]] = true;
if(j>=nums[i-1])
res[i][j] = res[i-1][j] || res[i-1][j-nums[i-1]];
}
}
return res[rows][target];
}
}
leetcode-494. 目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
提示:
数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/target-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
使用dfs(过了,没超时)
class Solution
{
int count = 0;
public void dfs(int[] nums,int S,int step,int sum)
{
if(sum == S && step == nums.length)
{
count++;
return;
}
if(step >= nums.length)
return;
dfs(nums,S,step+1,sum+nums[step]);
dfs(nums,S,step+1,sum-nums[step]);
}
public int findTargetSumWays(int[] nums, int S)
{
dfs(nums,S,0,0);
return count;
}
}
动态规划解法
import java.util.*;
class Solution
{
public static int findTargetSumWays(int[] nums, int S)
{
int sum = 0;
for(int i = 0;i < nums.length;i++)
{
sum = sum + nums[i];
}
if(Math.abs(S)>Math.abs(sum))
return 0;
int len = 2*sum+1;
//dp[i][j]表示前i个数,通过各个数之间的加减得到的结果j-sum的个数
int[][] dp = new int[nums.length][len];
if (nums[0] == 0) {
dp[0][sum] = 2;
} else {
dp[0][sum + nums[0]] = 1;
dp[0][sum - nums[0]] = 1;
}
for(int i = 1;i<nums.length;i++)
{
for(int j = -sum;j<=sum;j++)
{
if (dp[i - 1][sum + j] > 0){
//dp数组下面一行是根据数组上面一行推导来的
dp[i][sum+j+nums[i]] += dp[i-1][j+sum];
dp[i][sum+j-nums[i]] += dp[i-1][j+sum];
}
}
}
if(S+sum >= len)
{
System.out.println(111);
return 0;
}
else
return dp[nums.length-1][S+sum];
}
}