
动态规划(Dynamic Programming,简称DP)是一种解决复杂问题的方法,它通过将原问题分解为相对简单的子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
动态规划的核心思想是:
题目描述:斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:F(0) = 0,F(1) = 1,F(n) = F(n - 1) + F(n - 2),其中 n > 1。
示例: 输入:n = 4 输出:3 解释:F(4) = F(3) + F(2) = 2 + 1 = 3
这是动态规划的入门问题,我们可以使用递归、记忆化递归或动态规划来解决。
方法一:递归
最简单的方法是直接根据递推公式使用递归,但这种方法会有大量的重复计算,时间复杂度为O(2^n)。
public int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}方法二:记忆化递归
通过使用哈希表或数组来存储已经计算过的结果,避免重复计算,时间复杂度为O(n)。
public int fib(int n) {
if (n == 0) {
return 0;
}
int[] memo = new int[n + 1];
return fibMemo(n, memo);
}
private int fibMemo(int n, int[] memo) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
if (memo[n] != 0) {
return memo[n];
}
memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
return memo[n];
}方法三:动态规划
使用dp数组来存储中间结果,从底向上计算,时间复杂度为O(n),空间复杂度为O(n)。
public int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}方法四:空间优化的动态规划
由于每次计算只需要用到前两个状态,我们可以只使用两个变量来存储,空间复杂度优化为O(1)。
public int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int prev = 0;
int curr = 1;
for (int i = 2; i <= n; i++) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}题目描述:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例: 输入:n = 3 输出:3 解释:有三种方法可以爬到楼顶。
这是一个典型的动态规划问题,我们可以定义dp[i]表示爬到第i阶楼梯的不同方法数。
状态转移方程:dp[i] = dp[i-1] + dp[i-2]
边界条件:dp[1] = 1, dp[2] = 2
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}空间优化版本:
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int prev = 1;
int curr = 2;
for (int i = 3; i <= n; i++) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}时间复杂度:O(n),需要遍历一次数组。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例: 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
这是一个经典的动态规划问题,我们可以定义dp[i]表示以第i个元素结尾的最大子数组和。
状态转移方程:dp[i] = max(nums[i], dp[i-1] + nums[i])
边界条件:dp[0] = nums[0]
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int maxSum = dp[0];
for (int i = 1; i < n; i++) {
dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
maxSum = Math.max(maxSum, dp[i]);
}
return maxSum;
}空间优化版本:
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int currSum = nums[0];
int maxSum = nums[0];
for (int i = 1; i < n; i++) {
currSum = Math.max(nums[i], currSum + nums[i]);
maxSum = Math.max(maxSum, currSum);
}
return maxSum;
}时间复杂度:O(n),需要遍历一次数组。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1 。
示例: 输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1
这是一个典型的完全背包问题,我们可以定义dp[i]表示凑成总金额i所需的最少硬币个数。
状态转移方程:dp[i] = min(dp[i], dp[i-coin] + 1),其中coin是硬币的面额
边界条件:dp[0] = 0,其余初始化为一个较大的值
public int coinChange(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
if (coins == null || coins.length == 0) {
return -1;
}
int[] dp = new int[amount + 1];
// 初始化dp数组为一个较大的值
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}时间复杂度:O(amount * n),其中n是硬币的种类数。需要填充amount大小的dp数组,每个位置需要遍历n种硬币。 空间复杂度:O(amount),需要一个大小为amount+1的dp数组。
题目描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
示例: 输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长的递增子序列是 [2,3,7,101],因此长度为 4 。
这是一个经典的动态规划问题,我们可以定义dp[i]表示以第i个元素结尾的最长递增子序列的长度。
状态转移方程:dp[i] = max(dp[j] + 1),其中j < i且nums[j] < nums[i]
边界条件:dp[i] = 1,表示每个元素自身是一个长度为1的子序列
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
int maxLength = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLength = Math.max(maxLength, dp[i]);
}
return maxLength;
}时间复杂度:O(n²),其中n是数组的长度。需要填充n大小的dp数组,每个位置需要比较n次。 空间复杂度:O(n),需要一个大小为n的dp数组。
优化版本(贪心+二分查找):
我们可以维护一个数组tails,其中tails[i]表示长度为i+1的递增子序列的末尾元素的最小值。
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int[] tails = new int[n];
int len = 0;
for (int num : nums) {
int left = 0;
int right = len;
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
tails[left] = num;
if (left == len) {
len++;
}
}
return len;
}时间复杂度:O(n log n),其中n是数组的长度。对于每个元素,我们需要进行一次二分查找,时间复杂度为O(log n)。 空间复杂度:O(n),需要一个大小为n的tails数组。
题目描述:给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例: 输入:[1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。
这是一个典型的0-1背包问题,我们可以将其转化为:是否可以从数组中选择一些元素,使得它们的和等于数组总和的一半。
状态定义:dp[i][j]表示前i个元素中是否可以选出一些元素,使得它们的和为j
状态转移方程:dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]]
边界条件:dp[0][0] = true,表示前0个元素可以选出和为0的子集
public boolean canPartition(int[] nums) {
if (nums == null || nums.length == 0) {
return false;
}
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和为奇数,无法分成两个和相等的子集
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
int n = nums.length;
boolean[][] dp = new boolean[n + 1][target + 1];
dp[0][0] = true;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= target; j++) {
if (j < nums[i - 1]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[n][target];
}空间优化版本:
由于每次计算dp[i][j]只需要用到dp[i-1][j]和dp[i-1][j-nums[i-1]],我们可以使用一维数组来优化空间。
public boolean canPartition(int[] nums) {
if (nums == null || nums.length == 0) {
return false;
}
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int num : nums) {
// 注意这里需要从后往前遍历,避免重复使用同一个元素
for (int j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}时间复杂度:O(n * target),其中n是数组的长度,target是数组总和的一半。 空间复杂度:O(target),需要一个大小为target+1的dp数组。
题目描述:给定不同面额的硬币和一个总金额。假设每一种面额的硬币有无限个。计算可以凑成总金额的组合数。
示例: 输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
这是一个典型的完全背包问题,我们可以定义dp[i]表示凑成总金额i的组合数。
状态转移方程:dp[i] += dp[i-coin],其中coin是硬币的面额
边界条件:dp[0] = 1,表示凑成总金额0的方式只有1种(不选任何硬币)
public int change(int amount, int[] coins) {
if (amount == 0) {
return 1;
}
if (coins == null || coins.length == 0) {
return 0;
}
int[] dp = new int[amount + 1];
dp[0] = 1;
// 注意这里的遍历顺序,先遍历硬币,再遍历金额
// 这样可以保证每个硬币只使用一次
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}时间复杂度:O(amount * n),其中n是硬币的种类数。 空间复杂度:O(amount),需要一个大小为amount+1的dp数组。
题目描述:给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:
示例: 输入:word1 = “horse”, word2 = “ros” 输出:3 解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)
这是一个经典的编辑距离问题,我们可以定义dp[i][j]表示将word1的前i个字符转换成word2的前j个字符所需的最少操作数。
状态转移方程:
边界条件:
public int minDistance(String word1, String word2) {
if (word1 == null || word2 == null) {
return 0;
}
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// 初始化边界条件
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
Math.min(dp[i - 1][j], dp[i][j - 1]), // 删除和插入
dp[i - 1][j - 1] // 替换
) + 1;
}
}
}
return dp[m][n];
}时间复杂度:O(mn),其中m和n分别是两个字符串的长度。 空间复杂度:O(mn),需要一个m×n的dp数组。
题目描述:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例: 输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
这是一个典型的动态规划问题,我们可以定义dp[i]表示偷窃到第i个房屋时能够获得的最高金额。
状态转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
边界条件:
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
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 - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}空间优化版本:
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
if (n == 1) {
return nums[0];
}
int prev = nums[0];
int curr = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
int next = Math.max(curr, prev + nums[i]);
prev = curr;
curr = next;
}
return curr;
}时间复杂度:O(n),其中n是数组的长度。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给你一个字符串 s,找到 s 中最长的回文子串。
示例: 输入:s = “babad” 输出:“bab” 或 “aba”
这道题可以使用动态规划或中心扩展法来解决。这里介绍动态规划法。
状态定义:dp[i][j]表示字符串s的子串s[i…j]是否是回文串
状态转移方程:dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]
边界条件:
public String longestPalindrome(String s) {
if (s == null || s.isEmpty()) {
return "";
}
int n = s.length();
boolean[][] dp = new boolean[n][n];
int start = 0;
int maxLength = 1;
// 初始化单个字符的情况
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 初始化两个字符的情况
for (int i = 0; i < n - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
dp[i][i + 1] = true;
start = i;
maxLength = 2;
}
}
// 填充dp数组,长度从3开始
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) {
dp[i][j] = true;
start = i;
maxLength = len;
}
}
}
return s.substring(start, start + maxLength);
}时间复杂度:O(n²),其中n是字符串的长度。需要填充n×n的dp数组。 空间复杂度:O(n²),需要一个n×n的dp数组。
股票买卖问题是动态规划的一个重要应用场景,LeetCode上有一系列相关的题目。这里我们以其中的几道经典题目为例进行解析。
题目描述:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
示例: 输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(价格 = 1)的时候买入,在第 5 天(价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
这道题可以使用动态规划或一次遍历的方法来解决。这里介绍一次遍历的方法。
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) {
return 0;
}
int minPrice = prices[0];
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] < minPrice) {
minPrice = prices[i];
} else {
maxProfit = Math.max(maxProfit, prices[i] - minPrice);
}
}
return maxProfit;
}时间复杂度:O(n),其中n是数组的长度。只需要遍历一次数组。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
示例: 输入:[7,1,5,3,6,4] 输出:7 解释:在第 2 天(价格 = 1)的时候买入,在第 3 天(价格 = 5)的时候卖出,这笔交易所能获得利润 = 5-1 = 4 。随后,在第 4 天(价格 = 3)的时候买入,在第 5 天(价格 = 6)的时候卖出,这笔交易所能获得利润 = 6-3 = 3 。总利润为 4 + 3 = 7 。
这道题可以使用贪心算法或动态规划来解决。这里介绍贪心算法。
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) {
return 0;
}
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
// 如果当天价格高于前一天,就进行交易
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}时间复杂度:O(n),其中n是数组的长度。只需要遍历一次数组。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。
示例: 输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
这是一个典型的二维动态规划问题,我们可以定义dp[i][j]表示从左上角到位置(i,j)的最小路径和。
状态转移方程:dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
边界条件:
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
// 初始化第一行
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 初始化第一列
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 填充dp数组
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m - 1][n - 1];
}时间复杂度:O(mn),其中m和n分别是网格的行数和列数。 空间复杂度:O(mn),需要一个m×n的dp数组。
题目描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
示例: 输入:m = 3, n = 7 输出:28
这是一个典型的组合数学问题或动态规划问题,我们可以定义dp[i][j]表示从左上角到位置(i,j)的不同路径数。
状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件:
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一行和第一列
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 填充dp数组
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}时间复杂度:O(mn),其中m和n分别是网格的行数和列数。 空间复杂度:O(mn),需要一个m×n的dp数组。
题目描述:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0 。
示例: 输入:text1 = “abcde”, text2 = “ace” 输出:3 解释:最长公共子序列是 “ace”,它的长度为 3。
这是一个经典的二维动态规划问题,我们可以定义dp[i][j]表示text1的前i个字符和text2的前j个字符的最长公共子序列长度。
状态转移方程:
边界条件:dp[i][0] = 0,dp[0][j] = 0
public int longestCommonSubsequence(String text1, String text2) {
if (text1 == null || text2 == null || text1.isEmpty() || text2.isEmpty()) {
return 0;
}
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; 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[m][n];
}时间复杂度:O(mn),其中m和n分别是两个字符串的长度。 空间复杂度:O(mn),需要一个m×n的dp数组。
题目描述:给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
示例: 输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从位置 0 到达位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
这道题可以使用贪心算法或动态规划来解决。这里介绍动态规划法。
public boolean canJump(int[] nums) {
if (nums == null || nums.length == 0) {
return false;
}
int n = nums.length;
boolean[] dp = new boolean[n];
dp[0] = true;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && j + nums[j] >= i) {
dp[i] = true;
break;
}
}
}
return dp[n - 1];
}时间复杂度:O(n²),其中n是数组的长度。 空间复杂度:O(n),需要一个大小为n的dp数组。
贪心算法优化版本:
public boolean canJump(int[] nums) {
if (nums == null || nums.length == 0) {
return false;
}
int maxReach = 0;
for (int i = 0; i < nums.length; i++) {
if (i > maxReach) {
return false;
}
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= nums.length - 1) {
return true;
}
}
return true;
}时间复杂度:O(n),其中n是数组的长度。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例: 输入:[2,3,-2,4] 输出:6 解释:子数组 [2,3] 有最大乘积 6。
这道题需要同时跟踪最大乘积和最小乘积,因为负负得正的特性。
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int maxProduct = nums[0];
int currMax = nums[0];
int currMin = nums[0];
for (int i = 1; i < n; i++) {
int temp = currMax;
// 当前的最大值可能来自于当前值、当前值乘以之前的最大值、当前值乘以之前的最小值
currMax = Math.max(nums[i], Math.max(nums[i] * currMax, nums[i] * currMin));
// 当前的最小值可能来自于当前值、当前值乘以之前的最大值、当前值乘以之前的最小值
currMin = Math.min(nums[i], Math.min(nums[i] * temp, nums[i] * currMin));
maxProduct = Math.max(maxProduct, currMax);
}
return maxProduct;
}时间复杂度:O(n),其中n是数组的长度。 空间复杂度:O(1),只使用了常数级别的额外空间。
题目描述:给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例: 输入:[1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。
这道题可以转化为0-1背包问题,详见3.1节。
题目描述:在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例: 输入:[3,2,3,null,3,null,1] 输出:7 解释:小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
这道题可以使用动态规划结合树形递归或后序遍历来解决。
public int rob(TreeNode root) {
int[] result = robHelper(root);
// result[0]表示不偷当前节点的最大金额,result[1]表示偷当前节点的最大金额
return Math.max(result[0], result[1]);
}
private int[] robHelper(TreeNode root) {
if (root == null) {
return new int[]{0, 0};
}
int[] left = robHelper(root.left);
int[] right = robHelper(root.right);
// 不偷当前节点,可以选择偷或不偷子节点
int notRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 偷当前节点,不能偷子节点
int rob = root.val + left[0] + right[0];
return new int[]{notRob, rob};
}时间复杂度:O(n),其中n是树中的节点数。 空间复杂度:O(h),其中h是树的高度,最坏情况下为O(n)。
多维动态规划是一维动态规划的扩展,常见的有二维和三维动态规划。在处理多维动态规划问题时,需要注意以下几点:
状态压缩动态规划是一种将状态用二进制表示的动态规划方法,常用于解决子集选择问题。在状态压缩动态规划中,通常使用一个整数来表示一个子集,每一位表示一个元素是否被选中。
数位动态规划是一种用于解决与数字相关的计数问题的动态规划方法,通常用于统计满足某些条件的数的个数。在数位动态规划中,通常将数字拆分成各个位,然后逐位处理。
动态规划是一种解决复杂问题的强大工具,它通过将原问题分解为相对简单的子问题,利用子问题的解来构建原问题的解。动态规划在算法面试中占据着重要的地位,掌握好动态规划对于提高算法能力至关重要。
通过本文的学习,我们了解了动态规划的基本概念、核心思想和解题步骤,掌握了常见的动态规划问题类型和解法,并通过具体的LeetCode题目解析,深入理解了动态规划的实际应用。
在解决动态规划问题时,我们需要注意以下几点: