【每天一道算法题3】【Python】分割数组为连续子序列

题目描述

输入一个按升序排序的整数数组(可能包含重复数字),你需要将它们分割成几个子序列,其中每个子序列至少包含三个连续整数。返回你是否能做出这样的分割?

示例 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

解法一:使用堆求解
import heapq
from collections import defaultdict


class Solution:
    def isPossible(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """
        # 使用defaultdict初始化一个字典,当字典不存在某个key时返回[]
        chains = defaultdict(list)
        # 遍历nums
        for i in nums:
            if not chains[i - 1]:
                heapq.heappush(chains[i], 1)
            else:
                min_len = heapq.heappop(chains[i - 1])
                heapq.heappush(chains[i], min_len + 1)
            # print(chains)

        for chain in chains.values():
            if chain and chain[0] < 3:
                return False
        return True
解法二:开始结束时间

想法

我们可以把问题想象为在一条数字直线上画区间。这让我们想到开始结束事件。

为了说明这个概念,我们假设有 nums = [10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13] ,没有 9 和 14 。我们必须有 2 个序列从 10 开始, 2 个序列从 11 开始, 3 个序列到 12 结束。

总的来说,当考虑一连串连续的整数 x ,令 C = count[x+1] - count[x] ,如果 C > 0 ,必须有 C 个子序列从 x+1 开始,如果 C < 0 ,必须有 -C 个子序列在 x 结束。即使区间内有更多的结束端点,C 至少是一个下界。

在上面的例子中, count[11] - count[10] = 2 和 count[13] - count[12] = -3 表明有两个子序列从 11 开始,且有三个子序列在 12 结束。

如果我们知道有一些子序列从 1 和 4 开始,同时有一些子序列在 5 和 7 结束,为了最大化最短子序列,我们应该让 1 与 5 配对, 4 与 7 配对。

算法

对于每一组数字,我们求出数字是 t 的次数 count 。进一步地,令 prev, prev_count 为前一个数字和它出现的次数。

然后,总共会有 abs(count - prev_count) 个事件发生:如果 count > prev_count ,那么我们增加 count - prev_count 个以 t 开始的事件到 starts,如果 count < prev_count ,我们将从 starts.popleft() 获取以 t-1 为结束的子区间的开始数字。

更具体的,当我们结束一个连续的组,我们会得到 prev_count 个结束的子数组,而当我们在一个连续的组中时,我们会有 count - prev_count 个开始或者 prev_count - count 个结束。

比方说, nums = [1,2,3,3,4,5] ,那么开始的位置在 1 和 3,结束的位置在 3 和 5。我们的算法会如下进行:

当 t = 1, count = 1 时: starts = [1]
当 t = 2, count = 1 时: starts = [1]
当 t = 3, count = 2 时: starts = [1, 3]
当 t = 4, count = 1 时: starts = [3] ,由于 prev_count - count = 1 ,我们会结束一个事件,当 t-1 >= starts.popleft() + 2 成立时我们才认为这是一个合法的事件。
当 t = 5, count = 1 时: starts = [3]
在最后,我们将 prev_count 与 nums[-1] 作为最后一次结束事件的次数和数字。

class Solution(object):
    def isPossible(self, nums):
        prev, prev_count = None, 0
        starts = collections.deque()
        for t, grp in itertools.groupby(nums):
            count = len(list(grp))
            if prev is not None and t - prev != 1:
                for _ in xrange(prev_count):
                    if prev < starts.popleft() + 2:
                        return False
                prev, prev_count = None, 0

            if prev is None or t - prev == 1:
                if count > prev_count:
                    for _ in xrange(count - prev_count):
                        starts.append(t)
                elif count < prev_count:
                    for _ in xrange(prev_count - count):
                        if t-1 < starts.popleft() + 2:
                            return False

            prev, prev_count = t, count

        return all(nums[-1] >= x+2 for x in starts)

解法三:贪心算法

想法

我们把 3 个或更多的连续数字称作 chain。

我们从左到右考虑每一个数字 x,如果 x 可以被添加到当前的 chain 中,我们将 x 添加到 chain 中,这一定会比创建一个新的 chain 要更好。

为什么呢?如果我们以 x 为起点新创建一个 chain ,这条新创建更短的链是可以接在之前的链上的,这可能会帮助我们避免创建一个从 x 开始的长度为 1 或者 2 的短链。

算法

我们将每个数字的出现次数统计好,记 tails[x] 是恰好在 x 之前结束的链的数目。

现在我们逐一考虑每个数字,如果有一个链恰好在 x 之前结束,我们将 x 加入此链中。否则,如果我们可以新建立一条链就新建。

我们可以优化额外空间到 O(1)O(1),因为我们可以像 方法 1 一样统计数字的出现次数,而且我们只需要知道最后 3 个数字的出现次数即可。

代码

class Solution(object):
    def isPossible(self, nums):
        count = collections.Counter(nums)
        tails = collections.Counter()
        for x in nums:
            if count[x] == 0:
                continue
            elif tails[x] > 0:
                tails[x] -= 1
                tails[x+1] += 1
            elif count[x+1] > 0 and count[x+2] > 0:
                count[x+1] -= 1
                count[x+2] -= 1
                tails[x+3] += 1
            else:
                return False
            count[x] -= 1
        return True

复杂度分析

  1. 时间复杂度: O(N)O(N),其中 NN 是 nums 的长度。我们需要遍历整个数组一次。
  2. 空间复杂度: O(N)O(N),count 和 tail 的大小为 NN。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值