> 文章列表 > LeetCode 2369. Check if There is a Valid Partition For The Array【记忆化搜索,动态规划】中等

LeetCode 2369. Check if There is a Valid Partition For The Array【记忆化搜索,动态规划】中等

LeetCode 2369. Check if There is a Valid Partition For The Array【记忆化搜索,动态规划】中等

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个下标从 0 开始的整数数组 nums ,你必须将数组划分为一个或多个 连续 子数组。

如果获得的这些子数组中每个都能满足下述条件 之一 ,则可以称其为数组的一种 有效 划分:

  1. 子数组  由 2 个相等元素组成,例如,子数组 [2,2] 。
  2. 子数组  由 3 个相等元素组成,例如,子数组 [4,4,4] 。
  3. 子数组  由 3 个连续递增元素组成,并且相邻元素之间的差值为 1 。例如,子数组 [3,4,5] ,但是子数组 [1,3,5] 不符合要求。

如果数组 至少 存在一种有效划分,返回 true ,否则,返回 false 。

示例 1:

输入:nums = [4,4,4,5,6]
输出:true
解释:数组可以划分成子数组 [4,4][4,5,6] 。
这是一种有效划分,所以返回 true

示例 2:

输入:nums = [1,1,1,2]
输出:false
解释:该数组不存在有效划分。

提示:

  • 2 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^6

解法1 记忆化搜索

对于 nums = [4,4,4,5,6] ,第一个子数组可以是 [4,4][4,4,4] 。去掉这段恢复的整数,例如去掉 [4,4] 后,剩下要解决的问题就是「判断 nums = [4,5,6] 是否存在有效划分」。这是一个和原问题相似的子问题,所以可以用递归解决

根据上面的讨论,递归参数只需要一个 idfs(i) 表示数组 nums[i:n-1] 是否存在有效划分。假设划分的这段子数组(必须符合题意)的结束下标为 j ,那么以该子数组开头的 nums[i:n-1] 是否存在有效划分,就要看 dfs(j + 1) 的返回值。

考虑「能划分的第一段子数组」的不同结束下标 j j j ,只要其中有一个 d f s ( j + 1 ) dfs(j + 1) dfs(j+1) t r u e true true ,则 d f s ( i ) dfs(i) dfs(i) 也为 t r u e true true ,否则为 f a l s e false false ,不存在有效划分。

  • 递归边界: d f s ( n ) = t r u e , d f s ( n − 1 ) = f a l s e dfs(n) = true, dfs(n - 1) = false dfs(n)=true,dfs(n1)=false
  • 递归入口: d f s ( 0 ) dfs(0) dfs(0) ,就是答案。

不难发现,整个递归中有大量重复递归调用(递归入参相同),由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化。这里用DP数组进行记忆,为了区分已经访问过的位置,我们额外用一个 vis 数组进行标记:

class Solution { 
public:bool validPartition(vector<int>& nums) { int n = nums.size();vector<bool> dp(n + 1); vector<bool> vis(n + 1);function<bool(int)> dfs = [&](int i) -> bool {  if (i >= n) return dp[n] = vis[i] = true;if (i == n - 1) { vis[i] = true; return dp[i] = false; }if (vis[i]) return dp[i];vis[i] = true; // 标记访问if (nums[i] == nums[i + 1]) dp[i] = dp[i] || dfs(i + 2);if (i + 2 < n && (nums[i] == nums[i + 1] && nums[i + 1] == nums[i + 2])) dp[i] = dp[i] || dfs(i + 3); if (i + 2 < n && (nums[i] + 1 == nums[i + 1] && nums[i + 1] + 1 == nums[i + 2]))dp[i] = dp[i] || dfs(i + 3); return dp[i];}; return dfs(0);}
};

解法2 动态规划

将上述代码改为递推形式即可,不用递归简单多了(逃:

class Solution { 
public:bool validPartition(vector<int>& nums) {  int n = nums.size();vector<bool> dp(n + 1); dp[n] = true, dp[n - 1] = false; for (int i = n - 2; i >= 0; --i) {if (nums[i] == nums[i + 1]) dp[i] = dp[i] || dp[i + 2];if (i + 2 < n && nums[i] == nums[i + 1] && nums[i + 1] == nums[i + 2])dp[i] = dp[i] || dp[i + 3]; if (i + 2 < n && nums[i] + 1 == nums[i + 1] && nums[i + 1] + 1 == nums[i + 2])dp[i] = dp[i] || dp[i + 3];}return dp[0];}
};

解法3 动态规划+滚动数组优化

在解法2的基础上,我们发现DP数组只需使用一部分空间,可以用滚动数组进行优化。需要注意的是,已使用过的某个 dp[i] 可能被置为了 true ,不能直接用 dp[i] 进行逻辑或。

class Solution { 
public:bool validPartition(vector<int>& nums) {  int n = nums.size();vector<bool> dp(4); dp[n % 4] = true, dp[(n % 4 + 3) % 4] = false; for (int i = n - 2; i >= 0; --i) {int r = i % 4; bool flag = false;if (nums[i] == nums[i + 1]) flag = flag || dp[(i + 2) % 4];if (i + 2 < n && nums[i] == nums[i + 1] && nums[i + 1] == nums[i + 2])flag = flag || dp[(i + 3) % 4]; if (i + 2 < n && nums[i] + 1 == nums[i + 1] && nums[i + 1] + 1 == nums[i + 2])flag = flag || dp[(i + 3) % 4];dp[r] = flag;}return dp[0];}
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)