和相同的二元子数组

212 阅读2分钟

题目

滑动窗口

public class Main {
    public static void main(String[] args) {

        Main main = new Main();

        int [] nums = new int[] {1,1,1,1,1,0,1,1,1,1};
        int [] nums2 = new int[] {1, 1, 0};
        main.numSubarraysWithSum(nums, 5);
    }


    public int numSubarraysWithSum(int[] A, int S) {

        // A中只有0和1 有这个条件才能使用滑动窗口
        int count = 0;
        // 和为0需要单独考虑
        // 有一个规律例如[6, 2, 3, 4, 5]
        // 长度为1的子数组有5个
        // 长度为2的子数组有4个
        // 长度为3的子数组有3个
        // 长度为4的子数组有2个
        // 长度为5的子数组有1个
        // 因此长度为5的子数组的个数为(首 + 尾) * 高 / 2 = (1 + 5) * 5 / 2 = 15;
        if (S == 0) {
            // 获得连续0的个数, 然后得到所有子数组的和
            int zeroCount = 0;
            for (int i = 0; i < A.length; i ++) {
                if (A[i] != 0) {
                    count += (1 + zeroCount) * zeroCount / 2;
                    zeroCount = 0;
                } else {
                    zeroCount ++;
                }
            }
            // 下面这行针对以0结尾的数组
            count += (1 + zeroCount) * zeroCount / 2;
            return count;
        }

        // 遍历一遍A, 使用数组记录下每个1之前到上一个1中间0的个数
        // 使用数组记录每个值为1的索引的下一个1的索引是多少
        int [] zeroBefore = new int[A.length + 1];
        int [] nextOneIndex = new int[A.length];
        int leftZero = 0;
        int preOneIndex = -1;
        for (int i = 0; i < A.length; i ++) {
            if (A[i] == 1) {
                zeroBefore[i] = leftZero;
                leftZero = 0;

                if(preOneIndex != -1) {
                    nextOneIndex[preOneIndex] = i;
                    preOneIndex = i;
                } else {
                    preOneIndex = i;
                }
            } else {
                leftZero ++;
            }
            if (i == A.length - 1 && A[i] == 0) {
                zeroBefore[A.length] = leftZero;
            }
        }

        // 记录leftOne是窗口里最左侧的1
        int leftOne = -1;
        // 窗口内的和
        int windowSum = 0;
        for (int i = 0; i < A.length; i ++) {
            if (A[i] == 1) {
                if (windowSum == S) {
                    // 统计一次
                    count += (zeroBefore[leftOne] + 1) * (zeroBefore[i] + 1);
                    leftOne = nextOneIndex[leftOne];
                    windowSum --;
                    i --;

                } else {
                    windowSum ++;
                    if (leftOne == -1) {
                        leftOne = i;
                    }
                }
            }

        }
        if (windowSum == S) {
            if (A[A.length - 1] == 1) {
                count += (zeroBefore[leftOne] + 1);
            } else {
                count += (zeroBefore[leftOne] + 1) * (zeroBefore[A.length] + 1);
            }
        }


        return count;
    }

}

基本思路

  1. 通过找到两个1的位置(one1和one2), 然后两个1之间的和==S, 此时如果one1到前一个1有m个0, one2到下一个1有n个0, 那么这一段所有的子数组的个数为(m + 1) * (n + 1)

  2. 为了减少遍历次数, 例如你找到了one1和one2, 要想统计前后0的个数, 如果没有提前记录, 则需要遍历; 如果现在要从one1到下一个为1的位置, 如果没有记录, 还是需要遍历; 因此利用两个数组来用空间换时间, 减少遍历 数组1: 记录每个值为1的元素到前一个1之间0的个数 数组2: 记录每个值为1的元素, 下一个1的索引是多少

  3. 利用2和1, 就可以计算数组的个数, 但是需要注意数组如果是以0结尾, 还是以1结尾, 需要特殊处理

  4. 当S为0的时候, 计算逻辑不同

  5. 每个索引和变量的含义, 一定要明确, 不要想当然的取改, 不然越改越糊涂

前缀和

    public int numSubarraysWithSum(int[] A, int S) {
        // 在一个数组中求连续子数组的和, 即A[i] + A[i + 1] + ... + A[j] == S
        // 如果我记录dp[i] 表示 0i 所有数的和
        // 那么dp[j] - dp[i] + A[i] = A[i] + A[i + 1] + ... + A[j] == S
        // 那么我只要找到dp[j] = dp[i] + S + A[i], 有多少种不同的i和j最后就有多少种子数组
        int [] dp = new int[A.length];
        for (int i = 0; i < A.length; i ++) {
            if (i == 0) {
                dp[i] = A[i];
            } else {
                dp[i] = dp[i - 1] + A[i];
            }
        }

        Map<Integer, Integer> record = new HashMap<>();
        // 遍历一遍A, 为dp[i] + S - A[i]赋值
        int count = 0;
        // 注意期望的j一定是>=i的, 因此先添加map, 再判断当前i是否可能为j
        for (int i = 0; i < A.length; i ++) {
            // 以i为基准进行遍历, 得到的dp[j] = dp[i] + S - A[]i的j都是不同的
            // record put的这些元素, 可能很多都不会被get出来
            record.put(dp[i] + S - A[i], record.getOrDefault(dp[i] + S - A[i], 0) + 1);
            // map的key表示dp数组的元素加上S, value表示出现的次数
            // 如果dp中的某个元素在map的key中出现了, 就表示存在dp[j] = dp[i] + S - A[i]
            // 这样就不用单独考虑S==0的情况了
            count += record.getOrDefault(dp[i], 0);
        }

        return count;
    }

}

基本思路

  1. 求连续子数组的和就是求数组中任意两个索引之间元素的和, 那么可以通过一次遍历, 额外用一个数组dp[i]记录索引0到索引i所有元素的和, dp[i] = A[0] + A[1] + ... A[i]

  2. 题目就变成了寻找有多少组i和j, 满足dp[j] - dp[i] + A[i] == S; 同时j一定是大于等于i的

  3. 上述条件可以变成dp[j] == S + dp[i] - A[i], 那么我们遍历一遍A, 记录所有的S + dp[i] - A[i]到map中, 然后判断j是否可以和i相等, 如果map的keySet()中包含A[j], 就说明存在dp[j] - dp[i] + A[i] == S, 计数一次