算法-构建前缀信息专题

1. 最大公约/最小公倍/同余原理补充

这里我们先对本节的一些前提只是做一些补充(实质上就是一部分的知识点不开新专题了直接在这里简单介绍一下就可以了)

求最大公约数的欧几里得算法

其实就求最大公约数就下面的一行代码(证明我们之前的章节的注释有)
	public long gcd(int a, int b) {
       return b == 0 ? a : gcd(b, a % b);
    }

求最下公倍数是建立在求最大公约数的基础上

	/**
     * 求最小公倍数的代码
     */
    public long lcm(int a, int b) {
        return (long) a / gcd(a, b) * b;
    }

下面对我们的这个算法进行一个简单的应用, leetcode878, 神奇的数字
在这里插入图片描述

这道题用到了一点容斥原理以及二分答案法的相关的知识, 这个我们后期要做出来相关的专题单独进行处理, 但是我们今天就直接用一下就可以了, 也是没有什么问题的, 直接就可以听懂
首先如果只有一个数字a的话, 那我们的最好的数字的上界就是 n * a, 在这个范围内一定能满足有n个神奇的数字, 但是加入了数字b之后, 我们的神奇的数字的上界应该是 <= 上面的界限的, 所以我们在这个范围内进行二分, 判断是不是符合条件的, 对于任意一个数字num来说, 神奇的数字的个数为
num / a + num / b - num / lcm(ab的最小公倍数), 这里就是容斥原理, code如下…


