重温数据结构与算法之动态规划

前言

摘自 Leetcode

动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于朴素解法。

动态规划有自底向上和自顶向下两种解决问题的方式。自顶向下即记忆化递归,自底向上就是递推。

使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图。动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量。

DP 在笔试和面试中经常会遇到,而 DP 最重要有三点:状态定义,初始状态和状态转移方程。

  • 状态定义:用问题的特征来描述一个子问题
  • 初始状态:初始的状态,边界情况
  • 状态转移方程:状态之间的递推关系

下面会通过 Leetcode 上一些题目来练习 DP

一、爬楼梯问题

1.1 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

状态定义:dp[i] 表示爬到第 i 阶楼梯所需要的方法数

初始状态:dp[0]=0:第0个台阶没有方法, dp[1] = 1:第1个台阶有1种方法,dp[2] = 2:第2个台阶有爬1个再爬1个和爬2个两种方法

状态转移:一次可以爬1个或2个台阶,那么到第 i 阶楼梯的方案数 dp[i] 就等于 dp[i-1] 再爬 1 个台阶 加上 dp[i-2] 再爬 2 个台阶

d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i] = dp[i - 1] + dp[i - 2] dp[i]=dp[i1]+dp[i2]

int climbStairs(int n) {
    int dp[100] = {0, 1, 2};
    if(n == 1 || n == 2) return dp[n];
    for(int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

1.2 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

状态定义:dp[i] 表示 从第i个台阶开始向上爬的最低花费

初始状态:dp[n] = 0:已经到顶部不需要花费,dp[n - 1] = cost[n - 1]:第n-1个台阶再走1步就到楼顶了

状态转移:dp[i] 表示已经选择这个台阶,那么向上爬1个还是2个台阶,取决于上述两个台阶,哪个到顶部花费更少

d p [ i ] = m i n ( d p [ i + 1 ] , d p [ i + 2 ] ) + c o s t [ i ] dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i] dp[i]=min(dp[i+1],dp[i+2])+cost[i]

public int minCostClimbingStairs(int[] cost) {
    int n = cost.length;
    int [] dp = new int[n + 1];
    dp[n] = 0;
    dp[n - 1] = cost[n - 1];
    for (int i = n - 2; i >= 0; i--) {
        dp[i] = cost[i] + Math.min(dp[i + 1], dp[i + 2]); 
    }
    return Math.min(dp[0], dp[1]);
}

上述是自顶向下,还有一种自底向上的解法,,状态定义的不同,会导致初始状态和转移方程也会不同

状态定义:dp[i] 表示到达第i个的最低花费

初始状态:dp[0] = 0:到达第0个台阶不需要花费,dp[1] = 0:到达第1个台阶不需要花费

状态转移:dp[i] 的最低花费取决于到达前两个台阶的花费哪个更小

d p [ i ] = m i n ( d p [ i − 1 ] + c o s t [ i − 1 ] , d p [ i − 2 ] + c o s t [ i − 2 ] ) dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) dp[i]=min(dp[i1]+cost[i1],dp[i2]+cost[i2])

public int minCostClimbingStairs(int[] cost) {
    int len = cost.length;
    int [] dp = new int[n + 1];
    dp[0] = 0;
    dp[1] = 0;
    for (int i = 2; i < n + 1; i++) {
        dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); 
    }
    return dp[n];
}

二、打家劫舍问题

2.1 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

状态定义:dp[i] 表示 偷窃到第i个房间的最高金额

初始状态:dp[0] = nums[0] 表示偷窃第0个房间,dp[1] = max(nums[0], nums[1]) 表示偷窃到第1个房间,可选择第0、1间房中价格更高的房间

状态转移:由于相邻房间不能偷窃,第 i 间房是否偷窃(关系到 dp[i] 的取值)由两种情况决定:偷窃到第 i - 2 间房的最高金额 dp[i - 2], 和偷窃到第 i - 1 间房的最高金额dp[i - 1]。dp[i - 2] 如果和 nums[i] 相加比dp[i - 1]更大表示会偷第 i 间房,但是如果 dp[i - 1] 更大,则不会偷第 i 间房

