三等分

标签: 数组 数学

难度: Hard

给定一个由 01 组成的数组 arr ,将数组分成  3 个非空的部分 ,使得所有这些部分表示相同的二进制值。

如果可以做到,请返回任何 [i, j],其中 i+1 < j,这样一来:

  • arr[0], arr[1], ..., arr[i] 为第一部分;
  • arr[i + 1], arr[i + 2], ..., arr[j - 1] 为第二部分;
  • arr[j], arr[j + 1], ..., arr[arr.length - 1] 为第三部分。
  • 这三个部分所表示的二进制值相等。

如果无法做到,就返回 [-1, -1]

注意,在考虑每个部分所表示的二进制时,应当将其看作一个整体。例如,[1,1,0] 表示十进制中的 6,而不会是 3。此外,前导零也是被允许的,所以 [0,1,1] 和 [1,1] 表示相同的值。

示例 1:

输入:arr = [1,0,1,0,1]
输出:[0,3]

示例 2:

输入:arr = [1,1,0,1,1]
输出:[-1,-1]

示例 3:

输入:arr = [1,1,0,0,1]
输出:[0,2]

提示:

  • 3 <= arr.length <= 3 * 104
  • arr[i] 是 0 或 1

Submission

运行时间: 50 ms

内存: 17.7 MB

class Solution:
    def threeEqualParts(self, arr: List[int]) -> List[int]:
        # 看评论区:
        # 「1 的个数必须是 3 的整数倍。将 1 等分成 3 份。
        #   再根据最后一份末尾 0 的个数推导:找到前两份的最后一个坐标,
        #   最后判断 3 份去掉前导 0 是不是一样即可。」
        # 太妙了...!!!!!
        # Fatal: 我擦... 我原先的代码疯狂遍历各种 0,极为麻烦... 最后写出来,打败 9%...
        #   ChatGPT 给的建议:【直接把 0 都去掉,只保存 1 的 indices 数组...!!! 直接秒拆分... !!!】
        #   太妙了!!!
        n = len(arr)
        cnt = sum(arr)
        if cnt % 3 != 0:
            return [-1, -1]
        if cnt == 0:
            return [0, 2]
        avg = cnt // 3
        
        one_indices = [i for i, x in enumerate(arr) if x == 1]
        part1_one_start = one_indices[0]
        part1_one_end = one_indices[avg - 1]      # part1 最后一个 1 的结束位置...
        part2_one_start = one_indices[avg]    # part2 最后一个 1 的开始位置...
        part2_one_end = one_indices[avg * 2 - 1]  # part2 最后一个 1 的结束位置...
        part3_one_start = one_indices[avg * 2]
        part3_one_end = one_indices[-1]
        
        last_zeros = n - 1 - part3_one_end   # 最后有几个 0... 我擦... 直接能求出来...
        if part1_one_end + last_zeros >= part2_one_start:  # 如果 p1 的末端入侵到了 p2 的区域:
            return [-1, -1]
        if part2_one_end + last_zeros >= part3_one_start:
            return [-1, -1]
        
        part1_end = part1_one_end + last_zeros
        part2_end = part2_one_end + last_zeros
        part3_start = part2_end + 1
        
        # 最后一个校验:
        # 每个 seg 的 1 起始位置、到各自终结位置,都必须完全相同...

        # 先检查长度...
        if part1_end - part1_one_start == \
            part2_end - part2_one_start == \
            n-1 - part3_one_start:
            pass
        else:
            return [-1, -1]

        # ChatGPT 太妙了.. 这里省了算麻烦的 seg 长度,直接用最后一段...!!!
        i, j, k = part1_one_start, part2_one_start, part3_one_start
        while k < n:
            if arr[i] == arr[j] == arr[k]:
                i += 1
                j += 1
                k += 1
            else:
                return [-1, -1]
        
        return [part1_end, part3_start]

Explain

