题目一
659. 分割数组为连续子序列
给你一个按升序排序的整数数组 num(可能包含重复数字),请你将它们分割成一个或多个子序列,其中每个子序列都由连续整数组成且长度至少为 3 。
如果可以完成上述分割,则返回 true ;否则,返回 false 。
示例 1:
输入: [1,2,3,3,4,5]
输出: True
解释:
你可以分割出这样两个连续子序列 :
1, 2, 3
3, 4, 5
示例 2:
输入: [1,2,3,3,4,4,5,5]
输出: True
解释:
你可以分割出这样两个连续子序列 :
1, 2, 3, 4, 5
3, 4, 5
示例 3:
输入: [1,2,3,4,4,5]
输出: False
提示:
输入的数组长度范围为 [1, 10000]
我没做出来T T…
官方方法一:哈希表 + 最小堆
class Solution {
public boolean isPossible(int[] nums) {
Map<Integer, PriorityQueue<Integer>> map = new HashMap<Integer, PriorityQueue<Integer>>();
for (int x : nums) {
if (!map.containsKey(x)) {
map.put(x, new PriorityQueue<Integer>());
}
if (map.containsKey(x - 1)) {
int prevLength = map.get(x - 1).poll();
if (map.get(x - 1).isEmpty()) {
map.remove(x - 1);
}
map.get(x).offer(prevLength + 1);
} else {
map.get(x).offer(1);
}
}
Set<Map.Entry<Integer, PriorityQueue<Integer>>> entrySet = map.entrySet();
for (Map.Entry<Integer, PriorityQueue<Integer>> entry : entrySet) {
PriorityQueue<Integer> queue = entry.getValue();
if (queue.peek() < 3) {
return false;
}
}
return true;
}
}
复杂度分析
-
时间复杂度:O(nlogn),其中 n 是数组的长度。
需要遍历数组,对于数组中的每个数,都要对哈希表和最小堆进行更新。每个数对应的最小堆的长度不超过 n,因此每次对最小堆的操作的时间复杂度是 O(logn),数组长度为 n,因此时间复杂度是 O(nlogn)。
然后需要遍历哈希表中的每一条记录,判断是否满足每个子序列的长度都不小于 3,子序列的数量不会超过 n,因此时间复杂度是 O(n)。
因此总时间复杂度是 O(nlogn)。 -
空间复杂度:O(n),其中 n 是数组的长度。需要使用哈希表和最小堆存储以每个数结尾的各个子序列的长度,哈希表和最小堆中的元素数量不会超过数组的长度。
官方方法二:贪心
class Solution {
public boolean isPossible(int[] nums) {
Map<Integer, Integer> countMap = new HashMap<Integer, Integer>();
Map<Integer, Integer> endMap = new HashMap<Integer, Integer>();
for (int x : nums) {
int count = countMap.getOrDefault(x, 0) + 1;
countMap.put(x, count);
}
for (int x : nums) {
int count = countMap.getOrDefault(x, 0);
if (count > 0) {
int prevEndCount = endMap.getOrDefault(x - 1, 0);
if (prevEndCount > 0) {
countMap.put(x, count - 1);
endMap.put(x - 1, prevEndCount - 1);
endMap.put(x, endMap.getOrDefault(x, 0) + 1);
} else {
int count1 = countMap.getOrDefault(x + 1, 0);
int count2 = countMap.getOrDefault(x + 2, 0);
if (count1 > 0 && count2 > 0) {
countMap.put(x, count - 1);
countMap.put(x + 1, count1 - 1);
countMap.put(x + 2, count2 - 1);
endMap.put(x + 2, endMap.getOrDefault(x + 2, 0) + 1);
} else {
return false;
}
}
}
}
return true;
}
}
复杂度分析
时间复杂度:O(n),其中 n 是数组的长度。需要遍历数组两次,对于数组中的每个元素,更新哈希表的时间复杂度是常数。
空间复杂度:O(n),其中 n 是数组的长度。需要使用两个哈希表,每个哈希表的大小都不会超过 n。
最优贪心
class Solution {
public boolean isPossible(int[] nums) {
int n = nums.length;
int dp1 = 0; // 长度为1的子序列数目
int dp2 = 0; // 长度为2的子序列数目
int dp3 = 0; // 长度>=3的子序列数目
int idx = 0;
int start = 0; // 起始位置
while(idx < n){
start = idx; // 重新将起始位置赋值
int x = nums[idx];
while(idx < n && nums[idx] == x){ // 去掉所有和x重复的元素
idx++;
}
int cnt = idx - start;
if(start > 0 && x != nums[start - 1] + 1){ // 对于nums[idx] != nums[idx - 1] + 1
if(dp1 + dp2 > 0){ // 如果 dp1+dp2>0,说明有些长度≤2的序列无法被满足,因此不存在相应的分割方案。
return false;
}else{ // 否则,此前的序列全部作废
dp1 = cnt;
dp2 = dp3 = 0;
}
}else{ // 对于nums[idx] == nums[idx - 1] + 1
// 说明当前整数可以加入到所有以nums[idx-1]为结尾的序列中。假设数组中x的数目为cnt:
if(dp1 + dp2 > cnt){ // 首先,根据贪心的策略,我们要尽可能地先把 xx 添加到长度≤2 的子序列中,从而尽可能地保证子序列的长度都≥3。如果x的数量不够,说明不存在相应的分割方案。
return false;
}
int left = cnt - dp1 - dp2; // 此时 还剩下left = cnt -dp1 -dp2个 nums[idx-1](x)
int keep = Math.min(dp3,left); // 尽量将余下的left个整数添加到长度≥3的子序列中
// 最后,我们更新 dp1,dp2,dp3的取值
dp3 = keep + dp2;
dp2 = dp1;
dp1 = left - keep; // 开启 新序列的数目等于left−keep。
}
}
return dp1 == 0 && dp2 == 0;
}
}
最优贪心解法
- 时间复杂度是 O(N)。
- 空间复杂度是 O(1)。
详细大佬思路链接:https://leetcode-cn.com/problems/split-array-into-consecutive-subsequences/solution/tan-xin-o1-kong-jian-fu-za-du-de-zui-you-jie-fa-by/ (C++)
1ms不错的答案
class Solution {
public boolean isPossible(int[] nums) {
int n = nums.length, i = 0;
if (n < 3) return false;
// 当前长度为1,2的序列的数量,当前能用的多于2的序列数量
int one = 0, two = 0, more = 0;
// 初始化one
while (i < n && nums[i] == nums[0]) {
one++;
i++;
}
while (i < n) {
int pre = nums[i - 1];
// 下一个数与上一个数断了的情况,即count为0的特殊情况
if (nums[i] - pre > 1) {
// 有one或two则直接返回false
if (one > 0 || two > 0) {
return false;
}
// more全部断
more = 0;
int start = nums[i];
// 初始化one
while (i < n && nums[i] == start) {
one++;
i++;
}
} else {
int count = 0;
// 查找比上一个数大1的数的数量
while (i < n && nums[i] == pre + 1) {
count++;
i++;
}
int remain = count - one - two, temp;
if (remain < 0) {
// 长度1和长度为2的是一定需要被满足的,所以说如果count不够,返回false
return false;
} else {
// 满足长度1和2后,贪心算法,如果有more没必要另起一个one,所以remain尽可能给more
// 原来的two也变成more,remain如果小于more,那么有一部分more断了不再讨论
temp = remain - more;
more = Math.min(remain, more) + two;
}
// 原来的one变成two
two = one;
// 如果remain大于more,则多的另起one,否则没有one
one = Math.max(0, temp);
}
}
return one + two == 0;
}
}