动态规划算法模板和demo
366. 斐波纳契数列
查找斐波纳契数列中第 N 个数。
所谓的斐波纳契数列是指:
- 前2个数是 0 和 1 。
- 第 i 个数是第 i-1 个数和第i-2 个数的和。
斐波纳契数列的前10个数字是:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ...
样例
样例 1:
输入: 1
输出: 0
样例解释:
返回斐波那契的第一个数字,是0.
样例 2:
输入: 2
输出: 1
样例解释:
返回斐波那契的第二个数字是1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Solution: """ @param n: an integer @return: an ineger f(n) """ def fibonacci( self , n): # write your code here if n = = 1 : return 0 if n = = 2 : return 1 dp = [ 0 ] * n dp[ 1 ] = 1 for i in range ( 2 , n): dp[i] = dp[i - 1 ] + dp[i - 2 ] return dp[n - 1 ] |
最原始的DP!!!
在测试数据中第 N 个斐波那契数不会超过32位带符号整数的表示范围
纯用递归会超时,如果用带有记忆化的递归就可以,使用循环和记忆化递归的时间复杂度一样,都是O(n)。
优化:
1 2 3 4 5 6 7 | class Solution: def fibonacci( self , n): a = 0 b = 1 for i in range (n - 1 ): a, b = b, a + b return a |
近期(20230124)补充含子状态的dp! 309. 最佳买卖股票时机含冷冻期 详见:https://www.cnblogs.com/bonelee/p/17077984.html
322. 零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
示例 1:
[1, 2, 5]
11
3
示例 2:
[2]
3
示例 3:
输入:coins = [1], amount = 0 输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class Solution: def coinChange( self , coins: List [ int ], amount: int ) - > int : if amount = = 0 : return 0 inf = float ( 'inf' ) dp = [inf] * (amount + 1 ) coins.sort() for i in range ( 1 , amount + 1 ): for j in range ( len (coins)): if coins[j] > i: break if coins[j] = = i: dp[i] = 1 break dp[i] = min (dp[i], dp[coins[j]] + dp[i - coins[j]]) if dp[amount] = = inf: return - 1 return dp[amount] |
关键是注意2个for的正确设置!!!我原本两个for amount的,结果超时了!所以上面包含了剪纸的技巧。
337. 打家劫舍 III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1] 输出: 7 解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1] 输出: 9 解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
这个是属于树类dp,有两种解法,见:https://leetcode.cn/problems/house-robber-iii/solution/san-chong-fang-fa-jie-jue-shu-xing-dong-tai-gui-hu/
本质上是dfs,无非是返回节点标记了2个state!
1 2 3 4 5 6 7 8 9 10 11 12 | class Solution: def rob( self , root: Optional[TreeNode]) - > int : def dfs(root): if not root: return 0 , 0 l_steal, l_unsteal = dfs(root.left) r_steal, r_unsteal = dfs(root.right) unsteal = max (l_steal, l_unsteal) + max (r_steal, r_unsteal) steal = l_unsteal + r_unsteal + root.val return steal, unsteal return max (dfs(root)) |
110. 最小路径和
给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。
样例
样例 1:
输入: [[1,3,1],[1,5,1],[4,2,1]]
输出: 7
样例解释:
路线为: 1 -> 3 -> 1 -> 1 -> 1。
样例 2:
输入: [[1,3,2]]
输出: 6
解释:
路线是: 1 -> 3 -> 2
注意事项
你在同一时间只能向下或者向右移动一步
Dp[i][j] 存储从(0, 0) 到(i, j)的最短路径。
Dp[i][j] = min(Dp[i-1][j]), Dp[i][j-1]) + grid[i][j];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Solution: """ @param grid: a list of lists of integers @return: An integer, minimizes the sum of all numbers along its path """ def minPathSum( self , grid): # write your code here row, col = len (grid), len (grid[ 0 ]) dp = [[ 0 ] * col for i in range (row)] dp[ 0 ][ 0 ] = grid[ 0 ][ 0 ] for i in range ( 1 , row): dp[i][ 0 ] = grid[i][ 0 ] + dp[i - 1 ][ 0 ] for j in range ( 1 , col): dp[ 0 ][j] = grid[ 0 ][j] + dp[ 0 ][j - 1 ] for i in range ( 1 , row): for j in range ( 1 , col): dp[i][j] = min (dp[i - 1 ][j], dp[i][j - 1 ]) + grid[i][j] return dp[row - 1 ][col - 1 ] |
记得画图,二维矩阵。
dp的过程,初始化,状态转移方程:
空间优化:
滚动数组代码,java,考试不用这么肝,面试有明确思路即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public class Solution { / * * * @param grid: a list of lists of integers. * @ return : An integer, minimizes the sum of all numbers along its path * / public int minPathSum( int [][] A) { if (A = = null || A.length = = 0 || A[ 0 ].length = = 0 ) { return 0 ; } int m = A.length, n = A[ 0 ].length; int [][] f = new int [ 2 ][n]; int i, j; int old, now = 0 ; / / f[i] is stored in rolling array f[ 0 ] for (i = 0 ; i < m; + + i) { old = now; now = 1 - now; / / 0 - - > 1 , 1 - - > 0 for (j = 0 ; j < n; + + j) { if (i = = 0 && j = = 0 ) { f[now][j] = A[ 0 ][ 0 ]; continue ; } f[now][j] = Integer.MAX_VALUE; if (i > 0 ) { f[now][j] = Math. min (f[now][j], f[old][j]); } if (j > 0 ) { f[now][j] = Math. min (f[now][j], f[now][j - 1 ]); } f[now][j] + = A[i][j]; } } return f[now][n - 1 ]; } } |
其实滚动数组的代码还是很简单,画一个图就知道了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Solution: """ @param grid: a list of lists of integers @return: An integer, minimizes the sum of all numbers along its path """ def minPathSum( self , grid): # write your code here m, n = len (grid), len (grid[ 0 ]) dp = [ 0 ] * n dp[ 0 ] = grid[ 0 ][ 0 ] for j in range ( 1 , n): dp[j] = dp[j - 1 ] + grid[ 0 ][j] for i in range ( 1 , m): dp[ 0 ] = dp[ 0 ] + grid[i][ 0 ] for j in range ( 1 , n): dp[j] = min (dp[j - 1 ], dp[j]) + grid[i][j] return dp[n - 1 ] |
436. 最大正方形
在一个二维01矩阵中找到全为1的最大正方形, 返回它的面积.
样例
样例 1:
输入:
[
[1, 0, 1, 0, 0],
[1, 0, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 0, 0, 1, 0]
]
输出: 4
样例 2:
输入:
[
[0, 0, 0],
[1, 1, 1]
]
输出: 1
设定状态: dp[i][j] 表示以(i, j)为右下顶点的最大全1矩阵的边长.
状态转移方程:
if matrix[i][j] == 0
dp[i][j] = 0
else // 此时为dp[i-1][j-1], dp[i-1][j], dp[i][j-1] 确定的区域的最大全1矩阵
dp[i][j] = min{dp[i-1][j-1], dp[i-1][j], dp[i][j-1]} + 1 // 得到此方程需要一定推导, 纸笔画一下
边界: if i == 0 or j == 0: dp[i][j] = matrix[i][j]
答案: max{dp[i][j]}^2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Solution: """ @param grid: a matrix of 0 and 1 @return: an integer """ def maxSquare( self , grid): # write your code here row, col = len (grid), len (grid[ 0 ]) dp = [[ 0 ] * col for i in range (row)] for i in range ( 0 , row): dp[i][ 0 ] = grid[i][ 0 ] for j in range ( 0 , col): dp[ 0 ][j] = grid[ 0 ][j] for i in range ( 1 , row): for j in range ( 1 , col): if grid[i][j]: dp[i][j] = min (dp[i - 1 ][j - 1 ], dp[i - 1 ][j], dp[i][j - 1 ]) + 1 else : dp[i][j] = 0 max_edge = max ( max (dp[i]) for i in range (row)) return max_edge * max_edge |
也可以利用滚动数组优化。
也不复杂!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Solution: """ @param matrix: a matrix of 0 and 1 @return: an integer """ def maxSquare( self , matrix): # write your code here m, n = len (matrix), len (matrix[ 0 ]) dp = [[ 0 ] * n for i in range ( 2 )] ans = 0 for i in range (m): for j in range (n): if i = = 0 or j = = 0 : dp[i& 1 ][j] = matrix[i][j] else : if matrix[i][j]: dp[i& 1 ][j] = min (dp[(i - 1 )& 1 ][j - 1 ], dp[i& 1 ][j - 1 ], \ dp[(i - 1 )& 1 ][j]) + 1 else : dp[i& 1 ][j] = 0 ans = max (dp[i& 1 ][j] * * 2 , ans) return ans |
补充下最大矩形,比最大正方形要复杂:
动态规划8最大矩形(maximal-rectangle)
1. 问题描述
??给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
??示例:
??输入:
??[
?? [“1”,“0”,“1”,“0”,“0”],
?? [“1”,“0”,“1”,“1”,“1”],
?? [“1”,“1”,“1”,“1”,“1”],
?? [“1”,“0”,“0”,“1”,“0”]
??]
??输出: 6
2. 解法
2.1 暴力法
2.2 动态规划1【容易理解 掌握】
??我们定义一个dp数组,用于存储矩阵中以给定坐标为右下角的矩形的最大长度(每一行的更新可以视为row[i] = row[i - 1] + 1 if row[i] == ‘1’)。确定了每个点对应的最大长度,就可以在线性时间内计算出以该点为右下角的最大矩形。当我们遍历列时,可知从初始点到当前点矩形的最大高度,C++代码如下所示:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//解法2:动态规划1:时间复杂度O(N^2*M)[计算每个点的最大面积需O(N),全部N*M个点],空间复杂度O(NM)
int maximalRectangle(vector<vector<char>>& matrix) { if (matrix.size() == 0) return 0; int maxarea = 0; vector<vector<int>> dp(matrix.size(), vector<int>(matrix[0].size(), 0)); for (int i = 0; i < matrix.size(); i++) { for (int j = 0; j < matrix[0].size(); j++) { if (matrix[i][j] == '1') { //逐行更新该点对应的最大长度(横向) dp[i][j] = j == 0 ? 1 : dp[i][j - 1] + 1; int length = dp[i][j]; //自当前点竖直向上遍历得到不同的高度,在该高度下我们应该选这一列中较小的宽度 //毕竟短板才是能构成最大的。 for (int k = i; k >= 0; k--) { length = min(length, dp[k][j]); maxarea = max(maxarea, length * (i - k + 1)); } } } } return maxarea; } |
2.3 动态规划2【技巧性很强】
??考虑这样一个算法,对于每个点,我们先不断向上直到遇到零,然后向两边扩展,直到某列出现0。这种方式构建的矩形中必然存在最大矩形。
??我们通过定义三个数组height,left,right来记录每个点的高度,左边界和右边界,问题转化为如何更新每个数组。
??对于height数组,new_height[j]=old_height[j]+1 if row[j]==‘1’。
??对于left数组,new_left[j]=max(old_left[j],cur_left),其中cur_left是遇到的最右边的0的序号加1,向左扩展矩形时不能超过改点,否则会遇到0。
??对于right数组,new_right[j]=min(old_right[j],cur_right),cur_right表示我们遇到的最左边的0的序号。这里不减去1是因为这样就可以用height[j]*(right[j]-left[j])来计算矩形面积,也就是说矩形的底边由半开半闭区间[l,r)决定。为了记录正确的cur_right需要从右向左迭代,因此更新right时需要从右向左。C++代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
//解法3:动态规划2:时间复杂度O(NM)[],空间复杂度O(N)
int maximalRectangle(vector<vector<char>>& matrix) { if (matrix.size() == 0) return 0; int m = matrix.size(); int n = matrix[0].size(); //分别存储每一行上元素对应的最大矩形的高度以及左右区间 vector<int> left(n, 0);//最大的左边界为左端 vector<int> right(n, n);//最大的有边界为右端 vector<int> height(n, 0); int maxarea = 0; for (int i = 0; i < m; i++) { int cur_left = 0, cur_right = n; //更新高度,最后存储在height数组的就是每一列‘1’的个数 for (int j = 0; j < n; j++) { if (matrix[i][j] == '1') height[j]++; else height[j] = 0; } //更新左边界, for (int j = 0; j < n; j++) { if (matrix[i][j] == '1') left[j] = max(left[j], cur_left); else { left[j] = 0; cur_left = j + 1; } } //更新右边界 for (int j = n - 1; j >= 0; j--) { if (matrix[i][j] == '1') right[j] = min(right[j], cur_right); else { right[j] = n; cur_right = j; } } for (int j = 0; j < n; j++) { maxarea = max(maxarea, (right[j] - left[j]) * height[j]); } } return maxarea; } |
114. 不同的路径
有一个机器人的位于一个 m × n 个网格左上角。
机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。
问有多少条不同的路径?
样例
Example 1:
Input: n = 1, m = 3
Output: 1
Explanation: Only one path to target position.
Example 2:
Input: n = 3, m = 3
Output: 6
Explanation:
D : Down
R : Right
1) DDRR
2) DRDR
3) DRRD
4) RRDD
5) RDRD
6) RDDR
注意事项
n和m均不超过100
且答案保证在32位整数可表示范围内。
解法1:
数学模型:在n-1 + m-1长度的序列中,有n-1个D,和m-1个组成。
其中D表示向下,R表示向右。
因此满足组合数:C(n+m-2, n-1), 从n+m-2个位置中选n-1个位置放D的方案数。
解法2:
可以用Dp过程来求解:
Dp[i][j] 表示走到(i,j)的路径数,
考虑最后一步是从上往下走,还是从左往右走。
Dp[i][j] = Dp[i-1][j] + Dp[i][j-1];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Solution: # @return an integer """ def c(self, m, n): mp = {} for i in range(m): for j in range(n): if(i == 0 or j == 0): mp[(i, j)] = 1 else: mp[(i, j)] = mp[(i - 1, j)] + mp[(i, j - 1)] return mp[(m - 1, n - 1)] def uniquePaths(self, m, n): return self.c(m, n) """ def uniquePaths( self , m, n): dp = [[ 0 ] * n for i in range (m)] for i in range (m): dp[i][ 0 ] = 1 for j in range (n): dp[ 0 ][j] = 1 for i in range ( 1 , m): for j in range ( 1 , n): dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] return dp[m - 1 ][n - 1 ] |
200. 最长回文子串
给出一个字符串(假设长度最长为1000),求出它的最长回文子串,你可以假定只有一个满足条件的最长回文串。
样例
样例 1:
输入:"abcdzdcab"
输出:"cdzdc"
样例 2:
输入:"aba"
输出:"aba"
挑战
O(n2) 时间复杂度的算法是可以接受的,如果你能用 O(n) 的算法那自然更好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Solution: def longestPalindrome( self , s: str ) - > str : n = len (s) dp = [[ False ] * n for i in range (n)] ans = "" for i in range (n - 1 , - 1 , - 1 ): for j in range (i, n): if j = = i: dp[i][j] = True elif j = = i + 1 : dp[i][j] = s[i] = = s[j] else : if s[j] = = s[i]: dp[i][j] = dp[i + 1 ][j - 1 ] if dp[i][j] and len (ans) < (j - i + 1 ): ans = s[i:j + 1 ] return ans |
我的最新解法:2个for递增!!!记住下面的写法,几个if逻辑非常清晰!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Solution99: """ @param s: input string @return: the longest palindromic substring """ def longestPalindrome( self , s): def calc_dp(s): n = len (s) dp = [[ False ] * n for i in range (n)] for j in range (n): for i in range ( 0 , j + 1 ): if i = = j: dp[i][j] = True elif i = = j - 1 : dp[i][j] = s[i] = = s[j] else : if i < n - 1 and j > 0 and s[i] = = s[j]: dp[i][j] = dp[i + 1 ][j - 1 ] # print(i, j, dp[i][j]) return dp n = len (s) dp = calc_dp(s) ans = "" for i in range (n): for j in range (i + 1 , n): if dp[i][j] and len (ans) < (j - i + 1 ): ans = s[i:j + 1 ] return ans print (Solution99().longestPalindrome( "abcdzdcab" )) # |
我的老式解法,不建议再使用了,容易出错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class Solution: """ @param s: input string @return: the longest palindromic substring """ def longestPalindrome( self , s): # write your code here start,end = 0 , 0 n = len (s) def locatePalindrome(s, i, j): nonlocal start,end while i> = 0 and j<n: if s[i] = = s[j]: if end - start < j - i + 1 : start,end = i,j + 1 else : break i - = 1 j + = 1 for i in range (n): # center i locatePalindrome(s, i, i) # center i,i+1 locatePalindrome(s, i, i + 1 ) return s[start:end] |
最常规的解法。
使用dp的:
我们首先初始化一字母和二字母的回文,然后找到所有三字母回文,并依此类推…
注意为什么两个for一个递减一个递增???==》为了帮助理解,我来示意下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 举例说明: 字符串为:a b c d a X a d c 3 2 1 假设i,j位置如下: < - - - - - i j = i + 1 - - - - > a b c d a X a d c 3 2 1 为了知道d a X a d是不是回文?怎么办呢? 因为左右两边都是d,所以看a X a是不是回文对不对!!!那再往前追呢? 因为左右两边都是a,所以看X是不是回文。 而DP的本质就是用迭代的思路,上面反过来迭代就是DP了!!! 所以才得出来,i,j的方向是两边扩展!!! 因为一开始的时候,只知道单个字符是回文!如果是两个字符,则当两个字符相等的时候才是回文! 好了,上面 2 个是初始条件。 好了,开始迭代: a b c d a X a d c 3 2 1 - - - - - - - - - - - - - - - - - - - - - - i在这里最后一个位置 ,j越界,dp[i][i] = True a b c d a X a d c 3 2 1 - - - - - - - - - - - - - - - - - - - - i在这里,j在 1 的位置,dp[i][j] = False ,因为 2 ! = 1 a b c d a X a d c 3 2 1 - - - - - - - - - - - - - - - - - - i在这里,j在 2 的位置,dp[i][j] = False ,因为 3 ! = 2 a b c d a X a d c 3 2 1 - - - - - - - - - - - - - - - - - - i在这里,j在 1 的位置,dp[i][j] = False ,因为 3 ! = 1 |
==>哈哈哈,意识到本质了,两个for递增也可行的了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class Solution: """ @param s: input string @return: the longest palindromic substring """ def longestPalindrome( self , s): # write your code here # dp[i][j] = dp[i+1][j-1] + (s[i]==s[j]) if i<j n = len (s) dp = [[ False ] * n for i in range (n)] ans = s[ 0 ] for i in range (n): dp[i][i] = True if i < n - 1 and s[i] = = s[i + 1 ]: dp[i][i + 1 ] = True for i in range (n, - 1 , - 1 ): for j in range (i + 1 , n): if i < n - 1 and j > 0 : if s[i] = = s[j] and j ! = i + 1 : dp[i][j] = dp[i + 1 ][j - 1 ] for i in range (n): for j in range (i + 1 , n): if dp[i][j] and len (ans) < (j - i + 1 ): ans = s[i:j + 1 ] return ans |
换一种两个for递增的写法:
398. 最长上升连续子序列 II
给定一个整数矩阵. 找出矩阵中的最长连续上升子序列, 返回它的长度.
最长连续上升子序列可以从任意位置开始, 向上/下/左/右移动.
样例
样例 1:
输入:
[
[1, 2, 3, 4, 5],
[16,17,24,23,6],
[15,18,25,22,7],
[14,19,20,21,8],
[13,12,11,10,9]
]
输出: 25
解释: 1 -> 2 -> 3 -> 4 -> 5 -> ... -> 25 (由外向内螺旋)
样例 2:
输入:
[
[1, 2],
[5, 3]
]
输出: 5
解释: 1 -> 2 -> 3 -> 5
挑战
假定这是一个 N x M 的矩阵. 在 O(NM) 的时间复杂度和空间复杂度内解决这个问题.
• 循环(从小到大递推)
• 记忆化搜索(从大到小搜索)
这个题目要用dfs+记忆化搜索比较方便。
• 多重循环DP遇到的困难:
• 到从上到下循环不能解决问题
• 初始状态不好定义
• 那我们有没有可以比较暴力解决的方法呢? • DFS
动态规划, 设定状态 f[i][j] 表示矩阵中坐标 (i, j) 的点开始的最长上升子序列
状态转移方程:
int dx[4] = {0, 1, -1, 0};
int dy[4] = {1, 0, 0, -1};
f[i][j] = max{ f[i + dx[k]][j + dy[k]] + 1 }
k = 0, 1, 2, 3, matrix[i + dx[k]][j + dy[k]] > matrix[i][j]
这道题目可以向四个方向走, 所以推荐使用记忆化搜索(递归)的写法. 本质上就是dfs+cache!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | public class Solution { / * * * @param matrix: A 2D - array of integers * @ return : an integer * / int [][] dp; int n, m; public int longestContinuousIncreasingSubsequence2( int [][] A) { if (A.length = = 0 ) { return 0 ; } n = A.length; m = A[ 0 ].length; int ans = 0 ; dp = new int [n][m]; / / dp[i][j] means the longest continuous increasing path from (i,j) for ( int i = 0 ; i < n; + + i) { for ( int j = 0 ; j < m; + + j) { dp[i][j] = - 1 ; / / dp[i][j] has not been calculated yet } } for ( int i = 0 ; i < n; + + i) { for ( int j = 0 ; j < m; + + j) { search(i, j, A); ans = Math. max (ans, dp[i][j]); } } return ans; } int [] dx = { 1 , - 1 , 0 , 0 }; int [] dy = { 0 , 0 , 1 , - 1 }; void search( int x, int y, int [][] A) { if (dp[x][y] ! = - 1 ) { / / if dp[i][j] has been calculated, return directly return ; } int nx, ny; dp[x][y] = 1 ; for ( int i = 0 ; i < 4 ; + + i) { nx = x + dx[i]; ny = y + dy[i]; if (nx > = 0 && nx < n && ny > = 0 && ny < m) { if (A[nx][ny] > A[x][y]) { search(nx, ny, A); / / dp[nx][ny] must be calcuted dp[x][y] = Math. max (dp[x][y], dp[nx][ny] + 1 ); } } } } } |
什么时候用记忆化搜索? 1. 状态转移特别麻烦,不是顺序性
2. 初始化状态不是很容易找到
耗费更多空间,无法使用滚动数组优化 递归深度可能会很深
见:https://www.cnblogs.com/bonelee/p/17076086.html
特点:
1. 求一段区间的解max/min/count
2. 转移方程通过区间更新
3. 大区间的值依赖于小区间
168. 吹气球
https://leetcode.cn/problems/burst-balloons/
有n个气球,编号为0
到n-1
,每个气球都有一个分数,存在nums
数组中。每次吹气球i可以得到的分数为 nums[left] * nums[i] * nums[right]
,left和right分别表示i
气球相邻的两个气球。当i气球被吹爆后,其左右两气球即为相邻。要求吹爆所有气球,得到最多的分数。
样例
样例 1:
输入:[4, 1, 5, 10]
输出:270
解释:
nums = [4, 1, 5, 10] 吹爆 1, 得分 4 * 1 * 5 = 20
nums = [4, 5, 10] 吹爆 5, 得分 4 * 5 * 10 = 200
nums = [4, 10] 吹爆 4, 得分 1 * 4 * 10 = 40
nums = [10] 吹爆 10, 得分 1 * 10 * 1 = 10
总得分 20 + 200 + 40 + 10 = 270
样例 2:
输入:[3,1,5]
输出:35
解释:
nums = [3, 1, 5] 吹爆 1, 得分 3 * 1 * 5 = 15
nums = [3, 5] 吹爆 3, 得分 1 * 3 * 5 = 15
nums = [5] 吹爆 5, 得分 1 * 5 * 1 = 5
总得分 15 + 15 + 5 = 35
注意事项
- 你可以假设nums[-1] = nums[n] = 1。-1和n位置上的气球不真实存在,因此不能吹爆它们。
- 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
dp[i][j]含义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Solution: def maxCoins( self , nums: List [ int ]) - > int : n = len (nums) rec = [[ 0 ] * (n + 2 ) for _ in range (n + 2 )] val = [ 1 ] + nums + [ 1 ] for i in range (n - 1 , - 1 , - 1 ): for j in range (i + 2 , n + 2 ): for k in range (i + 1 , j): total = val[i] * val[k] * val[j] total + = rec[i][k] + rec[k][j] rec[i][j] = max (rec[i][j], total) return rec[ 0 ][n + 1 ] |
注意n是递减的,因为dp[i][j] = dp[i][k] + dp[k][j] k比i大,所以i如果递增就取不到后面dp[k][j]的值!
1 2 3 4 5 6 7 8 9 10 11 12 | class Solution: def maxCoins( self , nums: List [ int ]) - > int : n = len (nums) dp = [[ 0 ] * (n + 2 ) for i in range (n + 2 )] nums = [ 1 ] + nums + [ 1 ] for i in range (n + 1 , - 1 , - 1 ): for j in range (i + 2 , n + 2 ): for k in range (i + 1 , j): dp[i][j] = max (dp[i][j], dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]) return dp[ 0 ][n + 1 ] |
396. 硬币排成线 III
有 n
个硬币排成一条线, 第 i
枚硬币的价值为 values[i]
.
两个参赛者轮流从任意一边取一枚硬币, 直到没有硬币为止. 拿到硬币总价值更高的获胜.
请判定 第一个玩家 会赢还是会输.
样例
样例 1:
输入: [3, 2, 2]
输出: true
解释: 第一个玩家在刚开始的时候拿走 3, 然后两个人分别拿到一枚 2.
样例 2:
输入: [1, 20, 4]
输出: false
解释: 无论第一个玩家在第一轮拿走 1 还是 4, 第二个玩家都可以拿到 20.
挑战
在 n
是偶数时做到O(1) 空间, O(n) 时间
区间动态规划问题, 具体定义状态的方式有很多种, 但是大同小异, 时空复杂度都相似. 这里只介绍 version 1 的具体实现.
设定 dp[i][j] 表示当前剩余硬币的区间为 [i, j] 时, 此时该拿硬币的人能获取的最大值.
注意, dp[i][j] 并没有包含角色信息, dp[0][values.length - 1] 表示的是先手的人能获得的最大值, 而 dp[1][values.length -1] 表示的则是后手的人能获得的最大值. 需要这样做是因为: 两个人都会采用最优策略.
当前的人的决策就是拿左边还是拿右边, 而下一个人仍然会最优决策, 所以应该是最小值中取最大值:
dp[i][j] = max( // 取max表示当前的人选用最优策略
min(dp[i + 2][j], dp[i + 1][j - 1]) + values[i], // 取min表示下一个人选用最优策略
min(dp[i][j - 2], dp[i + 1][j - 1]) + values[j]
)
几个边界:
i > j : dp[i][j] = 0
i == j : dp[i][j] = values[i]
i + 1 == j : dp[i][j] = max(values[i], values[j])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Solution: # @param values: a list of integers # @return: a boolean which equals to True if the first player will win def firstWillWin( self , values): if not values: return False n = len (values) dp = [[ 0 ] * n for _ in range (n)] sum = [[ 0 ] * n for _ in range (n)] for i in range (n): dp[i][i] = values[i] sum [i][i] = values[i] for i in range (n - 2 , - 1 , - 1 ): # n-2 => 0 for j in range (i + 1 , n): # i+1 => n-1 sum [i][j] = sum [i + 1 ][j] + values[i] dp[i][j] = sum [i][j] - min (dp[i + 1 ][j], dp[i][j - 1 ]) return dp[ 0 ][n - 1 ] > sum [ 0 ][n - 1 ] - dp[ 0 ][n - 1 ] |
区间类的动态规划还是挺难的!!!看情况掌握吧。
双序列动态规划
77. 最长公共子序列
给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。
样例
样例 1:
输入: "ABCD" and "EDCA"
输出: 1
解释:
LCS 是 'A' 或 'D' 或 'C'
样例 2:
输入: "ABCD" and "EACB"
输出: 2
解释:
LCS 是 "AC"
说明
最长公共子序列的定义:
- 最长公共子序列问题是在一组序列(通常2个)中找到最长公共子序列(注意:不同于子串,LCS不需要是连续的子串)。该问题是典型的计算机科学问题,是文件差异比较程序的基础,在生物信息学中也有所应用。
- https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Solution: """ @param A: A string @param B: A string @return: The length of longest common subsequence of A and B """ def longestCommonSubsequence( self , A, B): # write your code here if not A or not B: return 0 m, n = len (A), len (B) dp = [[ 0 ] * n for i in range (m)] for i in range ( 0 , m): if A[i] = = B[ 0 ]: dp[i][ 0 ] = 1 for j in range ( 0 , n): if A[ 0 ] = = B[j]: dp[ 0 ][j] = 1 for i in range ( 1 , m): for j in range ( 1 , n): if A[i] = = B[j]: dp[i][j] = max (dp[i - 1 ][j - 1 ] + 1 , dp[i][j - 1 ], dp[i - 1 ][j]) else : dp[i][j] = max (dp[i][j - 1 ], dp[i - 1 ][j]) return dp[m - 1 ][n - 1 ] |
记得画个二维的图!!!
Dp[i][j] 表示A序列前i个数,与B的前j个数的LCS长度。
对A的每个位置i,枚举B的每个位置j。
更精简的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Solution: """ @param A, B: Two strings. @return: The length of longest common subsequence of A and B. """ def longestCommonSubsequence( self , A, B): n, m = len (A), len (B) f = [[ 0 ] * (n + 1 ) for i in range (m + 1 )] for i in range (n): for j in range (m): f[i + 1 ][j + 1 ] = max (f[i][j + 1 ], f[i + 1 ][j]) if A[i] = = B[j]: f[i + 1 ][j + 1 ] = f[i][j] + 1 return f[n][m] |
备注:也可以使用滚动数组进行优化!!!
119. 编辑距离
给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。
你总共三种操作方法:
- 插入一个字符
- 删除一个字符
- 替换一个字符
样例
样例 1:
输入:
"horse"
"ros"
输出: 3
解释:
horse -> rorse (替换 'h' 为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
样例 2:
输入:
"intention"
"execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (替换 'i' 为 'e')
enention -> exention (替换 'n' 为 'x')
exention -> exection (替换 'n' 为 'c')
exection -> execution (插入 'u')
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Solution: """ @param A: A string @param B: A string @return: The minimum number of steps. """ def minDistance( self , A, B): # write your code here m, n = len (A), len (B) dp = [[ 0 ] * (n + 1 ) for i in range (m + 1 )] for i in range (m + 1 ): dp[i][ 0 ] = i for j in range (n + 1 ): dp[ 0 ][j] = j for i in range ( 1 , m + 1 ): for j in range ( 1 , n + 1 ): if A[i - 1 ] = = B[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] else : dp[i][j] = min (dp[i - 1 ][j - 1 ], dp[i][j - 1 ], dp[i - 1 ][j]) + 1 return dp[m][n] |
dp[i][j] 代表第一个字符串以i结尾匹配上(编辑成)第二个字符串以j结尾的字符串,最少需要多少次编辑。
通过去判断i与j的匹配关系来变为更小的状态。
字符串相似性!!!
10. 正则表达式匹配
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s
的,而不是部分字符串。
示例 1:
输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "a*" 输出:true 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:s = "ab", p = ".*" 输出:true 解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
核心是对于.*场景 匹配2个以上的状态转移推导!如下:
摘录自:https://leetcode.cn/problems/regular-expression-matching/solution/by-flix-musv/

代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Solution: def isMatch( self , s: str , p: str ) - > bool : m, n = len (s), len (p) dp = [[ False ] * (n + 1 ) for _ in range (m + 1 )] dp[ 0 ][ 0 ] = True for j in range ( 2 , n + 1 ): # 这里容易踩坑,不要漏掉了 if p[j - 1 ] = = '*' : dp[ 0 ][j] = dp[ 0 ][j - 2 ] # 下面就是公式转换了 for i in range ( 1 , m + 1 ): for j in range ( 1 , n + 1 ): if p[j - 1 ] = = '*' : if p[j - 2 ] ! = '.' : if p[j - 2 ] ! = s[i - 1 ]: dp[i][j] = dp[i][j - 2 ] else : dp[i][j] = dp[i][j - 2 ] or dp[i - 1 ][j] else : dp[i][j] = dp[i][j - 2 ] or dp[i - 1 ][j] elif p[j - 1 ] = = '.' : dp[i][j] = dp[i - 1 ][j - 1 ] else : if p[j - 1 ] = = s[i - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] return dp[m][n] |
精简点可以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class Solution: def isMatch( self , s: str , p: str ) - > bool : m, n = len (s), len (p) dp = [[ False ] * (n + 1 ) for _ in range (m + 1 )] # 初始化 dp[ 0 ][ 0 ] = True for j in range ( 1 , n + 1 ): if p[j - 1 ] = = '*' : dp[ 0 ][j] = dp[ 0 ][j - 2 ] # 状态更新 for i in range ( 1 , m + 1 ): for j in range ( 1 , n + 1 ): if s[i - 1 ] = = p[j - 1 ] or p[j - 1 ] = = '.' : dp[i][j] = dp[i - 1 ][j - 1 ] elif p[j - 1 ] = = '*' : # 【题目保证'*'号不会是第一个字符,所以此处有j>=2】 if s[i - 1 ] ! = p[j - 2 ] and p[j - 2 ] ! = '.' : dp[i][j] = dp[i][j - 2 ] else : dp[i][j] = dp[i][j - 2 ] | dp[i - 1 ][j] return dp[m][n] |
总之,这个题目还是有难度的,公式推导也有难度,尤其是匹配2个以上的情形。
29. 交叉字符串
给出三个字符串:s1、s2、s3,判断s3是否由s1和s2交叉构成。
样例
样例 1:
输入:
"aabcc"
"dbbca"
"aadbbcbcac"
输出:
true
样例 2:
输入:
""
""
"1"
输出:
false
样例 3:
输入:
"aabcc"
"dbbca"
"aadbbbaccc"
输出:
false
挑战
要求时间复杂度为O(n2)或者更好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class Solution: """ @param A: A string @param B: A string @param C: A string @return: Determine whether s3 is formed by interleaving of s1 and s2 """ def isInterleave( self , A, B, C): # write your code here m, n = len (A), len (B) if m + n ! = len (C): return False dp = [[ False ] * (n + 1 ) for i in range (m + 1 )] dp[ 0 ][ 0 ] = True for i in range ( 1 , m + 1 ): if A[i - 1 ] = = C[i - 1 ]: dp[i][ 0 ] = dp[i - 1 ][ 0 ] for j in range ( 1 , n + 1 ): if B[j - 1 ] = = C[j - 1 ]: dp[ 0 ][j] = dp[ 0 ][j - 1 ] for i in range ( 1 , m + 1 ): for j in range ( 1 , n + 1 ): if A[i - 1 ] = = C[i + j - 1 ]: dp[i][j] = dp[i - 1 ][j] if B[j - 1 ] = = C[i + j - 1 ]: dp[i][j] | = dp[i][j - 1 ] return dp[m][n] |
dp[i][j]代表由s1的前i个字母和s2的前j个字母是否能构成当前i+j个字母。
然后状态转移即可。(看第i+j+1个是否能被s1的第i+1个构成或被s2的第j+1个构成)
623. K步编辑
给出一个只含有小写字母的字符串的集合以及一个目标串,输出所有可以经过不多于 k
次操作得到目标字符串的字符串。
你可以对字符串进行一下的3种操作:
-
加入1个字母
-
删除1个字母
-
替换1个字母
样例
样例 1:
给出字符串 `["abc","abd","abcd","adc"]`,目标字符串为 `"ac"` ,k = `1`
返回 `["abc","adc"]`
输入:
["abc", "abd", "abcd", "adc"]
"ac"
1
输出:
["abc","adc"]
解释:
"abc" 去掉 "b"
"adc" 去掉 "d"
样例 2:
输入:
["acc","abcd","ade","abbcd"]
"abc"
2
输出:
["acc","abcd","ade","abbcd"]
解释:
"acc" 把 "c" 变成 "b"
"abcd" 去掉 "d"
"ade" 把 "d" 变成 "b"把 "e" 变成 "c"
"abbcd" 去掉 "b" 和 "d"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | class TrieNode: def __init__( self ): # Initialize your data structure here. self .children = [ None for i in range ( 26 )] self .hasWord = False self . str = None @classmethod def addWord( cls , root, word): node = root for letter in word: child = node.children[ ord (letter) - ord ( 'a' )] if child is None : child = TrieNode() node.children[ ord (letter) - ord ( 'a' )] = child node = child node.hasWord = True node. str = word class Solution: # @param {string[]} words a set of strings # @param {string} target a target string # @param {int} k an integer # @return {string[]} output all the stirngs that meet the requirements def kDistance( self , words, target, k): # Write your code here root = TrieNode() for word in words: TrieNode.addWord(root, word) result = [] n = len (target) dp = [i for i in range (n + 1 )] self .find(root, result, k, target, dp) return result def find( self , node, result, k, target, dp): n = len (target) if node.hasWord and dp[n] < = k: result.append(node. str ) next = [ 0 for i in range (n + 1 )] for i in range ( 26 ): if node.children[i] is not None : next [ 0 ] = dp[ 0 ] + 1 for j in range ( 1 , n + 1 ): if ord (target[j - 1 ]) - ord ( 'a' ) = = i: next [j] = min (dp[j - 1 ], min ( next [j - 1 ] + 1 , dp[j] + 1 )) else : next [j] = min (dp[j - 1 ] + 1 , min ( next [j - 1 ] + 1 , dp[j] + 1 )) self .find(node.children[i], result, k, target, next ) |
挺难的题目,使用了Trie,结合DP。

背包类DP
92. 背包问题
在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]
样例
样例 1:
输入: [3,4,8,5], backpack size=10
输出: 9
样例 2:
输入: [2,3,5,7], backpack size=12
输出: 12
挑战
O(n x m) time and O(m) memory.
O(n x m) memory is also acceptable if you do not know how to optimize memory.
注意事项
你不可以将物品进行切割。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Solution: """ @param m: An integer m denotes the size of a backpack @param A: Given n items with size A[i] @return: The maximum size """ def backPack( self , m, A): # write your code here # f[i][j]表示前i个物品选一些物品放入容量为j的背包中能否放满。 n = len (A) f = [[ False ] * (m + 1 ) for _ in range (n + 1 )] f[ 0 ][ 0 ] = True for i in range ( 1 , n + 1 ): f[i][ 0 ] = True for j in range ( 1 , m + 1 ): if j > = A[i - 1 ]: f[i][j] = f[i - 1 ][j] or f[i - 1 ][j - A[i - 1 ]] else : f[i][j] = f[i - 1 ][j] for i in range (m, - 1 , - 1 ): if f[n][i]: return i return 0 |
自己画一个二维的矩阵图,推演下。
125. 背包问题 II
有 n
个物品和一个大小为 m
的背包. 给定数组 A
表示每个物品的大小和数组 V
表示每个物品的价值.
问最多能装入背包的总价值是多大?
样例
样例 1:
输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
样例 2:
输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10
挑战
O(nm) 空间复杂度可以通过, 不过你可以尝试 O(m) 空间复杂度吗?
注意事项
A[i], V[i], n, m
均为整数- 你不能将物品进行切分
- 你所挑选的要装入背包的物品的总大小不能超过
m
- 每个物品只能取一次
设定 f[i][j] 表示前 i 个物品装入大小为 j 的背包里, 可以获取的最大价值总和. 决策就是第i个物品装不装入背包, 所以状态转移方程就是 f[i][j] = max(f[i - 1][j], f[i - 1][j - A[i]] + V[i])
1 2 3 4 5 6 7 8 9 10 11 | class Solution: # @param m: An integer m denotes the size of a backpack # @param A & V: Given n items with size A[i] and value V[i] def backPackII( self , m, A, V): # write your code here f = [ 0 for i in range (m + 1 )] n = len (A) for i in range (n): for j in range (m, A[i] - 1 , - 1 ): f[j] = max (f[j] , f[j - A[i]] + V[i]) return f[m] |
440. 背包问题 III
给定 n
种物品, 每种物品都有无限个. 第 i
个物品的体积为 A[i]
, 价值为 V[i]
.
再给定一个容量为 m
的背包. 问可以装入背包的最大价值是多少?
样例
样例 1:
输入: A = [2, 3, 5, 7], V = [1, 5, 2, 4], m = 10
输出: 15
解释: 装入三个物品 1 (A[1] = 3, V[1] = 5), 总价值 15.
样例 2:
输入: A = [1, 2, 3], V = [1, 2, 3], m = 5
输出: 5
解释: 策略不唯一. 比如, 装入五个物品 0 (A[0] = 1, V[0] = 1).
注意事项
- 不能将一个物品分成小块.
- 放入背包的物品的总大小不能超过
m
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Solution: # @param {int[]} A an integer array # @param {int[]} V an integer array # @param {int} m an integer # @return {int} an array def backPackIII( self , A, V, m): # Write your code here f = [ 0 for i in range (m + 1 )] for (a, v) in zip (A, V): for j in range (a, m + 1 ): if f[j - a] + v > f[j]: f[j] = f[j - a] + v return f[m] |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2017-12-01 dns tunnel C&C
2016-12-01 linux tcpdump 抓包
2016-12-01 Asterisk——part 1
2016-12-01 spark出现task不能序列化错误的解决方法 org.apache.spark.SparkException: Task not serializable