class Solution {
    private static final long MOD = (long) (1e9 + 7);    
    public int nthMagicalNumber(int n, int a, int b) {
        //首先求一下边界的位置(一定能保证出现n个余数的最小的位置)
        long boundary = Math.min(a, b) * (long)n;
        //求出来a, b的最小公倍数
        long lcm = lcm(a, b);

        //下面就是定义两个指针 left 和 right 用来进行二分的操作
        long left = 0;
        long right = boundary;
        long mid = 0; //中点的坐标
        long ans = 0; //返回的答案
        while(left <= right){
            mid = left + ((right - left) >>> 1); //防止越界的方法
            if(mid / a + mid / b - mid / lcm >= n){
                //上面的if条件里面的计算有多少个神奇的方法就是容斥原理
                ans = mid;
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        return (int)(ans % MOD);
    }
    public long gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    /**
     * 求最小公倍数的代码
     */
    public long lcm(int a, int b) {
        return (long) a / gcd(a, b) * b;
    }
}

基础的同余原理就是对于任意一个数字, 如果进行运算的时候出现了溢出的问题, 那我们就进行先取余后进行运算的操作
这里可能有人说为什么我们不用bigInteger这种类进行运算, 我们想说的是, 在计算机底层, 我们的算术运算都是位运算拼接出来的, 对于k位的数字 加减的运算的时间复杂度是 o(k) , 乘除的时间复杂度是o(k^2) , 因为我们的int, long这种的长度我们也可以认为是一个常数, 所以运算的复杂度是 o(1), 但如果数字过大的话, 我们就不能进行忽略了,这就会导致计算机的底层运算速度变慢, 所以我们取余的操作是必要的…
下面是测试的代码

/**
     * 下面是关于同余原理的测试
     * 首先是加法的同余原理 :
     *     举例如下 : (a + b + c) % m == (a % m + b % m + c % m) % m == ...(任意结合)
     * 乘法的同余原理 :
     *     跟加法是一样的, 也是随意的结合
     * 减法的同余原理 :
     *     (a - b) % mod == (a % mod - b % mod + mod) % mod
     * 关于除法的同余原理我们暂时不进行介绍, 因为比较难, 等待我们后期学习了乘法的逆元操作在进行学习
     */
    // 为了测试
class Test {
    public static long random() {
        return (long) (Math.random() * Long.MAX_VALUE);
    }

    // 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
    public static int f1(long a, long b, long c, long d, int mod) {
        BigInteger o1 = new BigInteger(String.valueOf(a)); // a
        BigInteger o2 = new BigInteger(String.valueOf(b)); // b
        BigInteger o3 = new BigInteger(String.valueOf(c)); // c
        BigInteger o4 = new BigInteger(String.valueOf(d)); // d
        BigInteger o5 = o1.add(o2); // a + b
        BigInteger o6 = o3.subtract(o4); // c - d
        BigInteger o7 = o1.multiply(o3); // a * c
        BigInteger o8 = o2.multiply(o4); // b * d
        BigInteger o9 = o5.multiply(o6); // (a + b) * (c - d)
        BigInteger o10 = o7.subtract(o8); // (a * c - b * d)
        BigInteger o11 = o9.add(o10); // ((a + b) * (c - d) + (a * c - b * d))
        // ((a + b) * (c - d) + (a * c - b * d)) % mod
        BigInteger o12 = o11.mod(new BigInteger(String.valueOf(mod)));
        if (o12.signum() == -1) {
            // 如果是负数那么+mod返回
            return o12.add(new BigInteger(String.valueOf(mod))).intValue();
        } else {
            // 如果不是负数直接返回
            return o12.intValue();
        }
    }

    // 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
    public static int f2(long a, long b, long c, long d, int mod) {
        int o1 = (int) (a % mod); // a
        int o2 = (int) (b % mod); // b
        int o3 = (int) (c % mod); // c
        int o4 = (int) (d % mod); // d
        int o5 = (o1 + o2) % mod; // a + b
        int o6 = (o3 - o4 + mod) % mod; // c - d
        int o7 = (int) (((long) o1 * o3) % mod); // a * c
        int o8 = (int) (((long) o2 * o4) % mod); // b * d
        int o9 = (int) (((long) o5 * o6) % mod); // (a + b) * (c - d)
        int o10 = (o7 - o8 + mod) % mod; // (a * c - b * d)
        int ans = (o9 + o10) % mod; // ((a + b) * (c - d) + (a * c - b * d)) % mod
        return ans;
    }

    public static void main(String[] args) {
        System.out.println("测试开始");
        int testTime = 100000;
        int mod = 1000000007;
        for (int i = 0; i < testTime; i++) {
            long a = random();
            long b = random();
            long c = random();
            long d = random();
            if (f1(a, b, c, d, mod) != f2(a, b, c, d, mod)) {
                System.out.println("出错了!");
            }
        }
        System.out.println("测试结束");

        System.out.println("===");
        long a = random();
        long b = random();
        long c = random();
        long d = random();
        System.out.println("a : " + a);
        System.out.println("b : " + b);
        System.out.println("c : " + c);
        System.out.println("d : " + d);
        System.out.println("===");
        System.out.println("f1 : " + f1(a, b, c, d, mod));
        System.out.println("f2 : " + f2(a, b, c, d, mod));
    }
}

2. 构建前缀和数组

前缀和其实就是针对一个数组, 把前 k 位的前缀先进行求和操作, 示例如下
int[] arr = { 1 , 2 , 3 , 4 , 5 }; //原数组
int[] sum = { 1, 3 , 6 , 10 , 15 }; //前缀和数组(直接求和的形式)
int[] sum1 = { 0 , 1 , 3 , 6 , 10 , 15 }; //前缀和数组(前几个数求和的形式)
上面的前缀和数组的形式适时而用(后者一般不用考虑边界情况)

在这里插入图片描述
上面这个题就是直接构建出来前缀和的数组然后进行进行检索就可以了, 代码实现如下…

//第一种构建的前缀和数组的方式
class NumArray {

    private int[] sum; // 前缀和数组

    public NumArray(int[] nums) {
        // 构建前缀和数组
        sum = new int[nums.length];
        sum[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            sum[i] = sum[i - 1] + nums[i];
        }
    }

    public int sumRange(int left, int right) {
        return left == 0 ? sum[right] : sum[right] - sum[left - 1];
    }
}

//第二种构建前缀和数组的方式
class NumArray {

    private int[] sum; // 前缀和数组

    public NumArray(int[] nums) {
        // 构建前缀和数组
        sum = new int[nums.length + 1];
        for (int i = 1; i <= nums.length; i++) {
            sum[i] = sum[i - 1] + nums[i - 1];
        }
    }

    public int sumRange(int left, int right) {
        return sum[right + 1] - sum[left];
    }
}

3. 构建前缀和最早出现的位置

题目一 : 返回无序数组中累加和等于target的最长的子数组长度
这个题的突破点就是对于一个位置 i , 此时的前缀和是 sum, 然后我们想要找到的前缀和是 sum - target, 找到此时出现的下标然后减一下就是最大的长度, 所以是哈希表加上前缀和的思路…

在这里插入图片描述

代码实现如下


// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        //下面是数据的读入环节
        Scanner in = new Scanner(System.in);
        int length = in.nextInt();
        int target = in.nextInt();
        int[] arr = new int[length];
        for(int i = 0; i < length; i++){
            arr[i] = in.nextInt();
        }
    
        int res = getMaxLength(arr, target);
        System.out.print(res);
    }

