题目描述

✅ 927. 三等分

image-20260929105208544

image-20260929105208664

题意分析

将二进制数组切成三个非空连续部分,使它们表示的二进制数值相同。返回 [i,j] 时,三部分分别是 [0,i]、[i+1,j-1]、[j,n-1],所以返回的两个下标分别表示第一段末尾和第三段开头。

前导零不改变数值,但有效数字后面的零不能省略。不需要把各段转换为整数,只需比较去掉前导零后的二进制表示,避免长度很大时溢出。

解法:定位三段起点 + 三指针同步比较

核心思路

[!blue]

若所有元素都是零,任意非空三段都相等,题目保证长度至少为 3,可直接返回 [0,2]。否则三段表示的数为正数,它们去掉前导零后的表示必须完全相同,所以每段包含的 1 数量也必须相同。总数不是 3 的倍数时无解。

设每段应包含 k 个 1,三个有效起点就被唯一确定:i1、i2、i3 分别是全局第 1、第 k+1、第 2k+1 个 1 的位置。改变切口只能调整前导零,不能改变这三个起点。

第三段必须延伸到数组末尾,因此它从 i3 开始的有效表示长度固定为 L=n-i3,包含全部尾零。前两段从各自有效起点开始,也必须保留完全相同的 L 位。令三个指针分别从 i1、i2、i3 同步前进,遇到不同位就无解;第三指针到末尾时,前两段所需的有效表示也比较完了。

比较完成后,第一段结束于 p1-1,第二段的有效表示结束于 p2-1,所以可以让第三段从 p2 开始。代码中的 end2 就是这个结束后一位的下标,实际用作返回的第三段起点。还需保证第一段没有越过 i2、第二段没有越过 i3,即 p1<=i2 且 p2<=i3。

在这些边界成立时,切口到下一有效起点之间只可能是零,分别成为第二、第三段的前导零;三段都含有 k 个 1,因此非空且数值相同。反过来,起点和有效长度都已固定,逐位比较或边界检查一旦失败,也不可能通过改选切口补救。

解题步骤

  1. 统计一的数量,处理全零与不能三等分的情况。
  2. 定位三个有效起点。
  3. 同步比较三段,从第一个一一直比较到第三段末尾。
  4. 确认前两段有效表示不重叠,返回第一段末尾与第二段结束后的下标。

同步比较时,p1、p2 始终小于 p3,而 p3<n 才访问数组,因此比较阶段不会越界;范围检查进一步保证得到的是三个不重叠的连续部分。

代码实现

class Solution {
    public int[] threeEqualParts(int[] arr) {
        int n = arr.length;
        int ones = 0;

        for (int x : arr) {
            if (x == 1) {
                ones++;
            }
        }

        // 全零没有有效的一起点,直接返回任意非空三段切法。
        if (ones == 0) {
            return new int[] {
                0,
                2
            };
        }

        if (ones % 3 != 0) {
            return new int[] {
                -1,
                -1
            };
        }

        int k = ones / 3;
        int i1 = findKthOne(arr, 1);
        int i2 = findKthOne(arr, k + 1);
        int i3 = findKthOne(arr, 2 * k + 1);

        int p1 = i1;
        int p2 = i2;
        int p3 = i3;

        // 第三段必须比较到数组末尾,尾零也属于有效表示。
        while (p3 < n) {
            if (arr[p1] != arr[p2] || arr[p2] != arr[p3]) {
                return new int[] {
                    -1,
                    -1
                };
            }

            p1++;
            p2++;
            p3++;
        }

        int end1 = p1 - 1;
        int start3 = i3;
        int lenTail = n - start3;
        int end2 = i2 + lenTail;

        if (end1 >= i2 || end2 > i3) {
            return new int[] {
                -1,
                -1
            };
        }

        return new int[] {
            end1,
            end2
        };
    }

    private int findKthOne(int[] arr, int kth) {
        int cnt = 0;

        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == 1) {
                cnt++;

                if (cnt == kth) {
                    return i;
                }
            }
        }

        return -1;
    }
}
func threeEqualParts(arr []int) []int {
    n := len(arr)
    ones := 0
    for _, x := range arr {
        if x == 1 {
            ones++
        }
    }
    // 全零没有有效的一起点,直接返回任意非空三段切法。
    if ones == 0 {
        return []int{
            0,
            2,
        }
    }
    if ones%3 != 0 {
        return []int{
            -1,
            -1,
        }
    }

    k := ones / 3
    i1 := findKthOne(arr, 1)
    i2 := findKthOne(arr, k+1)
    i3 := findKthOne(arr, 2*k+1)

    p1, p2, p3 := i1, i2, i3
    // 第三段必须比较到数组末尾,尾零也属于有效表示。
    for p3 < n {
        if arr[p1] != arr[p2] || arr[p2] != arr[p3] {
            return []int{
                -1,
                -1,
            }
        }
        p1++
        p2++
        p3++
    }

    end1 := p1 - 1
    lenTail := n - i3
    end2 := i2 + lenTail
    if end1 >= i2 || end2 > i3 {
        return []int{
            -1,
            -1,
        }
    }
    return []int{
        end1,
        end2,
    }
}

func findKthOne(arr []int, kth int) int {
    cnt := 0
    for i := 0; i < len(arr); i++ {
        if arr[i] == 1 {
            cnt++
            if cnt == kth {
                return i
            }
        }
    }
    return -1
}

复杂度分析

  • 时间复杂度:$O(n)$,常数次线性扫描。
  • 空间复杂度:$O(1)$。

关键点总结

[!green]

  • 比较从第一个一开始,跳过无意义的前导零。
  • 第三段末尾固定,决定另外两段需要保留多少尾零。
  • 返回下标的两端含义不同:第一段末尾、第三段开头。

易错点总结

[!yellow]

  • 只检查一的数量相同:位的排列与尾零也必须一致。
  • 全零时继续找第一个一:有效起点不存在。
  • 把第三段起点写成第二段末尾:切口偏一,可能漏掉有效位。
  • 要求三段长度相同:前导零可以让三段实际长度不同。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/50200482
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!