d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) dp[i]=max(dp[i2]+nums[i],dp[i1])

public int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];
    int [] dp = new int[n];
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    for (int i = 2; i < n; i++) {
        dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
    }
    return dp[n - 1];
}

2.2 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

这题和上一题打家劫舍类似,由于首尾相连,不能同时偷首和尾,可以转化为不偷首或不偷尾 A B ‾ = A ‾ + B ‾ \overline{AB} = \overline{A}+ \overline{B} AB=A+B,取最大值

public int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];
    if (n == 2) return Math.max(nums[0], nums[1]);
    return Math.max(help(nums, 0), help(nums, 1));
}
public int help(int[] nums, int start) {
    int n = nums.length;
    int [] dp = new int[n - 1];
    dp[0] = nums[start];
    dp[1] = Math.max(nums[start], nums[start + 1]);
    for (int i = 2; i < n - 1; i++) {
        dp[i] = Math.max(dp[i - 2] + nums[start + i], dp[i - 1]);
    }
    return dp[n - 2];
}

三、买卖股票问题

3.1 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

状态定义:dp[i] 表示 前 i 天获取的最大利润

初始状态:dp[0] = 0 第0天可以买入但不能卖出,不买不卖利润最大,为0

状态转移:由于本题中股票只买卖1次,需要维护一个前i - 1天最小价格值 curMinPrice,这是买入的时机。第 i 天是否卖出 需要当前价格的利润(prices[i] - curMinPrice)和前 i - 1 天的最大利润比较

d p [ i ] = m a x ( d p [ i − 1 ] , p r i c e s [ i ] − c u r M i n P r i c e ) dp[i] = max(dp[i - 1], prices[i] - curMinPrice) dp[i]=max(dp[i1],prices[i]curMinPrice)

public int maxProfit(int[] prices) {
    int n = prices.length;
    if (n == 0) return 0;
    int [] dp = new int[n];
    dp[0] = 0;
    int curMinPrice = prices[0];
    for (int i = 1; i < n; i++) {
        dp[i] = Math.max(dp[i - 1], prices[i] - curMinPrice);
        curMinPrice = Math.min(curMinPrice, prices[i]);
    }
    return dp[n - 1];
}

3.2 买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

状态定义:dp[i][j] 表示到第 i 天,持股状态为 j 时,拥有的最大利润。j = 0 表示不持有股票,j = 1表示持有股票

初始状态:dp[0][0] = 0 第0天不持有股票说明没买,利润为0; dp[0][1] = -prices[0] 说明已买股票

状态转移:dp[i][0],如果第i天交易完后手里没有股票,那么可能为前一天(第i-1天)已经没有股票,或者前一天结束的时候手里持有一支股票,现在将其卖出,获得收益prices[i]。因此为了收益最大化,需要将两者比较。

dp[i][1]同理,前一天已经持有一支股票,现在依然持有或者前一天结束时还没有股票,现在将其买入,两者需要比较收益。

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i])

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])

public int maxProfit(int[] prices) {
    int n = prices.length;
    int [][] dp = new int[n][2];
    dp[0][0] = 0;
    dp[0][1] = -prices[0];
    for (int i = 1; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
    }
    return dp[n - 1][0];
}

3.3 买卖股票的最佳时机 III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

状态定义:dp[i][j] 表示 第 i 天处于状态j能获取的最大利润,状态j有5种,只有进入前一种状态,才能进入后一种状态,或者保持原来的状态,如下所示

  • 0:初始状态,不买不卖
  • 1:第一次买入
  • 2:第一次卖出
  • 3:第二次买入
  • 4:第二次卖出

初始状态:dp[0][0] = 0,dp[0][1] = -prices[0], dp[0][2] = 0, dp[0][3] = -prices[0], dp[0][4] = 0,dp[i][0] = 0