该题解策略首先计算数组中1的总数。如果1的总数不能被3整除,直接返回[-1, -1]。如果整个数组都是0,返回[0, 2]。然后,将数组中1的位置存储在一个列表中,根据1的总数除以3的结果,确定三个部分中每一部分1的起始和结束位置。计算第三部分结束后剩余的0的数量,用这个信息来确定第一部分和第二部分的结束位置。最后,进行一次遍历来确认三部分的内容是否完全相同。如果相同,则返回第一部分和第三部分的结束位置作为结果;如果在任何点不相同,则返回[-1, -1]。

时间复杂度: O(n)

空间复杂度: O(n)

class Solution:
    def threeEqualParts(self, arr: List[int]) -> List[int]:
        n = len(arr)
        cnt = sum(arr)  # 计算数组中1的总数
        if cnt % 3 != 0:
            return [-1, -1]  # 如果1的总数不能被3整除,无解
        if cnt == 0:
            return [0, 2]  # 如果数组全为0,任意切分即可

        avg = cnt // 3

        one_indices = [i for i, x in enumerate(arr) if x == 1]  # 存储1的位置
        part1_one_start = one_indices[0]
        part1_one_end = one_indices[avg - 1]
        part2_one_start = one_indices[avg]
        part2_one_end = one_indices[avg * 2 - 1]
        part3_one_start = one_indices[avg * 2]
        part3_one_end = one_indices[-1]

        last_zeros = n - 1 - part3_one_end  # 计算最后一部分后的0的数量

        if part1_one_end + last_zeros >= part2_one_start:  # 检查部分之间的重叠
            return [-1, -1]
        if part2_one_end + last_zeros >= part3_one_start:
            return [-1, -1]

        part1_end = part1_one_end + last_zeros
        part2_end = part2_one_end + last_zeros
        part3_start = part2_end + 1

        i, j, k = part1_one_start, part2_one_start, part3_one_start
        while k < n:  # 比较三个部分的内容
            if arr[i] == arr[j] == arr[k]:
                i += 1
                j += 1
                k += 1
            else:
                return [-1, -1]

        return [part1_end, part3_start]  # 如果比较通过,返回结果

Explore

在算法中,通过确保每一部分中1的数量相等并且每一部分后面的0的数量也相等来保证每一部分的二进制值相等。首先,算法找到每一部分中1的起始和结束位置。接着,计算第三部分结束后剩余的0的数量,并将这些0分配到前两部分的末尾,确保每部分结束后的0的数量相同。最后,通过逐一比较三个部分从第一个1到最后一个0的内容,来确认它们是否完全相同。这样即使存在前导零,只要三部分内容完全一致,就能保证它们的二进制值相等。

如果1的总数不能被3整除,那么无法将这些1均等地分配到三个部分中。每一部分必须含有相等数量的1,以确保三个部分的二进制值相等。如果1的总数不能整除3,至少有一个部分的1的数量将与其他部分不同,导致无法形成三个相等的部分。因此,如果1的总数不能被3整除,算法直接返回`[-1, -1]`表示无法分割成三个相等的部分。

最后一部分后的0的数量决定了我们可以在第一部分和第二部分后添加多少个0以保持三部分结构上的一致性。每部分的结束位置需要包括足够的0,使得三部分在1的分布和尾部0的数量上完全相同。因此,计算出第三部分后的0的数量后,我们可以确定第一部分和第二部分应当在哪里结束,即在对应部分的最后一个1之后再加上相同数量的0,以确保三个部分在二进制表示上完全相同。

当数组全为0时,由于0的二进制值总是相等,因此可以任意切分数组为三个部分而保证它们的二进制值相同。返回`[0, 2]`是一种简单的切分方法,将前三个元素分为第一部分,其余为第二和第三部分。实际上,只要确保每部分至少有一个元素,任何切分方法都是有效的,例如`[0, 1]`或`[1, 2]`等都是可行的切分,满足题目要求。