    //计算最长的长度的主函数(哈希表加上前缀和的思路)
    private static int getMaxLength(int[] arr, int target){
        //不符合要求直接返回
        if(arr == null || arr.length == 0) return -1;

        //哈希表构建前缀信息(key是前缀和的位置, value是前缀和出现的最早的下标位置)
        HashMap<Integer, Integer> map = new HashMap<>();
        //避开边界位置的讨论
        map.put(0, -1);
        //前缀和sum
        int sum = 0;
        //返回的最大长度
        int maxLength = 0;
        for(int i = 0; i < arr.length; i++){
            sum += arr[i];
            if(map.containsKey(sum - target)){
                maxLength = Math.max(maxLength, i - map.get(sum - target));
            }
            if(!map.containsKey(sum)){
                map.put(sum, i);
            }
        }
        return maxLength;
    }
}

在这里插入图片描述

题目二 : 返回无序数组中正数和负数个数相等的最大子数组长度
这个其实就是对上面的那个问题进行转换一下, 我们把正数看作 1 , 0就还看做0, 负数就看成 -1 , 所以对于任意一个前缀和 sum , 我们只需要找到map中出现的 sum的下标位置相减即可, 其实跟上面的那个问题是一模一样的, 代码实现如下…


// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int len = in.nextInt();
        int[] nums = new int[len];
        for (int i = 0; i < len; i++) {
            nums[i] = in.nextInt();
        }
        int res = getMaxEqualsLength(nums);
        System.out.println(res);
    }

    public static int getMaxEqualsLength(int[] arr) {
        //特殊情况直接返回
        if(arr == null || arr.length == 0) return -1;

        //构建map里面key是前缀和sum, value是下标位置
        HashMap<Integer, Integer> map = new HashMap<>();
        //避开边界位置的讨论
        map.put(0, -1);
        //前缀和
        int sum = 0;
        //返回的最大的子数组的长度
        int maxLength = 0;
        for(int i = 0; i < arr.length; i++){
            sum += arr[i] > 0 ? 1 : arr[i] == 0 ? 0 : -1;
            if(map.containsKey(sum)){
                maxLength = Math.max(maxLength, i - map.get(sum));
            }
            if(!map.containsKey(sum)){
                map.put(sum, i);
            }
        }
        return maxLength;
    }
}

题目三 : 工作良好的最长时间段
这个题和上面的类似, 但也是多出来了一步转化的过程, 我们把大于8的看作1, 小于等于8的看作-1, 所以我们的sum就一定是一个每次加减1的变化, 所以假设有一个 sum , 在他的左侧位置一定有一个大于他的值, 代码如下

在这里插入图片描述

class Solution {
    public int longestWPI(int[] hours) {
        return getBestTimes(hours);
    }

