您现在的位置是:首页 >学无止境 >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
,你必须将数组划分为一个或多个 连续 子数组。
如果获得的这些子数组中每个都能满足下述条件 之一 ,则可以称其为数组的一种 有效 划分:
- 子数组 恰 由
2
个相等元素组成,例如,子数组[2,2]
。 - 子数组 恰 由
3
个相等元素组成,例如,子数组[4,4,4]
。 - 子数组 恰 由
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]
是否存在有效划分」。这是一个和原问题相似的子问题,所以可以用递归解决。
根据上面的讨论,递归参数只需要一个 i
,dfs(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(n−1)=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)