状态转移:dp[i][j]为第i天的状态j所获取最大的利润,进入这种状态有两种,一个是今天进行操作,另一个是不进行操作。进行操作需要获取上一天的上一个状态dp[i - 1][j - 1],然后根据买入卖出加减价格prices[i]。不进行操作就是前一天状态dp[i - 1][j]

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 1 ] ) dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]) dp[i][1]=max(dp[i1][0]prices[i],dp[i1][1])

d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] , d p [ i − 1 ] [ 2 ] ) dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) dp[i][2]=max(dp[i1][1]+prices[i],dp[i1][2])

d p [ i ] [ 3 ] = m a x ( d p [ i − 1 ] [ 2 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 3 ] ) dp[i][3] = max(dp[i - 1][2] - prices[i], dp[i - 1][3]) dp[i][3]=max(dp[i1][2]prices[i],dp[i1][3])

d p [ i ] [ 4 ] = m a x ( d p [ i − 1 ] [ 3 ] + p r i c e s [ i ] , d p [ i − 1 ] [ 4 ] ) dp[i][4] = max(dp[i - 1][3] + prices[i], dp[i - 1][4]) dp[i][4]=max(dp[i1][3]+prices[i],dp[i1][4])

public int maxProfit(int[] prices) {
    int n = prices.length;
    int [][] dp = new int[n][5];
    dp[0][0] = 0;
    dp[0][1] = -prices[0];
    dp[0][2] = 0;
    dp[0][3] = -prices[0];
    dp[0][4] = 0;
    for (int i = 1; i < n; i++) {
        dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
        dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
        dp[i][3] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][3]);
        dp[i][4] = Math.max(dp[i - 1][3] + prices[i], dp[i - 1][4]);
    }
    return dp[n - 1][4]; 
}

还有一种解法可以借助买卖股票的最佳时机的代码

状态定义:dp1[i] 第i天之前执行一次买卖所获得最大利润, dp2[i] 第i天之后(包含)执行一次买卖所获得最大利润

初始状态:dp1[0] = 0, dp2[n - 1] = 0

状态转移:dp1和 上面第一题买卖股票的最佳时机一样的状态转移方程,dp2和dp1类似,只不过需要找到当前最大的价格

d p 1 [ i ] = m a x ( d p 1 [ i − 1 ] , p r i c e s [ i ] − c u r M i n P r i c e ) dp1[i] = max(dp1[i - 1], prices[i] - curMinPrice) dp1[i]=max(dp1[i1],prices[i]curMinPrice)

d p 2 [ i ] = m a x ( d p 2 [ i + 1 ] , c u r M a x P r i c e − p r i c e s [ i ] ) dp2[i] = max(dp2[i + 1], curMaxPrice - prices[i]) dp2[i]=max(dp2[i+1],curMaxPriceprices[i])

public int maxProfit(int[] prices) {
    int n = prices.length;
    int [] dp1 = new int[n];
    int [] dp2 = new int[n];
    dp1[0] = 0;
    dp2[n - 1] = 0;
    int curMinPrice = prices[0], curMaxPrice = prices[n - 1];
    for (int i = 1; i < n; i++) {
        dp1[i] = Math.max(dp1[i - 1], prices[i] - curMinPrice);
        curMinPrice = Math.min(curMinPrice, prices[i]);
    }

    for (int i = n - 2; i >= 0; i--) {
        dp2[i] = Math.max(dp2[i + 1], curMaxPrice - prices[i]);
        curMaxPrice = Math.max(curMaxPrice, prices[i]);
    }

    int ans = 0;
    for (int i = 0; i < n; i++) {
        int t = dp1[i] + dp2[i];
        if (t > ans) {
            ans = t;
        }
    }
    return ans;
}

3.4 买卖股票的最佳时机 IV

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

状态定义:dp[i][j] 第 i 天处于状态j能获取的最大利润(j < 2k + 1), 上一题中k=2就是5种状态,k次就可以推导为2k+1种状态