    // 下面才是构建的主函数
    private int getBestTimes(int[] hours) {
        // 特殊条件直接返回
        if (hours == null || hours.length == 0)
            return -1;
        // 构建前缀信息, key是前缀和(转化后), value是下标位置
        HashMap<Integer, Integer> map = new HashMap<>();
        // 避开边界位置
        map.put(0, -1);
        // 前缀和sum(转换之后(大于8就是1), 小于等于8就是-1)
        int sum = 0;
        // 返回的最长的长度
        int maxLength = 0;
        for (int i = 0; i < hours.length; i++) {
            // 前缀和的转化
            sum += hours[i] > 8 ? 1 : -1;
            // 如果是正数直接返回下标 + 1
            if (sum > 0) {
                maxLength = i + 1;
            } else {
                // 这里我们只需要查询sum - 1的位置就可以了, 因为比sum - 1小的位置也可以
                // 但是sum - 1一定比他们靠前且如果比sum - 1的小的存在, 那么sum - 1一定存在
                if (map.containsKey(sum - 1)) {
                    maxLength = Math.max(maxLength, i - map.get(sum - 1));
                }
            }
            if (!map.containsKey(sum)) {
                map.put(sum, i);
            }
        }
        return maxLength;
    }
}

这个构建前缀信息的技巧我们还有几道题运用
将x减到0的最小的操作次数
https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/
长度最小的子数组(用TreeMap替代)
https://leetcode.cn/problems/minimum-size-subarray-sum/

4. 构建前缀和出现的次数

题目一 : 返回无序数组中累加和等于给定值的子数组的数量
在这里插入图片描述
代码实现如下

class Solution {
    public int subarraySum(int[] nums, int k) {
        //这里构建的前缀信息是前缀和出现的数量
        HashMap<Integer, Integer> map = new HashMap<>();
        //特殊的情况, 避开特殊位置的讨论
        map.put(0, 1);
        //前缀和sum
        int sum = 0;
        int resCnt = 0;
        for(int i = 0; i < nums.length; i++){
            sum += nums[i];
            resCnt += map.getOrDefault(sum - k, 0);
            map.put(sum, map.getOrDefault(sum, 0) + 1);
        }
        return resCnt;
    }
}

5. 构建前缀和余数出现的最晚位置

在这里插入图片描述

这道题就是构建的我们最晚出现的余数信息
其实就下面一个点需要注意, 假如全体数字和的余数位 mod == 7 , p == 9;
第一种情况, 当前的余数信息是 cur == 8, 也就是 cur >= 7 的情况, 此时我们需要找到的位置是余数为 cur - mod == 1 的位置
另一种情况, 当前的余数信息是cur == 5, 也就是 cur < 7 的情况, 此时余数5其实等效为余数为14(cur + p), 所以此时要找到的位置是 cur + p - mod;
代码实现如下

class Solution {
    //这个题目就是简单应用了一下我们的同余原理
    public int minSubarray(int[] nums, int p) {
        // 首先计算一下全体的余数
        int allSum = 0;
        for (int elem : nums) {
            allSum += (elem % p);
            allSum = allSum % p;
        }
        int mod = allSum % p;
        if (mod == 0)
            return 0;
        // 本题中构建的前缀信息是, 某一段前缀和余数出现的最晚的位置
        HashMap<Integer, Integer> map = new HashMap<>();
        // 避开边界情况的讨论
        map.put(0, -1);
        // 前缀和
        int sum = 0;
        // 返回的最小的长度
        int resMinLen = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
            sum += (nums[i] % p); // 真实的前缀和
            sum = sum % p;
            int cur = sum % p; // 当前位置的余数
            int find = mod <= cur ? cur - mod : cur + p - mod; // 待找到的位置
            if (map.containsKey(find)) {
                resMinLen = Math.min(resMinLen, i - map.get(find));
            }
            map.put(cur, i);
        }
        return resMinLen == nums.length ? -1 : resMinLen;
    }
}

本节的主要内容就是如何构建我们的某某前缀信息, 这个还需要打磨…

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值