leetcode 里面的动态规划 就那几类 背包问题 LIS 加上股票和打家劫舍
在算法基础课里面和算法提高课里面都学过相关内容
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
class Solution {
public int fib(int n) {
int a = 0 , b = 1 ;
while(n-- > 0){
int c = a + b ;
a = b ;
b = c ;
}
return a ;
}
}
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length ;
int[] f = new int [n+10];
f[0] = cost[0];
f[1] = cost[1] ;
for(int i = 2 ; i < n ; i ++){
f[i] = Math.min(f[i-1],f[i-2]) + cost[i] ;
}
return Math.min(f[n-1] , f[n-2]);
}
}
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
class Solution {
public int integerBreak(int n) {
if(n <= 3) return 1 *(n-1);
int p = 1 ;
while(n >= 5){
n -= 3 ;
p *= 3 ;
}
//最后答案是2 3 4
于是就是剩啥乘啥
return p * n;
}
}
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
f[i] 表示是否能凑出体积为j的 方案
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0 ;
for(int x : nums) sum += x ;
if(sum % 2 != 0) return false;
sum /= 2 ;
boolean[] f = new boolean[sum + 1];
f[0] = true ;
for(int x : nums){
for(int i = sum ; i >= x ; i--){
f[i] |= f[i-x] ;
//这里面是和 f[i] = f[i] || f[i-x]意思一样的
}
}
return f[sum];
}
}
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背dp[j]这么重的石头。
最后dp[target]里是容量为target的背包所能背的最大重量。
那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。
class Solution {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
int sum =0 ;
for(int a : stones)sum += a ;
int m = sum / 2 ;
int[] f = new int [m + 1];
for(int a : stones){
for(int i = m ; i >= a ; i-- ){
f[i] = Math.max(f[i] , f [i-a] + a);
}
}
return sum - f[m] * 2;
}
}
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3
class Solution {
public int findTargetSumWays(int[] nums, int S) {
if(S < -1000 || S > 1000) return 0;
int n = nums.length, Offset = 1000;//偏移1000位
int[][] f = new int[n + 1][2010];
f[0][Offset] = 1;
for(int i = 1;i <= n;i ++)
for(int j = -1000;j <= 1000;j ++)
{
//j - nums[i - 1] >= -1000 说明是nums[i-1]是负的
if(j - nums[i - 1] >= -1000) f[i][j + Offset] = f[i - 1][j - nums[i - 1] + Offset];
//j - nums[i - 1] <= 1000 说明是nums[i-1]是正的
if(j + nums[i - 1] <= 1000) f[i][j + Offset] += f[i - 1][j + nums[i - 1] + Offset];
}
return f[n][S + Offset];
}
}
class Solution {
public int findTargetSumWays(int[] nums, int S) {
int n = nums.length;
int sum = 0;
for(int i = 0;i < n;i ++) sum += nums[i];
if((S + sum) % 2 == 1) return 0;//S + sum是奇数则无解
if(S > sum) return 0;
int x = (S + sum) / 2;
//可能和小与0
if(x < 0) x = -x ;
int[] f = new int[x+10];
f[0] = 1;
for(int a : nums)
for(int j = x;j >= a;j --)
f[j] += f[j - a];
return f[x];
}
}
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
属于二维费用的背包 中的01背包问题 价值的话就是1
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] f = new int[m+1][n+1];
for(String x : strs){
int a = 0 , b = 0 ;
for(char y : x.toCharArray()){
if(y == '1') b++;
else a++;
}
for(int i = m ; i >= a ; i--){
for(int j = n ; j >= b ; j --){
f[i][j] = Math.max(f[i][j] , f[i-a][j-b]+1);
}
}
}
return f[m][n];
}
}
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int [] f = new int[amount+10];
f[0] = 1;
for(int i = 0 ; i < n ; i++){
for(int j = coins[i] ; j <= amount ; j ++){
f[j] += f[j-coins[i]] ;
}
}
return f[amount];
}
}
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
详细题解
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
class Solution {
public int combinationSum4(int[] nums, int target) {
int n = nums.length;
int[] f= new int[target+10];
f[0] = 1 ;
for(int i = 1 ; i <= target ; i++){//遍历背包
for(int a : nums)//遍历物品
if(i >= a) f[i] += f[i-a];
}
return f[target];
}
}
改为:一步一个台阶,两个台阶,三个台阶,…,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
1阶,2阶,… m阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
此时大家应该发现这就是一个完全背包问题了!
确定dp数组以及下标的含义
dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
确定递推公式
在动态规划:494.目标和 (opens new window)、 动态规划:518.零钱兑换II (opens new window)、动态规划:377. 组合总和 Ⅳ (opens new window)中我们都讲过了,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
本题呢,dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]
那么递推公式为:dp[i] += dp[i - j]
dp数组如何初始化
既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。
下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果
确定遍历顺序
这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!
所以需将target放在外循环,将nums放在内循环。
每一步可以走多次,这是完全背包,内循环需要从前向后遍历。
这就和上一题完全一样了
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
int[] weight = {1,2};
dp[0] = 1;
for (int i = 0; i <= n; i++) {
for (int j = 0; j < weight.length; j++) {
if (i >= weight[j]) dp[i] += dp[i - weight[j]];
}
}
return dp[n];
}
}
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
class Solution {
public int coinChange(int[] coins, int m) {
int n = coins.length;
int [] f = new int[m+1];
Arrays.fill(f,0x3f3f3f);
f[0] = 0 ;
for(int v : coins){
for(int j = v ; j <= m ; j++ ){
f[j] = Math.min(f[j] , f[j-v] + 1 ) ;
}
}
if(f[m] == 0x3f3f3f) return -1 ;
return f[m] ;
}
}
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
class Solution {
public int numSquares(int n) {
int[] f = new int[n+1];
Arrays.fill(f,0x3f3f3f);
f[0] = 0 ;
//把n看作背包容量 1、4、9这样的看作体积 价值看作1
//完全背包问题
for(int i = 0 ; i <= n ; i++){
for(int j = 1 ; j*j <= i ; j ++){
f[i] = Math.min(f[i] , f[i- j*j] + 1);
}
}
return f[n] ;
}
}
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
HashSet<String> set = new HashSet<>(wordDict);
int n = s.length();
boolean[] f = new boolean[n+10];
s = " " + s ;
f[0] = true ;
for(int i = 1 ; i <= n ; i++){
for(int j = 1 ; j <= i ; j++){
String t = s.substring(j,i+1);
if(set.contains(t)) f[i] |= f[j-1];
}
}
return f[n];
}
}
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 0 ) return 0 ;
int[] f = new int[n+1] ;
f[1] = nums[0] ;
for(int i = 2 ; i <= n ; i++){
//因为这个nums的下标是错一位 nums[n-1]是最后一位
f[i] = Math.max(f[i-1] , f[i-2] + nums[i-1] );
}
return f[n-1] ;
}
}
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
由于是一个环形,每相邻两个必须选一个时才会出现最优
1、求[1, n - 1]区间能拿到的最大价值是f[n - 1] 不要尾元素
2、求[2, n]区间能拿到的最大价值g[n] 不要首元素
3、求出f[n - 1]和g[n]的最大值即可
class Solution {
public int rob(int[] nums) {
int n = nums.length ;
if(n == 0 ) return 0 ;
if(n == 1 ) return nums[0] ;
// 1- n-1
int [] f = new int[n+1] ;//不要尾元素
f[1] = nums[0] ;
int [] g = new int[n+1];//不要首元素
//2 - n
g[2] = nums[1] ;
for(int i = 2 ; i < n ;i++){
f[i] = Math.max(f[i-1] , f[i-2] + nums[i-1]);
}
for(int i = 3 ; i <= n ; i++){
g[i] = Math.max(g[i-1] , g[i-2] + nums[i-1]);
}
return Math.max(f[n-1],g[n]) ;
}
}
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] f = dfs(root);
return Math.max(f[0] , f[1]);
}
int[] dfs(TreeNode root){
if(root == null) return new int[]{0,0};
int[] x = dfs(root.left);
int[] y = dfs(root.right);
return new int[] {Math.max(x[0],x[1])+Math.max(y[0] , y[1]),x[0]+y[0]+root.val};
}
}
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
交易的最大次数就是n/2次;
&1 等于%2 用滚动数组 详细题解
0代表的是交易完的状态 1 表示没交易完的状态
使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j]
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length ;
int [][] f = new int[n+1][2*k+1];
for(int i = 1 ; i < 2*k ; i += 2){
f[0][i] = -prices[0];//有这个初始化 所以即使从1开始算prices[i]也不用变i-1
}
//奇数是买 偶数是卖
//奇数状态
//买入股票 f[i][1] = f[i-1][0] - p[i]
//f[i][1] = f[i-1][1];没操作
//偶数状态
//卖出 f[i][2] = f[i-1][1] + p[i] ;
//没操作 f[i][2] = f[i-1][2];
for(int i = 1 ; i < n ; i++){//第0天只能买入的 所以直接从1开始算
for(int j = 0 ; j < 2*k - 1 ; j+= 2 ){
f[i][j+1] = Math.max(f[i-1][j+1],f[i-1][j] - prices[i]);
f[i][j+2] = Math.max(f[i-1][j+1]+ prices[i],f[i-1][j+2]);
}
}
return f[n-1][2*k];
}
}
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] f = new int[n+1][3];
f[0][1] = -prices[0] ;
f[0][0] = 0 ;
for(int i = 1; i < n ; i ++){
//这里面的f[i-1][0]指的是在冷冻期一直没操作
//因为卖出2状态下 只有一个来源 就是前一天卖出
f[i][0] = Math.max(f[i-1][2],f[i-1][0]);
f[i][1] = Math.max(f[i-1][0]-prices[i],f[i-1][1]);
f[i][2] = f[i-1][1]+prices[i] ;
}
return Math.max(f[n-1][0],Math.max(f[n-1][1],f[n-1][2]));
}
}
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int[][] f = new int[n+1][2] ;
//把手续费算到 1状态下
f[0][1] = -prices[0]- fee ;
for(int i = 1 ; i < n ; i++){
f[i][0] = Math.max(f[i-1][0],f[i-1][1]+prices[i] );
f[i][1] = Math.max(f[i-1][1],f[i-1][0]-prices[i]-fee);
}
return Math.max(f[n-1][0],f[n-1][1]) ;
}
}
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
最简单的LIS模板题 时间复杂度是n^2的
class Solution {
public int lengthOfLIS(int[] a) {
int n = a.length ;
int[] f = new int[n+10];
int res = 0 ;
for(int i = 0 ; i < a.length ; i++){
f[i] = 1 ;
for(int j = 0 ; j < i ; j++){
if(a[j] < a[i] ) f[i] = Math.max(f[i] , f[j]+1);
}
res = Math.max(res , f[i]);
}
return res;
}
}
n*log(n)的时间复杂度
可以发现DP的内循环的遍历还是比较傻的。
开一个数组 f[i] 表示最长子序列长度为i时的最小的结尾num值,这个数组应该是单调增的,可以用二分找到当前元素可以append的位置。
要找到最大的一个长度 满足<=a[i]最大数的最大长度
先二分出来小与等于a[i] 的最后一个数
这里面的idx目的要和C++的对应上,
// 贪心 + 二分:时间复杂度:O(nlogn), 空间复杂度:O(n)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> q; // 数组 q 不是nums最长递增子序列, 只是长度相同
for (auto x: nums) {
// 操作 1:q中无元素 或 q中有元素且x>q.back()时, 将x直接添加到q的数组尾
if (q.empty() || x > q.back()) q.push_back(x); // 在q尾 添加 x
// 操作2 : q中有元素且x<=q.back()时,用x替换 q中 第一个 >=x 的元素。
else { // q中有元素 且 x <= q.back()
int l = 0, r = q.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
q[l] = x; // 用 x 替换 q 中元素
}
}
// for (auto& x : q) cout << x << ' '; 打印 数组 q
return q.size();
}
};
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int res = 0 ;
int[] f = new int[n+10];
int idx = -1 ;
for(int a :nums){
if(idx == -1 || a > f[idx]) f[++idx] = a ;
else {
if(a <= f[0]) f[0] = a ;
else{
int l = 0 , r = idx;
while(l < r){
int mid = (l+r+1)>>1 ;
if(f[mid] < a ) l = mid ;
else r = mid -1 ;
}
f [r+1] = a ;
}
}
}
return idx + 1;
}
}
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
class Solution {
public int findLengthOfLCIS(int[] nums) {
int res = 0 ;
for(int i = 0 ; i < nums.length ; i++ ){
int j = i + 1 ;
while(j < nums.length && nums[j] > nums[j-1]) j++ ;
res = Math.max(res , j-i ) ;
i = j - 1 ;
}
return res ;
}
}
动态规划会慢很多 还是不适用 双指针最佳
class Solution {
public int findLengthOfLCIS(int[] nums) {
int n = nums.length ;
int [] f = new int[n+10];
int res = 1 ;
for(int i = 0 ; i < n ; i ++){
f[i] = 1 ;
}
for(int i = 0 ; i < n -1 ; i ++){
if(nums[i+1] > nums[i]) f[i+1] = f[i] +1 ;
res = Math.max(res ,f[i+1] );
}
return res ;
}
}
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
如果用DP来做 那么n^2级别 注意的是他这里面的是连续子序列
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
int[][] f = new int[n + 1 ][m + 1];
int res = 0 ;
for(int i = 1 ; i <= n ; i++){
for(int j = 1 ; j <= m ; j++){
if(nums1[i-1] == nums2[j-1])
f[i][j] = Math.max(f[i][j] , f[i-1][j-1] + 1 );
res = Math.max(res , f[i][j]);
}
}
return res ;
}
}
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
由于找的是最大值 所以中间有交叉的两种也没有关系 所以用前三个状态也可以
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n = text1.length();
int m = text2.length();
int[][] f = new int[n+10][m+10];
//text1 = " " + text1 ;
//text2 = " " + text2 ;
for(int i = 1 ; i <= n ; i++ ){
for(int j = 1 ; j <=m ; j++){
f[i][j] = Math.max(f[i-1][j] , f[i][j-1]);
if(text1.charAt(i - 1) == text2.charAt(j -1 ))//状态是1开始 原字符串是0开始
f[i][j] = Math.max(f[i][j] , f[i-1] [j-1]+1);
}
}
return f[n][m] ;
}
}
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length ;
int[][] f= new int[n + 10 ][m + 10 ];
for(int i = 1 ; i <= n ; i++){
for(int j= 1 ; j <= m ; j++){
f[i][j] = Math.max(f[i-1][j] , f[i][j-1]);
if(nums1[i-1] == nums2[j-1])
f[i][j] = Math.max(f[i][j] ,f[i-1][j-1]+1);
}
}
return f[n][m] ;
}
}
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
输入:s = “abc”, t = “ahbgdc”
输出:true
class Solution {
public boolean isSubsequence(String s, String t) {
int n = s.length();
int m = t.length();
int k = 0 ;
for(int i = 0 ; i < m; i ++){
if(k < n && s.charAt(k) == t.charAt(i)) k++;
}
return k == n ;
}
}
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] f = new int[n+10][m+10];//s1[1-i]变成s2[1-j]的所有方案
//初始化f[]
for(int i = 1 ; i <= n ; i ++)f[i][0] = i ;
for(int j = 1 ; j <= m ; j++) f[0][j] = j ;
for(int i = 1 ; i <= n ; i++ ){
for(int j = 1 ; j <=m ; j++){
f[i][j] = Math.min(f[i-1][j] , f[i][j-1]) + 1;
if(word1.charAt(i - 1) == word2.charAt(j -1 ))
f[i][j] = Math.min(f[i][j] , f[i-1] [j-1]);
}
}
return f[n][m] ;
}
}
按照最长公共子序列来做
class Solution {
public int minDistance(String word1, String word2) {
//最长公共子序列
int n = word1.length();
int m = word2.length();
int[][] f = new int[n+10][m+10];
for(int i = 1 ; i <= n ; i++ ){
for(int j = 1 ; j <=m ; j++){
f[i][j] = Math.max(f[i-1][j] , f[i][j-1]);
if(word1.charAt(i - 1) == word2.charAt(j -1 ))//状态是1开始 原字符串是0开始
f[i][j] = Math.max(f[i][j] , f[i-1] [j-1]+1);
}
}
return n + m - f[n][m] * 2;
}
}
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
双指针的话
先枚举中心 在枚举两边长度 n^2级别
class Solution {
public int countSubstrings(String s) {
int n = s.length();
int res = 0 ;
for(int i = 0 ; i < n ; i++){
//奇数
for(int k = i , j = i ; j >=0 && k < n ; j -- , k++){
if(s.charAt(j) != s.charAt(k)) break ;
res++;
}
//偶数
for(int k = i+1 , j = i ; j >=0 && k < n ; j -- , k++){
if(s.charAt(j) != s.charAt(k)) break ;
res++;
}
}
return res;
}
}
动态规划的话
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
情况二:下标i 与 j相差为1,例如aa,也是回文子串
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。
所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。
class Solution {
public int countSubstrings(String s) {
int n = s.length();
int res = 0 ;
boolean[][] f = new boolean[n+1][n+1];
for(int i = n-1 ; i >= 0 ; i--){
for(int j = i ; j < n ; j++){
if(s.charAt(i) == s.charAt(j)){
if(j - i <= 1){
f[i][j] =true;
res++;
}else if(f[i+1][j-1]){
res++;
f[i][j] = true;
}
}
}
}
return res;
}
}
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
s = " " + s;
int[][] f = new int[n + 10][n + 10];
for(int len = 1;len <= n;len ++)
{
for(int i = 1;i + len - 1 <= n;i ++)
{
int j = i + len - 1;
if(len == 1) f[i][j] = 1;
else
{
f[i][j] = Math.max(f[i + 1][j], f[i][j - 1]);
if(s.charAt(i) == s.charAt(j)) f[i][j] = Math.max(f[i][j], f[i + 1][j - 1] + 2);
}
}
}
return f[1][n];
}
}
代码随想录的做法
这里的初始化是因为f数组不包括i和j 相等的时候
也可以这样初始化
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] f = new int[n+10][n+10];
for(int i = n-1 ; i >= 0 ;i--){
f[i][i] = 1 ;//可能这个是为了只有一个长度的时候
for(int j = i+1 ; j < n ; j++){
//放到这里就是错的
if(s.charAt(i) == s.charAt(j)) f[i][j] = f[i+1][j-1]+2;
f[i][j] = Math.max(f[i][j] , Math.max(f[i+1][j] ,f[i][j-1]));
}
}
return f[0][n-1];
}
}