标签:最小 哪些 最长上升子序列 index nfa 不用 打印 amp max
讲解动态规划的资料很多,官方的定义是指把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。概念中的各阶段之间的关系,其实指的就是状态转移方程。
我们一般看到的状态转移方程,基本长成下面这样(注:i、j、k 都是在定义DP方程中用到的参数。opt 指代特殊的计算逻辑,大多数情况下为 max 或 min。func 指代逻辑函数):
dp[i] = opt(dp[i-1]) + 1
dp[i][j] = opt(dp[i-1][j-1], dp[i-1][j]+arr[i][j])
dp[i][j] = opt(dp[i-1][j] + xi, dp[i][j-1] + yj, ...)
dp[i][j] = func(i,j,k) + opt(dp[i-1][k])
适用范围
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
python实现:
class Solution:
def numWays(self, n: int) -> int:
if n == 0:
return 1
elif n <= 2:
return n
dp = [1, 2]
for i in range(3, n+1):
tmp = (dp[1] + dp[0])%1000000007
dp[0] = dp[1]
dp[1] = tmp
return dp[1]
使用标准的动态规划实现:
class Solution {
public int numWays(int n) {
if(n <= 1) return 1;
int dp[] = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i=2; i< n+1; i++) {
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
}
return dp[n];
}
}
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
输入: coins = [2], amount = 3
输出: -1
python实现:
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 使用动态规划,找到状态方程
# dp[i]: 表示金额i的最优解
# dp[i] 分别为min(dp[i-1], dp[i-2], dp[i-5], ...)+1
dp = [-1 for _ in range(amount+1)]
dp[0] = 0
for i in range(1, amount+1):
for j in coins:
if i - j >= 0 and dp[i-j] != -1:
# 说明可以组成
if dp[i] == -1:
dp[i] = dp[i-j] + 1
else:
dp[i] = min(dp[i], dp[i-j]+1)
return dp[amount]
典型的动态规划问题。
另一种实现:
class Solution:
def coinChange(self, coins, amount):
# amount+1 这里表示极大值, amount金额不可能有amount+1个硬币
dp = [amount+1] * (amount+1)
# base case
dp[0] = 0
for i in range(amount+1):
for coin in coins:
# i小于coin无解
if i - coin < 0:
continue
dp[i] = min(dp[i], dp[i-coin] + 1)
if dp[amount] == amount + 1:
# 说明无解,还是初始值
return -1
else:
return dp[amount]
状态转移方程:dp[n] =dp[n-1] + dp[n-2]
上1阶台阶:有1种方式
上2阶台阶:有1+1和2两种方式
上n阶台阶:到达第n阶的方法总数就是到第n-1阶和第n-2阶的方法数之和。
func climbStairs(n int) int {
if n ==1 {
return 1
}
dp := make([]int, n+1)
dp[1] = 1
dp[2] = 2
for i:=3; i<=n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
给定一个整数数组nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
首先我们分析题目,一个连续子数组一定要以一个数作为结尾,那么我们可以将状态定义成如下:
dp[i]:表示以 nums[i] 结尾的连续子数组的最大和。
根据状态的定义,我们继续进行分析:如果要得到 dp[i],那么 nums[i] 一定会被选取。并且 dp[i] 所表示的连续子序列与 dp[i-1] 所表示的连续子序列很可能就差一个 nums[i] 。即:
dp[i] = dp[i-1]+nums[i] , if (dp[i-1] >= 0)
但是这里我们遇到一个问题,很有可能 dp[i-1] 本身是一个负数。那这种情况的话,如果 dp[i] 通过 dp[i-1]+nums[i] 来推导,那么结果其实反而变小了,因为我们 dp[i] 要求的是最大和。所以在这种情况下,如果 dp[i-1] < 0,那么 dp[i] 其实就是 nums[i] 的值。即
dp[i] = nums[i] , if (dp[i-1] < 0)
综上分析,我们可以得到:
dp[i]=max(nums[i], dp[i?1]+nums[i])
得到了状态转移方程,但是我们还需要通过一个已有的状态的进行推导,我们可以想到 dp[0] 一定是以 nums[0] 进行结尾,所以
dp[i] = dp[i-1]+nums[i] , if (dp[i-1] >= 0)
dp[0] = nums[0]
在很多题目中,因为 dp[i] 本身就定义成了题目中的问题,所以 dp[i] 最终就是要的答案。但是这里状态中的定义,并不是题目中要的问题,不能直接返回最后的一个状态 (这一步经常有初学者会摔跟头)。所以最终的答案,其实我们是寻找:
max(dp[0], dp[1], ..., d[i-1], dp[i])
python实现:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 创建一个dp数组
n = len(nums)
dp = [0]*n
dp[0] = nums[0]
for i in range(1, n):
dp[i] = max(nums[i], dp[i-1]+nums[i])
return max(dp)
不得不说,这是一道经典的动态规划题目,必须要掌握。
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
因为题目中没有要求连续,所以LIS可能是连续的,也可能是非连续的。同时,LIS符合可以从其子问题的最优解来进行构建的条件。所以我们可以尝试用动态规划来进行求解。首先我们定义状态:
dp[i] :表示以nums[i]结尾的最长上升子序列的长度
分析:
我们先初步得出上面的结论,但是我们发现了一些问题。因为dp[i]前面比他小的元素,不一定只有一个!
可能除了 nums[j],还包括 nums[k],nums[p] 等等等等。
dp[i] = max(dp[j]+1,dp[k]+1,dp[p]+1,.....)
只要满足:
nums[i] > nums[j]
nums[i] > nums[k]
nums[i] > nums[p]
...
golang实现:
func lengthOfLIS(nums []int) int {
if len(nums) < 1 {
return 0
}
dp := make([]int, len(nums))
result := 1
for i := 0; i < len(nums); i++ {
dp[i] = 1
for j := 0; j < i; j++ {
if nums[j] < nums[i] {
dp[i] = max(dp[j]+1, dp[i])
}
}
result = max(result, dp[i])
}
return result
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
python实现:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
if n<1:
return 0
dp = [0] * n
result = 1
for i in range(n):
dp[i] = 1
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[j]+1, dp[i])
result = max(result, dp[i])
return result
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
题解:
其实也就等同于,每一步我们只能往下移动一格或者往右移动一格。
题目很明显就是一个找最优解的问题,并且可以从子问题的最优解进行构建。所以我们通过动态规划进行求解。首先,我们定义状态:
dp[i][j]:表示包含第i行第j列元素的最小路径和
我们很容易想到可以自顶向下进行分析。并且,无论最后的路径是哪一条,它一定要经过最顶上的元素,即 [0,0]。所以我们需要对 dp[0][0] 进行初始化。
dp[0][0] = [0][0]位置所在的元素值
继续分析,如果我们要求dp[i][j]
,那么气其一定来自于自己头顶上的两个元素移动而来的,它们分别是dp[i-1][j-1]
、dp[i-1][j]
。所以状态转移方程为:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
golang的实现:
func minimumTotal(triangle [][]int) int {
if len(triangle) < 1 {
return 0
}
if len(triangle) == 1 {
return triangle[0][0]
}
dp := make([][]int, len(triangle))
for i, arr := range triangle {
dp[i] = make([]int, len(arr))
}
result := 1<<31 - 1
dp[0][0] = triangle[0][0]
dp[1][1] = triangle[1][1] + triangle[0][0]
dp[1][0] = triangle[1][0] + triangle[0][0]
for i := 2; i < len(triangle); i++ {
for j := 0; j < len(triangle[i]); j++ {
if j == 0 {
dp[i][j] = dp[i-1][j] + triangle[i][j]
} else if j == (len(triangle[i]) - 1) {
dp[i][j] = dp[i-1][j-1] + triangle[i][j]
} else {
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
}
}
}
for _,k := range dp[len(dp)-1] {
result = min(result, k)
}
return result
}
func min(a, b int) int {
if a > b {
return b
}
return a
}
python的实现:
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
if n < 1:
return 0
if n == 1:
return triangle[0][0]
dp = [0] * n
for i in range(n):
dp[i] = [0] * len(triangle[i])
result = 10**5
dp[0][0] = triangle[0][0]
dp[1][1] = triangle[1][1] + triangle[0][0]
dp[1][0] = triangle[1][0] + triangle[0][0]
for i in range(2, n):
for j in range(len(triangle[i])):
if j == 0:
dp[i][j] = dp[i-1][j] + triangle[i][j]
elif j == len(triangle[i]) - 1:
dp[i][j] = dp[i-1][j-1] + triangle[i][j]
else:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
result = min(dp[-1])
return result
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
题解:题目明显符合可以从子问题的最优解进行构建,所以我们考虑使用动态规划进行求解。首先我们定义状态:
dp[i][j]:表示包含第i行j列元素的最小路径和
同样,因为任何一条到达右下角的路径,都会经过[0][0]
这个元素,所以我们需要对dp[0][0]
进行初始化。
dp[0][0] = [0][0]位置所在的元素值
继续分析,根据题目给的条件,如果我们要求dp[i][j]
,那么它一定是从自己的上方或者左边移动而来。
进而,我们得到状态转移方程:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
同样,我们需要考虑两种特殊情况:
最后,因为我们的目标是从左上角走到右下角,整个网格的最小路径和其实就是包含右下角元素的最小路径和。
设:dp的长度为L
最终结果就是:dp[L-1][len(L-1)-1]
综上,我们就分析完了,我们总共进行了4步:
go实现:
func minPathSum(grid [][]int) int {
l := len(grid)
if l < 1 {
return 0
}
dp := make([][]int, l)
for i, arr := range grid {
dp[i] = make([]int, len(arr))
}
dp[0][0] = grid[0][0]
for i := 0; i < l; i++ {
for j := 0; j < len(grid[i]); j++ {
if i == 0 && j != 0 {
dp[i][j] = dp[i][j-1] + grid[i][j]
} else if j == 0 && i != 0 {
dp[i][j] = dp[i-1][j] + grid[i][j]
} else if i !=0 && j != 0 {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
}
}
}
return dp[l-1][len(dp[l-1])-1]
}
func min(a, b int) int {
if a > b {
return b
}
return a
}
python实现:
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
# m行 * n列
m = len(grid)
n = len(grid[0])
# 初始化dp
dp = [0] * m
for i in range(m):
dp[i] = [0] * n
for i in range(m):
for j in range(n):
if i==0 and j == 0:
dp[i][j] = grid[0][0]
elif i == 0 and j > 0:
dp[i][j] = dp[i][j-1] + grid[i][j]
elif i > 0 and j == 0:
dp[i][j] = dp[i-1][j] + grid[i][j]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例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 。
题解:
如何定义dp[i]
?
可以有两种定义:
如果我们定义为状态一,因为我们没办法知道获取最高金额时,小偷到底偷盗了哪些房屋。所以我们需要找到所有状态中的最大值,才能找到我们的最终答案。即:
max(dp[0],dp[1],.....,dp[len(dp)-1])
如果我们定义为状态二,因为小偷一定会从前偷到最后(强调:偷盗至第i个房间,不代表小偷要从第i个房间中获取财物)。所以我们的最终答案很容易确定。即:
dp[i]
如果是状态一,偷盗含第 i 个房间时能获取的最高金额,我们相当于要找到偷盗每一间房子时可以获取到的最大金额。比如下图,我们要找到 dp[4] ,也就是偷盗 9 这间房子时,能获取到的最大金额。那我们就需要找到与9不相邻的前后两段中能获取到的最大金额。我们发现题目进入恶性循环,因为我们若要找到与9不相邻的两端中能偷盗的最大金额,根据 dp[i] 的定义,我们就又需要分析在这两段中盗取每一间房子时所能获取的最大利益!想想都很可怕!所以我们放弃掉这种状态的定义。
如果是状态二,偷盗至第 i 个房子时,所能获取的最大利益。那我们可以想到,由于不可以在相邻的房屋闯入,所以 至i房屋可盗窃的最大值,要么就是至 i-1 房屋可盗窃的最大值,要么就是至 i-2 房屋可盗窃的最大值加上当前房屋的值,二者之间取最大值,即:
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
python实现:
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) < 1:
return 0
if len(nums) == 1:
return nums[0]
if len(nums) == 2:
return max(nums[0], nums[1])
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-2] + nums[i], dp[i-1])
return dp[-1]
python实现的时候一定要注意边界条件!
java实现:
class Solution {
//找到动态规划的算式
// dp[i] = max(dp[i-2]+nums[i], dp[i-1])
public int rob(int[] nums) {
int prevMax = 0;
int currMax = 0;
for (int x : nums) {
int temp = currMax;
currMax = Math.max(prevMax + x, currMax);
prevMax = temp;
}
return currMax;
}
}
go实现:
func rob(nums []int) int {
if len(nums) < 1 {
return 0
}
if len(nums) == 1 {
return nums[0]
}
if len(nums) == 2 {
return max(nums[0],nums[1])
}
dp := make([]int, len(nums))
dp[0] = nums[0]
dp[1] = max(nums[0],nums[1])
for i := 2; i < len(nums); i++ {
dp[i] = max(dp[i-2]+nums[i],dp[i-1])
}
return dp[len(dp)-1]
}
func max(a,b int) int {
if a > b {
return a
}
return b
}
给两个整数数组A和B,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
最直接的思路是使用动态规划,python实现:
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
A_len = len(A)
B_len = len(B)
max_count = 0
# 注意边界是len+1
dp = [0] * (A_len+1)
for i in range(A_len+1):
dp[i] = [0] * (B_len+1)
# 另一种简单的初始化方法:dp = [[0 for _ in range(B_len+1)] for _ in range(A_len+1)]
# 动态规划 dp[i][j]表示以A[i]开头,B[j]结尾的最长子数组的长度
# dp[i][j] = dp[i-1][j-1] +1 if A[i] == B[j]
# dp[i][j] = 0 if A[i] != B[j]
for i in range(1, A_len+1):
for j in range(1, B_len+1):
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = 0
if dp[i][j] > max_count:
max_count = dp[i][j]
return max_count
这个题目还有滑动窗口的解法,貌似更快:
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
# 滑动窗口,计算这个区域内最长公共子串的长度
def get_max_length(a_start, b_start, length):
max_length = 0
count = 0
for i in range(length):
if A[a_start+i] == B[b_start+i]:
count += 1
if count > max_length:
max_length = count
else:
count = 0
return max_length
A_len = len(A)
B_len = len(B)
max_count = 0
# 固定A,移动B,使得B首元素对应A某个元素
for i in range(A_len):
length = min(A_len-i, B_len)
max_count = max(max_count, get_max_length(i, 0, length))
# 固定B,移动A, 使得A首元素对应B某个元素
for j in range(B_len):
length = min(A_len, B_len-j)
max_count = max(max_count, get_max_length(0, j, length))
return max_count
给定一个数组,它的i个元素是一支给定股票第i天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
思路:状态转移方程 dp[i]表示第i天卖出的最大值,那么:
dp[i] 有两种情况:
1. 要么当前卖出,最大利润就是price - minprice (minprice为当前最低价)
2. 要么不卖,dp[i] = dp[i-1]
python实现:
#给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
#如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
#注意:你不能在买入股票前卖出股票。
#示例 1:
#输入: [7,1,5,3,6,4]
#输出: 5
#解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
#注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
#示例 2:
#输入: [7,6,4,3,1]
#输出: 0
#解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
minprice = 2**32
max_profit = 0
for p in prices:
minprice = min(minprice, p)
max_profit = max(max_profit, p-minprice)
return max_profit
java实现:
class Solution {
public int maxProfit(int[] prices) {
int minprice = Integer.MAX_VALUE;
int max_profit = 0;
for(int i=0; i<prices.length; i++) {
if (prices[i]<minprice){
minprice = prices[i];
}
if ((prices[i]-minprice) > max_profit) {
max_profit = prices[i]-minprice;
}
}
return max_profit;
}
}
你可以尽可能的完成多次交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
java实现:
class Solution {
public int maxProfit(int[] prices) {
// 使用贪心算法,每次获取谷峰
int max_profit = 0;
for (int i=1; i<prices.length; i++){
if (prices[i] > prices[i-1]) {
max_profit += prices[i] - prices[i-1];
}
}
return max_profit;
}
}
每次获取谷峰,也就是说如果前天和今天进行比较,如果今天股票价格涨了,就把涨的部分算作利润(如果真的存在股神,那这种也是股神能获取的最大值了)。
证明贪心算法的正确性:
从第 i 天(这里 i >= 1)开始,与第 i - 1 的股价进行比较,如果股价有上升(严格上升),就将升高的股价( prices[i] - prices[i- 1] )记入总利润,按照这种算法,得到的结果就是符合题意的最大利润。
"贪心算法"在每一步总是做出在当前看来最好的选择。
贪心算法和动态规划相比,它既不看前面(也就是说它不需要从前面的状态转移过来),也不看后面(无后效性,后面的选择不会对前面的选择有影响),因此贪心算法时间复杂度一般是线性的,空间复杂度是常数级别的。
这道题“贪心”的地方在于,对于“今天的股价 - 昨天的股价”,得到的结果有3种可能:正数、0、负数
贪心算法的决策是:只加正数
使用反证法:
假设 “贪心算法” 得到的解并不是最优解,即我们还能够找到一个可行解比 “贪心算法” 得到的利润还多。差分数组中除了差分为正数的项以外,还有就是差分为 0 的项与差分为负数的项。“贪心算法” 是所有差分为正数的项的和。
1、如果可行解在 “贪心算法” 的基础上,选择了差分为 0 的项,得到的结果与“贪心算法”得到的结果一样,因此加上差分为 0 的项不会比“贪心算法”得到的结果更好;
2、如果可行解在 “贪心算法” 的基础上,选择了差分为负数的项,加上一个负数得到的结果一定比 “贪心算法” 得到的结果要少,加上差分为负数的项,一定比 “贪心算法” 得到的结果更少;
3、如果可行解在 “贪心算法” 的基础上,去掉了任何一个差分为正数的项,同上,得到的结果一定比 “贪心算法” 得到的结果要小,因此,“贪心算法” 的所有组成项不能删去任何一个。
综上,除了 “贪心算法” 以外,找不到一个更优的解法,因此 “贪心算法” 就是最优解。(证完)
使用动态规划
定义状态
状态dp[i][j]
定义如下:
i
表示索引为i
的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;j
表示索引为i
的那一天是持有股票,还是持有现金。这里0
表示持有现金,1
表示持有股票。思考状态转移方程
开始:
dp[0][0] = 0 什么都不做
dp[0][1] = -price[0] 如果买入股票,当前收益是负数
中间:
dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
当天选择持有现金时,最大值要么是前天不操作,要么是今天卖掉股票(前天买入股票)
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
当天选择持有股票时,最大值要么是前天不操作,要么是今天买入股票(前天持有现金)
终止:
输出dp[len-1][0],因为一定有dp[len-1][0] > dp[len-1][1]
python实现:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
total = len(prices)
if total < 2:
return 0
# 0:持有现金
# 1:持有股票
# 状态转移:0 → 1 → 0 → 1 → 0 → 1 → 0
dp = [[0 for _ in range(2)] for _ in range(total)]
dp[0][0] = 0
dp[0][1] = -prices[0]
for i in range(1, total):
dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
# 返回最后持有现金的值
return dp[total-1][0]
给你一个整数数组nums
,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
java实现:
class Solution {
public int maxProduct(int[] nums) {
int max = Integer.MIN_VALUE;
int imax = 1, imin = 1;
for(int i =0;i<nums.length; i++)
{
if(nums[i]<0){
//交换imax和imin
int tmp = imax;
imax = imin;
imin = tmp;
}
imax = Math.max(imax*nums[i], nums[i]);
imin = Math.min(imin*nums[i], nums[i]);
max = Math.max(max, imax);
}
return max;
}
}
python实现:
class Solution:
def maxProduct(self, nums: List[int]) -> int:
res = -2**32
imax = 1
imin = 1
for i in range(len(nums)):
if nums[i] < 0:
imax, imin = imin, imax
imax = max(imax*nums[i], nums[i])
imin = min(imin*nums[i], nums[i])
res = max(res, imax)
return res
以上是巧妙解法,最基础的思考是使用动态规划,使用两个数组dp_max[i]和dp_min[i],分别代表:到i位置时乘积最大的连续子数组和乘积最小的连续子数组。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
n = len(nums)
dp_max = [-2**32] * n
dp_min = [-2**32] * n
dp_max[0] = nums[0]
dp_min[0] = nums[0]
for i in range(1, n):
dp_max[i] = max(dp_max[i-1]*nums[i], nums[i], dp_min[i-1]*nums[i])
dp_min[i] = min(dp_min[i-1]*nums[i], nums[i], dp_max[i-1]*nums[i])
return max(dp_max)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
示例:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
输入: m = 7, n = 3
输出: 28
思路:考虑用动态规划的思想解决问题。定义dp[i][j]
为:到达i,j
位置处的路径个数,那么:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
特殊情况:
dp[0][j] = dp[0][j-1] = 1
dp[i][0] = dp[i-1][0] = 1
python实现:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# dp[n][m]
dp = [[0 for _ in range(m)] for _ in range(n)]
for i in range(n):
dp[i][0] = 1
for j in range(m):
dp[0][j] = 1
for i in range(1, n):
for j in range(1, m):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[n-1][m-1]
另一种实现,可能更容易理解:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# dp[n][m]
dp = [[0 for _ in range(m)] for _ in range(n)]
for i in range(n):
dp[i][0] = 1
for j in range(m):
dp[0][j] = 1
for i in range(1, n):
for j in range(1, m):
# 不是第一行时,第一行时不需要处理
if i > 0:
dp[i][j] += dp[i-1][j]
# 不是第一列时,第一列时不需要处理
if j > 0:
dp[i][j] += dp[i][j-1]
return dp[n-1][m-1]
现在考虑网络中有障碍物,那么从左上角到右下角将会有多少条不同的路径?
思路:实际上,是类似的,如果一个位置的左侧有障碍物,则它只能由上方到达,如果一个位置的上方有障碍物,则它只能由左侧到达。如果上方左方没有障碍物,则可以从上方或者左方到达。路径的方法传递类似。
换句话说,障碍物会影响它右边和下边的路径。
python实现:
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
n = len(obstacleGrid)
m = len(obstacleGrid[0])
# dp[n][m]
dp = [[0 for _ in range(m)] for _ in range(n)]
dp[0][0] = 1
for i in range(n):
for j in range(m):
if obstacleGrid[i][j] == 1:
dp[i][j] = 0
else:
# 不是第一行
if i > 0:
dp[i][j] += dp[i-1][j]
# 不是第一列
if j > 0:
dp[i][j] += dp[i][j-1]
return dp[n-1][m-1]
给定一个只包含(
和)
的字符串,找出最长的包含有效括号的子串的长度。
示例:
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"
这个题目属于DP问题中非常复杂的。dp[i] 表示:以该字符结尾的最长有效括号的长度。
先将dp[] 初始化为0
以`(`结尾的子串对应的dp值必定为0
只考虑以`)`结尾的:
情况1:
s[i] = ‘)‘ and s[i-1] = ‘(‘, 则: dp[i] = dp[i-2] + 2
情况2:
s[i] = ‘)‘ and s[i-1] = ‘)‘
给定一个方形整数数组 A,我们想要得到通过 A 的下降路径的最小和。
下降路径可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列。
示例:
输入:[[1,2,3],[4,5,6],[7,8,9]]
输出:12
解释:
可能的下降路径有:
[1,4,7], [1,4,8], [1,5,7], [1,5,8], [1,5,9]
[2,4,7], [2,4,8], [2,5,7], [2,5,8], [2,5,9], [2,6,8], [2,6,9]
[3,5,7], [3,5,8], [3,5,9], [3,6,8], [3,6,9]
和最小的下降路径是 [1,4,7],所以答案是 12。
python实现:
class Solution:
def minFallingPathSum(self, A: List[List[int]]) -> int:
m = len(A)
n = len(A[0])
dp = [[0 for _ in range(m)] for _ in range(n)]
# dp[i][j] 表示 到(i,j)位置时的最小和
for i in range(n):
for j in range(m):
if i == 0:
dp[i][j] = A[i][j]
elif j == n-1:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + A[i][j]
elif j == 0:
dp[i][j] = min(dp[i-1][j], dp[i-1][j+1]) + A[i][j]
else:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1]) + A[i][j]
return min(dp[n-1])
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
python实现:
class Solution:
def waysToStep(self, n: int) -> int:
# 典型的dp问题
if n == 0:
return 0
elif n == 1:
return 1
elif n == 2:
return 2
elif n==3:
return 4
dp = [0] * (n+1)
dp[0] = 0
dp[1] = 1
dp[2] = 2
dp[3] = 4
for i in range(4, n+1):
dp[i] = (dp[i-3] + dp[i-2] + dp[i-1]) %1000000007
return dp[n]
这是最常规的解法,还有一些优化的小技巧:比如不用数组。
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
注意:本题相对原题稍作改动
思路:
定义dp[i][0]
:表示前i预约中,第i个预约不接的最长预约时间
定义dp[i][1]
:表示前i预约中,第i个预约接的最长预约时间
思路和算法
dp[i][0]=max(dp[i-1][0],dp[i-1][1])
dp[i][1]=dp[i?1][0]+nums
最后答案即为 max(dp[n][0], dp[n][1]), 其中n表示预约的个数。
再回来看状态转移方程,我们发现计算dp[i][0/1]时,只与前一个状态dp[i-1][0/1]有关,所以我们可以不用开数组,只用两个变量dp0、dp1,分别存储dp[i-1][0]和dp[i-1][1]的答案,然后去更新答案即可。
实现:
class Solution:
def massage(self, nums: List[int]) -> int:
n = len(nums)
if n == 0:
return 0
dp0, dp1 = 0, nums[0]
for i in range(1, n):
tdp0 = max(dp0, dp1) # 计算 dp[i][0]
tdp1 = dp0 + nums[i] # 计算 dp[i][1]
dp0, dp1 = tdp0, tdp1
return max(dp0, dp1)
基本概念掌握:每个节点的深度与它左右子树的深度有关,且等于其左右子树最大深度值加上 1。即:
maxDepth(root) = max(maxDepth(root.left),maxDepth(root.right)) + 1
其实我们上面用的递归方式,本质上是使用了DFS的思想。所以这里就可以引出什么是DFS:深度优先搜索算法(Depth First Search),对于二叉树而言,
它沿着树的深度遍历树的节点,尽可能深的搜索树的分支
这一过程一直进行到已发现从源节点可达的所有节点为止。
( 注意,这里的前提是对二叉树而言。DFS本身作为图算法的一种,在后续我会单独拉出来和回溯放一起讲。)
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
go实现:
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
return max(maxDepth(root.Left), maxDepth(root.Right)) + 1
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
二叉搜索树(Binary Search Tree),(又:二叉查找树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉搜索树。
删除二叉搜索树中的节点。
第450题:给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
这里有两个概念:前缀节点和后缀节点。
后缀节点是指大于本节点的最小节点。
前缀节点是指小于本节点的最大节点。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def successor(self, root):
"""
One step right and then always left
"""
root = root.right
while root.left:
root = root.left
return root.val
def predecessor(self, root):
"""
One step left and then always right
"""
root = root.left
while root.right:
root = root.right
return root.val
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
if not root:
return None
# delete from the right subtree
if key > root.val:
root.right = self.deleteNode(root.right, key)
# delete from the left subtree
elif key < root.val:
root.left = self.deleteNode(root.left, key)
# delete the current node
else:
# the node is a leaf
# 如果是叶子节点直接删除
if not (root.left or root.right):
root = None
# the node is not a leaf and has a right child
elif root.right:
# 将当前节点赋值为后缀节点
root.val = self.successor(root)
# 在右子树中删除这个后缀节点
root.right = self.deleteNode(root.right, root.val)
# the node is not a leaf, has no right child, and has a left child
else:
# 将当前节点赋值为前缀节点
root.val = self.predecessor(root)
# 在它的左子树中删除这个前缀节点
root.left = self.deleteNode(root.left, root.val)
return root
第110题:给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
思路:算出树的高度,判断差值是否为1。
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
if not root:
return True
if not self.isBalanced(root.left) or not self.isBalanced(root.right):
return False
l_depth = self.dfs(root.left) + 1
r_depth = self.dfs(root.right) + 1
if abs(l_depth-r_depth) > 1:
return False
return True
def dfs(self, root: TreeNode):
if not root:
return 0
if root.left is None and root.right is None:
return 1
l_depth = self.dfs(root.left) + 1
r_depth = self.dfs(root.right) + 1
return max(l_depth, r_depth)
给你一个二叉树,请你返回其按层序遍历得到的节点值。(即逐层地,从左到右访问所有节点)
我们先考虑本题的递归解法。想到递归,我们一般先想到DFS。我们可以对该二叉树进行先序遍历
(根左右的顺序),同时,记录节点所在的层次level,并且每一层都定义一个数组,然后将访问到的节点值放入对应层的数组中。
go实现:
func levelOrder(root *TreeNode) [][]int {
return dfs(root, 0, [][]int{})
}
func dfs(root *TreeNode, level int, res [][]int) [][]int {
if root == nil {
return res
}
if len(res) == level {
// 需要新加一层slice
res = append(res, []int{root.Val})
} else {
res[level] = append(res[level], root.Val)
}
res = dfs(root.Left, level+1, res)
res = dfs(root.Right, level+1, res)
return res
}
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
levels = []
if not root:
return levels
def helper(node, level):
# start the current level
if len(levels) == level:
levels.append([])
# append the current node value
levels[level].append(node.val)
# process child nodes for the next level
if node.left:
helper(node.left, level+1)
if node.right:
helper(node.right, level+1)
helper(root, 0)
return levels
给定一个二叉树,判断其是否是一个有效的二叉搜索树BST。
假设一个二叉搜索树具有如下特征:
思路:离不开递归思想。首先看完题目,我们很容易想到 遍历整棵树,比较所有节点,通过 左节点值<节点值,右节点值>节点值 的方式来进行求解。但是这种解法是错误的,因为对于任意一个节点,我们不光需要左节点值小于该节点,并且左子树上的所有节点值都需要小于该节点。(右节点一致)所以我们在此引入上界与下界,用以保存之前的节点中出现的最大值与最小值。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
if not root:
return True
return self.isBST(root, -2**32, 2**32)
def isBST(self, root: TreeNode, min: int, max: int) -> bool:
if root is None:
return True
if min >=root.val or max <=root.val:
return False
return self.isBST(root.left, min, root.val) and self.isBST(root.right, root.val, max)
go实现:
func isValidBST(root *TreeNode) bool {
if root == nil{
return true
}
return isBST(root,math.MinInt64,math.MaxInt64)
}
func isBST(root *TreeNode,min, max int) bool{
if root == nil{
return true
}
if min >= root.Val || max <= root.Val{
return false
}
// 左子树root.Val应该是最大值,右子树root.Val应该是最小值
return isBST(root.Left,min,root.Val) && isBST(root.Right,root.Val,max)
}
给定二叉搜索树(BST)的根节点和一个值。你需要在BST中找到节点值等于给定值的节点。返回以该节点为根的子树。如果节点不存在,则返回NULL。
比较简单,就是二叉搜素树的应用。
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
if not root:
return None
if val == root.val:
return root
elif val < root.val:
return self.searchBST(root.left, val)
else:
return self.searchBST(root.right, val)
go实现:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func searchBST(root *TreeNode, val int) *TreeNode {
if root == nil {
return root
}
if val == root.Val {
return root
}else if val < root.Val {
return searchBST(root.Left, val)
}else {
return searchBST(root.Right, val)
}
}
java迭代实现:
//迭代
public TreeNode searchBST(TreeNode root, int val) {
while (root != null) {
if (root.val == val) {
return root;
} else if (root.val > val) {
root = root.left;
} else {
root = root.right;
}
}
return null;
}
迭代与递归的区别
递归:重复调用函数自身实现循环称为递归;
迭代:利用变量的原值推出新值称为迭代,或者说迭代是函数内某段代码实现循环;
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h),h 为树的高度。
题解:
首先找到该删除节点,找到之后会出现三种情况:
go语言使用后继节点来实现:
func deleteNode(root *TreeNode, key int) *TreeNode {
if root == nil {
return nil
}
if key < root.Val {
root.Left = deleteNode( root.Left, key )
return root
}
if key > root.Val {
root.Right = deleteNode( root.Right, key )
return root
}
//到这里意味已经查找到目标
if root.Right == nil {
//右子树为空
return root.Left
}
if root.Left == nil {
//左子树为空
return root.Right
}
// 先取当前节点的右节点,然后一直取该节点的左节点,即比当前节点大的最小节点
minNode := root.Right
for minNode.Left != nil {
minNode = minNode.Left
}
root.Val = minNode.Val
root.Right = deleteMinNode( root.Right )
return root
}
func deleteMinNode( root *TreeNode ) *TreeNode {
if root.Left == nil {
pRight := root.Right
root.Right = nil
return pRight
}
root.Left = deleteMinNode( root.Left )
return root
}
python实现:
class Solution:
def successor(self, root):
"""
One step right and then always left
"""
root = root.right
while root.left:
root = root.left
return root.val
def predecessor(self, root):
"""
One step left and then always right
"""
root = root.left
while root.right:
root = root.right
return root.val
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
if not root:
return None
# delete from the right subtree
if key > root.val:
root.right = self.deleteNode(root.right, key)
# delete from the left subtree
elif key < root.val:
root.left = self.deleteNode(root.left, key)
# delete the current node
else:
# the node is a leaf
if not (root.left or root.right):
root = None
# the node is not a leaf and has a right child
elif root.right:
root.val = self.successor(root)
root.right = self.deleteNode(root.right, root.val)
# the node is not a leaf, has no right child, and has a left child
else:
root.val = self.predecessor(root)
root.left = self.deleteNode(root.left, root.val)
return root
给定一个二叉树,判断它是否是高度平衡的二叉树。
利用全局变量的一种解法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private boolean result = true;
public boolean isBalanced(TreeNode root) {
maxDepth(root);
return result;
}
public int maxDepth(TreeNode root){
if(root == null) return 0;
int left_deep = maxDepth(root.left);
int right_deep = maxDepth(root.right);
if (Math.abs(left_deep - right_deep) > 1) result = false;
return Math.max(left_deep, right_deep) + 1;
}
}
另一种解法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
if (!isBalanced(root.left)) return false;
if(!isBalanced(root.right)) return false;
int leftH = dfs(root.left) + 1;
int rightH = dfs(root.right) + 1;
if(Math.abs(leftH - rightH)>1){
return false;
}
else{
return true;
}
}
//计算二叉树的深度
public int dfs(TreeNode x){
if(x == null) return 0;
if(x.left == null && x.right == null) return 1;
int leftH = dfs(x.left) + 1;
int rightH = dfs(x.right) + 1;
return Math.max(leftH, rightH);
}
}
python解法:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
if not root:
return True
if not self.isBalanced(root.left) or not self.isBalanced(root.right):
return False
l_depth = self.dfs(root.left) + 1
r_depth = self.dfs(root.right) + 1
if abs(l_depth-r_depth) > 1:
return False
return True
# 计算出二叉树的高度
def dfs(self, root: TreeNode):
if not root:
return 0
if root.left is None and root.right is None:
return 1
l_depth = self.dfs(root.left) + 1
r_depth = self.dfs(root.right) + 1
return max(l_depth, r_depth)
给出一格完全二叉树,求出该树的节点个数。
说明:
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^h 个节点。
可以直接通过递归求出二叉树的个数:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def countNodes(self, root: TreeNode) -> int:
if root:
return 1 + self.countNodes(root.left) + self.countNodes(root.right)
else:
return 0
但是上述解法对所有二叉树都有效,没有充分利用完全二叉树的特性来简化计算。
由于题中已经告诉我们这是一颗完全二叉树,我们又已知了完全二叉树除了最后一层,其他层都是满的,并且最后一层的节点全部靠向了左边。那我们可以想到,可以将该完全二叉树可以分割成若干满二叉树和完全二叉树,满二叉树直接根据层高h计算出节点为2^h-1,*然后*继续计算子树中完全二叉树节点。那如何分割成若干满二叉树和完全二叉树呢?对任意一个子树,遍历其左子树层高left,右子树层高right,相等左子树则是满二叉树,否则右子树是满二叉树。
我们看到根节点的左右子树高度都为3,那么说明左子树是一颗满二叉树。因为节点已经填充到右子树了,左子树必定已经填满了。所以左子树的节点总数我们可以直接得到,是2^left - 1,加上当前这个root节点,则正好是2^3,即 8。然后只需要再对右子树进行递归统计即可。
我们看到左子树高度为3,右子树高度为2。说明此时最后一层不满,但倒数第二层已经满了,可以直接得到右子树的节点个数。同理,右子树节点+root节点,总数为2right,即22。再对左子树进行递归查找。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
int left = countLevel(root.left);
int right = countLevel(root.right);
if (left == right) {
return countNodes(root.right) + (1 << left);
} else {
return countNodes(root.left) + (1 << right);
}
}
//计算完全二叉树的高度
private int countLevel(TreeNode root) {
int level = 0;
while (root != null) {
level++;
root = root.left;
}
return level;
}
}
go实现:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func countNodes(root *TreeNode) int {
if root == nil {
return 0
}
left := countLevel(root.Left)
right := countLevel(root.Right)
if left == right {
return countNodes(root.Right) + (1<<left)
} else {
return countNodes(root.Left) + (1<<right)
}
}
func countLevel(root *TreeNode) int {
level := 0
for root != nil {
level++
root = root.Left
}
return level
}
给定二叉树根结点 root ,此外树的每个结点的值要么是 0,要么是 1。
返回移除了所有不包含 1 的子树的原二叉树。
( 节点 X 的子树为 X 本身,以及所有 X 的后代。)
假设有一棵树,最上层的是root节点,而父节点会依赖子节点。如果现在有一些节点已经标记为无效,我们要删除这些无效节点。如果无效节点的依赖的节点还有效,那么不应该删除,如果无效节点和它的子节点都无效,则可以删除。剪掉这些节点的过程,称为剪枝,目的是用来处理二叉树模型中的依赖问题。
剪什么大家应该都能理解。那关键是怎么剪?过程也很简单,在递归的过程中,如果当前结点的左右节点皆为空,且当前结点为0,我们就将当前节点剪掉即可。
go语言实现:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func pruneTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
root.Left = pruneTree(root.Left)
root.Right = pruneTree(root.Right)
if root.Left == nil && root.Right == nil && root.Val == 0 {
return nil
}
return root
}
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def pruneTree(self, root: TreeNode) -> TreeNode:
if root is None:
return root
root.left = self.pruneTree(root.left)
root.right = self.pruneTree(root.right)
if root.left == None and root.right == None and root.val == 0:
return None
return root
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean compareTree(TreeNode l, TreeNode r){
if(l==null && r==null) return true;
if(l==null || r==null) return false;
if(l.val != r.val) return false;
return compareTree(l.left, r.right) && compareTree(l.right, r.left);
}
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return compareTree(root.left, root.right);
}
}
也叫做:二叉树的镜像。
java实现:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
TreeNode left = root.left;
root.left = invertTree(root.right);
root.right = invertTree(left);
return root;
}
}
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def mirrorTree(self, root: TreeNode) -> TreeNode:
if root is None:
return root
# 中间变量保存临时值
left = root.left
root.left = self.mirrorTree(root.right)
root.right = self.mirrorTree(left)
return root
给定一个二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根节点。
思路:这个题目跟二叉树结点的深度有关。一定要理解一点:
每个节点的最大直径其实就是: 左子树深度 + 右子树深度
这样,我们把直径
这个概念,转换成了我们熟悉的深度
的概念。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int maxd = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return maxd;
}
public int depth(TreeNode node){
if(node == null) {
return 0;
}
int Left = depth(node.left);
int Right = depth(node.right);
// 每个节点的最大直径其实就是:左子树深度 + 右子树深度
maxd = Math.max(Left+Right, maxd); //将每个节点最大直径(左子树深度+右子树深度)当前最大值比较并取大者
return Math.max(Left, Right) + 1; //返回节点深度
}
}
给定一个二叉树,计算整个树的坡度。
一个树的节点的坡度定义即为,该节点左子树的结点之和和右子树结点之和的差的绝对值。空结点的的坡度是0。
整个树的坡度就是其所有节点的坡度之和。
输入:
1
/ 2 3
输出:1
解释:
结点 2 的坡度: 0
结点 3 的坡度: 0
结点 1 的坡度: |2-3| = 1
树的坡度 : 0 + 0 + 1 = 1
思路:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int ans = 0;
public int findTilt(TreeNode root) {
traverse(root);
return ans;
}
public int traverse(TreeNode node){
if (node == null){
return 0;
}
int left = traverse(node.left);
int right = traverse(node.right);
ans += Math.abs(left - right);
return left+right+node.val;
}
}
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def findTilt(self, root: TreeNode) -> int:
res = 0
def dfs(root):
nonlocal res
if not root: return 0
left = dfs(root.left)
right = dfs(root.right)
res += abs(left - right)
return root.val + left + right
dfs(root)
return res
如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树
。
只有给定的树是单值二叉树,才返回true,否则返回false。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isUnivalTree(self, root: TreeNode) -> bool:
if not root:
return True
left_correct, right_correct = False, False
if not root.left:
left_correct = True
elif root.val == root.left.val and self.isUnivalTree(root.left):
left_correct =True
if not root.right:
right_correct = True
elif root.val == root.right.val and self.isUnivalTree(root.right):
right_correct = True
return left_correct and right_correct
一定要理解好递归,否则这道题目还真不好实现。
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
示例:
你可以将以下二叉树:
1
/ 2 3
/ 4 5
序列化为 "[1,2,3,null,null,4,5]"
思考: 这道题目是考察将二叉树存储和反存储的实现。
序列化:例子给的序列化过程是二叉树的层序遍历(广度优先)过程,那我也用层序遍历吧,实际上,你可以采用任意方式,比如深度优先的方式,不会层序遍历可以做一做102题二叉树的层序遍历,我们利用队列依次将每一层的节点压入和弹出,保证了层序,压入过程压入该层节点的所有子节点,弹出过程弹出该层所有节点,因为这里只需要序列化,我们不必知道节点是哪一层的,所以只需要清空队列即可。
反序列化:反序列化的过程同样需要队列,这个过程是我们模拟序列化的过程,然后依次将val值给我们构建的二叉树节点。
留意函数输入和输出数据的类型:序列化输出字符串,反序列化输入为字符串,不是直接拿来就用,或者直接返回数组就行,要进行一次字符串和数组的转换。
python实现:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
if not root:
return "[]"
queue = collections.deque()
# 使用层次遍历
queue.append(root)
res = [] # 存储序列化后的结果
while queue:
node = queue.popleft()
if node:
res.append(str(node.val))
queue.append(node.left)
queue.append(node.right)
else:
res.append(‘null‘)
return ‘[‘ + ‘,‘.join(res) + ‘]‘
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
if data == ‘[]‘:
return None
# 去掉data最前面和最后面的[]
vals, i = data[1:-1].split(‘,‘), 1
root = TreeNode(int(vals[0]))
queue = collections.deque()
queue.append(root)
while queue:
node = queue.popleft()
if vals[i] != "null":
node.left = TreeNode(int(vals[i]))
queue.append(node.left)
i += 1
if vals[i] != "null":
node.right = TreeNode(int(vals[i]))
queue.append(node.right)
i += 1
return root
# Your Codec object will be instantiated and called as such:
# codec = Codec()
# codec.deserialize(codec.serialize(root))
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
层次遍历,python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
res = []
queue = collections.deque()
queue.append(root)
while queue:
node = queue.popleft()
if node:
res.append(node.val)
queue.append(node.left)
queue.append(node.right)
return res
很好地利用了队列。
从上到下打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
输入: [3,0,1]
输出: 2
输入: [9,6,4,2,3,5,7,0,1]
输出: 8
思路:
思路:对输出格式有了要求。
python实现1:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
res = []
queue = collections.deque()
queue.append(root)
while queue:
tmp = []
# 弹出每一层
for _ in range(len(queue)):
node = queue.popleft()
tmp.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
res.append(tmp)
return res
我们可以想到最朴素的方法是用一个二元组(node, level)
来表示状态,它表示某个节点和它所在的层数,每个新进队列的节点的level
值都是父节点的level+1。
python实现2:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
levels = []
if not root:
return levels
def helper(node, level):
# start the current level
if len(levels) == level:
levels.append([])
# append the current node value
levels[level].append(node.val)
# process child nodes for the next level
if node.left:
helper(node.left, level+1)
if node.right:
helper(node.right, level+1)
helper(root, 0)
return levels
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
res = []
queue = collections.deque()
queue.append(root)
flag = 0
while queue:
if flag == 0:
flag = 1
else:
flag = 0
tmp = []
# 弹出每一层
for _ in range(len(queue)):
node = queue.popleft()
tmp.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
if flag == 0:
tmp.reverse()
res.append(tmp)
return res
1038.给出二叉搜索树的根节点,该二叉树的节点值各不相同,修改二叉树,使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。
节点的右子树仅包含键 大于 节点键的节点。
左右子树也必须是二叉搜索树。
本题是关于二叉搜索树的问题,那我们第一想到的就是中序遍历,这是二叉搜索树的一个非常重要的性质,二叉搜索树的中序遍历是一个递增的有序序列。本道题我们需要将其转换为累加树,使得每个节点的值是原来的节点值加上所有大于它的节点值之和。那我们看下面的例子:
观察累加前中序遍历与累加后中序遍历,我们会发现,其实后者就是前者的一个从后的累加结果。那问题就迎刃而解了,我们只需反向中序遍历即可,并把每次的节点值进行累加,就能得到最终的累加树。而且这样保证了我们对每个节点只访问了一次。
python实现:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int sum = 0;
public TreeNode bstToGst(TreeNode root) {
if(root != null){
bstToGst(root.right);
sum = sum + root.val;
root.val = sum;
bstToGst(root.left);
}
return root;
}
}
这个题目要多看几个题解,彻底吃透它!
给定一个二叉树,返回所有从根节点到叶子节点的路径。
输入:
1
/ 2 3
5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
最直观的方法是使用递归。在递归遍历二叉树时,需要考虑当前的节点和它的孩子节点。如果当前的节点不是叶子节点,则在当前的路径末尾添加该节点,并递归遍历该节点的每一个孩子节点。如果当前的节点是叶子节点,则在当前的路径末尾添加该节点后,就得到了一条从根节点到叶子节点的路径,可以把该路径加入到答案中。
class Solution {
public void construct_paths(TreeNode root, String path, LinkedList<String> paths) {
if (root != null) {
path += Integer.toString(root.val);
if ((root.left == null) && (root.right == null)) // 当前节点是叶子节点
paths.add(path); // 把路径加入到答案中
else {
path += "->"; // 当前节点不是叶子节点,继续递归遍历
construct_paths(root.left, path, paths);
construct_paths(root.right, path, paths);
}
}
}
public List<String> binaryTreePaths(TreeNode root) {
LinkedList<String> paths = new LinkedList();
construct_paths(root, "", paths);
return paths;
}
}
python实现:
class Solution:
def binaryTreePaths(self, root):
"""
:type root: TreeNode
:rtype: List[str]
"""
def construct_paths(root, path):
if root:
path += str(root.val)
if not root.left and not root.right: # 当前节点是叶子节点
paths.append(path) # 把路径加入到答案中
else:
path += ‘->‘ # 当前节点不是叶子节点,继续递归遍历
construct_paths(root.left, path)
construct_paths(root.right, path)
paths = []
construct_paths(root, ‘‘)
return paths
给定二叉搜索树的根节点root
,返回L和R(含)之间的所有节点的值的和。
二叉搜索树保证具有唯一的值。
示例:
输入:root = [10,5,15,3,7,null,18], L = 7, R = 15
输出:32
输入:root = [10,5,15,3,7,13,18,1,null,6], L = 6, R = 10
输出:23
要考虑二叉搜索树的性质,BST, BST一定要考虑中序遍历。
java实现如下:
// 中序遍历
class Solution {
public int rangeSumBST(TreeNode root, int L, int R) {
if(root == null) return 0;
return helper(root, L, R);
}
public int helper(TreeNode root, int L, int R) {
if(root == null) return 0;
int sum = 0;
int left = helper(root.left, L, R);
if(root.val >= L && root.val <= R) {
sum += root.val;
}
int right = helper(root.right, L, R);
return sum + left + right;
}
}
python实现,有详细的解释:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def rangeSumBST(self, root: TreeNode, L: int, R: int) -> int:
# 定义一个内部函数,返回符合要求的数的和
def judge(root):
if not root:
return 0
total = 0
left = judge(root.left)
if root.val >= L and root.val <= R:
total += root.val
right = judge(root.right)
return total + left + right
return judge(root)
给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
注意:两个节点之间的路径长度由它们之间的边数表示。
class Solution(object):
def longestUnivaluePath(self, root):
self.ans = 0
def arrow_length(node):
if not node: return 0
left_length = arrow_length(node.left)
right_length = arrow_length(node.right)
left_arrow = right_arrow = 0
if node.left and node.left.val == node.val:
left_arrow = left_length + 1
if node.right and node.right.val == node.val:
right_arrow = right_length + 1
self.ans = max(self.ans, left_arrow + right_arrow)
return max(left_arrow, right_arrow)
arrow_length(root)
return self.ans
深刻理解递归。
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:你可以假设树中没有重复的元素。
思路: 先从前序遍历找到root根节点,根据root在中序遍历中找到当前root对应的左右子树,再递归对当前root的左右子树进行构造。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder or not inorder: # 递归终止条件
return
root = TreeNode(preorder[0]) # 先序为“根左右”,所以根据preorder可以确定root
idx = inorder.index(preorder[0]) # 中序为“左根右”,根据root可以划分出左右子树
# 下面递归对root的左右子树求解即可
root.left = self.buildTree(preorder[1:1 + idx], inorder[:idx])
root.right = self.buildTree(preorder[1 + idx:], inorder[idx + 1:])
return root
给定一个单链表,其中的元素按升序排序,将其转换成高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。
思路:一个简单的思路:
先把链表中的值记录在val_list列表中,再每次选取列表中间的元素构造节点,剩下的左边递归构造左子树,右边递归构造右子树。
class Solution:
def sortedListToBST(self, head: ListNode) -> TreeNode:
# 递归使用递增数组构造二叉搜索树
def builtTree(A):
n = len(A)
if n == 0:
return None
m = (n-1) // 2
root = TreeNode(A[m])
root.left = builtTree(A[:m])
root.right = builtTree(A[m+1:])
return root
val_list = []
while head:
val_list.append(head.val)
head = head.next
return builtTree(val_list[:])
第124题,给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
python实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
self.ans = -2**32
def one_side_max(root):
if not root:
return 0
left = max(0, one_side_max(root.left))
right = max(0, one_side_max(root.right))
self.ans = max(self.ans, left+right+root.val)
return max(left, right) + root.val
one_side_max(root)
return self.ans
本题中,想用maxPathSum()
直接递归(能简则简),但是发现行不通,因为始终需要一个外部变量参与比较,更新最大值。
定义dfs函数:计算当前子树能向父节点“提供”的最大路径和。即,一条从父节点延伸下来的路径,能在当前子树中获得的最大收益。
题目说,路径不一定经过根节点,说明最大路径和可能产生于局部子树中,如下图左一。
每次递归调用时,都求一下「当前子树内部的最大路径和」,与全局的最大比较。
二叉搜索树中的两个结点被错误地交换。
请在不改变其结构的情况下,恢复这棵树。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def recoverTree(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
nodes = []
def dfs(node):
if not node:
return
dfs(node.left)
nodes.append(node)
dfs(node.right)
dfs(root)
x = None
y = None
pre = nodes[0]
# 扫面遍历的结果,找出可能存在错误交换的节点x和y
for i in range(1, len(nodes)):
if pre.val > nodes[i].val:
y = nodes[i]
if not x:
x = pre
pre = nodes[i]
# 如果x和y不为空,则交换这两个节点值,恢复二叉搜索树
if x and y:
x.val, y.val = y.val, x.val
充分地利用了BST的性质,对BST进行中序遍历得到的是一个递增的序列。我们可以根据这个性质找到两个互换的节点值,最后进行交换即可。
给定一个N叉树,返回其节点值的前序遍历
。
"""
# Definition for a Node.
class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children
"""
class Solution:
def preorder(self, root: ‘Node‘) -> List[int]:
if not root:
return []
stack, output = [root, ], []
while stack:
root = stack.pop()
output.append(root.val)
stack.extend(root.children[::-1])
return output
标签:最小 哪些 最长上升子序列 index nfa 不用 打印 amp max
原文地址:https://www.cnblogs.com/kelvinchiang/p/13534059.html