初始状态:从上一题可以很容易推导出j为偶数就是卖(0除外),并且是买了卖,所以利润为0,奇数是买,利润是-prices[0]

KaTeX parse error: {equation} can be used only in display mode.

状态转移:从上一题k=2可以推导出4个状态公式,都是关于买入和卖出,可进一步推导2k个状态

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − 1 ] − p r i c e s [ i ] , d p [ i − 1 ] [ j ] ) dp[i][j] = max(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]) dp[i][j]=max(dp[i1][j1]prices[i],dp[i1][j])

d p [ i ] [ j + 1 ] = m a x ( d p [ i − 1 ] [ j + 1 ] + p r i c e s [ i ] , d p [ i − 1 ] [ j + 1 ] ) dp[i][j + 1] = max(dp[i - 1][j + 1] + prices[i], dp[i - 1][j + 1]) dp[i][j+1]=max(dp[i1][j+1]+prices[i],dp[i1][j+1])

public int maxProfit(int k, int[] prices) {
    int n = prices.length;
    int [][] dp = new int[n][2 * k + 1];
    for (int j = 1; j < 2 * k + 1; j += 2) {
        dp[0][j] = -prices[0];
    }
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < 2 * k + 1; j += 2) {
            dp[i][j] = Math.max(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]);
            dp[i][j + 1] = Math.max(dp[i - 1][j] + prices[i], dp[i - 1][j + 1]);
        } 
    }
    return dp[n - 1][2 * k]; 
}

3.5 最佳买卖股票时机含冷冻期

给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

状态定义:dp[i][j] 第i天结束后持股状态为j, 获取的最大利润。

  • j = 0 手上不持有股票,而且不在冷冻期
  • j = 1 手上不持有股票,在冷冻期
  • j = 2 手上持有股票

初始状态:dp[0][0] = 0,不买不卖,利润为0,dp[0][1] = 0 买了又卖,处于冷冻期,利润还是0,dp[0][2] = -prices[0] 买了股票

状态转移:

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] ) dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]) dp[i][0]=max(dp[i1][0],dp[i1][1])

d p [ i ] [ 1 ] = d p [ i − 1 ] [ 2 ] + p r i c e s [ i ] dp[i][1] = dp[i - 1][2] + prices[i] dp[i][1]=dp[i1][2]+prices[i]

d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 2 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][2] = max(dp[i - 1][2], dp[i - 1][0] - prices[i]) dp[i][2]=max(dp[i1][2],dp[i1][0]prices[i])

public int maxProfit(int[] prices) {
    int n = prices.length;
    int [][] dp = new int[n][3];
    dp[0][2] = -prices[0];
    for (int i = 1; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
        dp[i][1] = dp[i - 1][2] + prices[i];
        dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][0] - prices[i]);
    }
    return Math.max(dp[n - 1][0], dp[n - 1][1]);
}

3.6 买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

状态定义:dp[i][j] 表示第i天进入状态j获取的最大利润。j = 0 不持有股票, j = 1持有股票

初始状态:dp[0][0] = 0 dp[0][1] = -price[0]

状态转移:

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] − f e e ) dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i]fee)

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])

public int maxProfit(int[] prices, int fee) {
    int n = prices.length;
    int [][] dp = new int[n][2];
    dp[0][1] = -prices[0];
    for (int i = 1; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
    }
    return dp[n - 1][0];
}

四、子序列问题

4.1 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

状态定义:dp[i] 为以nums[i] 结尾的连续子数组的最大和

初始状态:dp[0] = nums[0]

状态转移:dp[i] 的取值跟dp[i - 1]和nums[i]有关系, 由于dp[i]是以nums[i]结尾的子数组,肯定会加上nums[i], 是否加上前面的连续子数组,取决于dp[i - 1] 的值,只要dp[i - 1] 大于 0 就可以加上,也可以通过max函数判断

d p [ i ] = m a x ( n u m s [ i ] , d p [ i − 1 ] + n u m s [ i ] ) dp[i] = max(nums[i], dp[i - 1] + nums[i]) dp[i]=max(nums[i],dp[i1]+nums[i])

public int maxSubArray(int[] nums) {
    int n = nums.length;
    int [] dp = new int[n];
    dp[0] = nums[0];
    int ans = nums[0];
    for (int i = 1; i < n; i++) {
        dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
        ans = Math.max(ans, dp[i]);
    }
    return ans;
}

4.2 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

状态定义:dp[i] 表示以nums[i]结尾的最长子序列长度

初始状态:dp[0] = 1,dp[i] = 1

状态转移:由于严格递增,dp[i] 的取值和前i个值中比nums[i]小的nums[j],但是dp[j]最大的值相关,加1即可

d p [ i ] = m a x ( d p [ 0.. j ] ) + 1 , 0 ≤ j < i , n u m [ j ] < n u m [ i ] dp[i]= max(dp[0..j]) + 1, 0≤j<i, num[j]<num[i] dp[i]=max(dp[0..j])+1,0j<i,num[j]<num[i]

public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int [] dp = new int[n];
    dp[0] = 1;
    int ans = 1;
    for (int i = 1; i < n; i++) {
        dp[i] = 1;
        for (int j = i - 1; j >= 0; j--) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        ans = Math.max(ans, dp[i]);
    }
    return ans;
}

4.3 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

状态定义:dp[i][j] 表示 字符串text1[0, i - 1]和字符串text2[0, j - 1]的最长公共子序列长度

初始状态:dp[0][0] = 0, dp[i][0] = dp[0][j] = 0

状态转移:text1[i-1]和text[j-1]两个字符如果相等,那么很显然,最长公共子序列长度增加一位,需要将dp[i-1][j-1]加1,如果不相等,需要对相邻的两个子序列进行比较,取最大值。举个例子:abec和 ae,最终的c和e不相等,这时需要根据abe,ae和abec,a比较得到最大值

d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , text1[i−1] = text2[j−1]  m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , text1[i−1]  ≠ text2[j−1] \begin{equation} dp[i][j]=\begin{cases} dp[i-1][j-1] + 1, & \text{text1[i−1] = text2[j−1] }\\ max(dp[i-1][j], dp[i][j-1]), & \text{text1[i−1] }\neq \text{text2[j−1]} \end{cases} \end{equation} dp[i][j]={dp[i1][j1]+1,max(dp[i1][j],dp[i][j1]),text1[i−1] = text2[j−1] text1[i−1] =text2[j−1]

public int longestCommonSubsequence(String text1, String text2) {
    int n1 = text1.length();
    int n2 = text2.length();
    int [][] dp = new int[n1 + 1][n2 + 1];
    for (int i = 1; i < n1 + 1; i++) {
        for (int j = 1; j < n2 + 1; j++) {
            if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                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[n1][n2];
}

五、总结

  1. DP 的重点就是状态定义,初始状态,状态转移方程三点,它能解决的问题都是可以拆分为子问题的题目,DP会记住过往,减少重复计算
  2. 从上面题目可以看出,dp经常会用来求一些极值,最大利润,最长序列,最高金额,最小花费等等,当碰到类似时,可以考虑DP
  3. 从上面的DP例子会发现,一维的dp[i]总是和dp[i-1]或dp[i+1] 有关,二维的dp[i][j]和dp[i-1][j-1], dp[i][j-1], dp[i - 1][j]相关,刷题多了,很容易猜到状态转移方程
  4. 上述很多DP题目在编写代码的时候都用了O(n)的空间,如果dp[i]只和dp[i-1]或dp[i+1]相关,可以用前后变量保存,空间优化为O(1)

参考

  1. 暴力搜索、贪心算法、动态规划(Java)
  2. 『 动态规划 』字符串DP解决一众字符串匹配问题
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aabond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值