Differential

之前我有一篇 《差分+前缀和》的学习笔记,记录了差分的思想和基本用法。这里就只记录灵神题单的刷题记录。

1. LC 1094 拼车

我记得这是哪次每日一题来着,入门差分前缀和了。

差分数组维护每站新增的乘客(当然数量可以是≤0的),每一项在上车对应位置加。下车对应位置减即可。

class Solution {
    public boolean carPooling(int[][] trips, int capacity) {
        int[] diff = new int[1001];
        for (int[] trip : trips) {
            diff[trip[1]] += trip[0];
            diff[trip[2]] -= trip[0];
        }
        for (int i : diff) {
            capacity -= i;
            if(capacity<0){
                return false;
            }
        }
        return true;
    }
}

2. LC 1109 航班预订统计

同样入门题。差分数组维护每站新增座位。对于每个预订项,first(i)增加对应量,而last(i)之后就不再预订这些位置,所以要减掉。

class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] ans = new int[n];
        // 我有一计O(n)的差分前缀和,可做此题
        for (int[] booking : bookings) {
            ans[booking[0]-1] += booking[2];
            if(booking[1]<n){
                ans[booking[1]] -= booking[2];
            }
        }
        // 原地更新
        for (int i = 1; i < ans.length; i++) {
            ans[i] += ans[i-1];
        }
        return ans;
    }
}

3. LC 2381 字母移位Ⅱ

也是入门题。差分数组维护每个位置的移位偏移量,前缀和还原。

修改字符的时候,可以利用向左移动1等于向右移动25的技巧来规避掉对于负数的分类讨论。

import java.util.Arrays;

class Solution {
    public String shiftingLetters(String s, int[][] shifts) {
        int[] diff = new int[s.length()];
        int dir ;
        for (int[] shift : shifts) {
            dir = shift[2]==1?1:-1;
            diff[shift[0]] += dir;
            if(shift[1]<s.length()-1){
                diff[shift[1]+1] -= dir;
            }
        }
        // 原地还原
        for (int i = 1; i < diff.length; i++) {
            diff[i] += diff[i-1];
        }
        char[] ch = s.toCharArray();
        for (int i = 0; i < ch.length; i++) {
            int offset = (ch[i] - 'a' + diff[i]) % 26;
            if(offset<0){
                ch[i] = (char) ('a'+26+offset);
            }else{
                ch[i] = (char) ('a'+offset);
            }
        }
        return String.valueOf(ch);
    }
}
class Solution {
    public String shiftingLetters(String s, int[][] shifts) {
        char[] chars = s.toCharArray();
        int n = chars.length;
        int[] diff = new int[n + 1];
        for(int[] shift : shifts){
            int dir = shift[2] == 1 ? 1 : 25;
            diff[shift[0]] += dir;
            diff[shift[1] + 1] -= dir;
        }
        int t = 0;
        for(int i = 0; i < n; i++){
            t += diff[i];
            chars[i] = (char)('a' + (chars[i] + t - 'a') % 26);
        }
        return new String(chars);
    }
}

4. LC 2406 将区间分为最少组数

说实话,这题是标了个差分,提示我了。不标差分我真想不明白,汗流浃背了捏。

差分数组维护每个区间端点的使用次数,前缀和还原后就是每个数字用到的次数。最终的结果就是这些次数中的最大值。

想想看为什么,如果任意某个数字在不同区间出现了多次,那么我们就要把这些区间全部拆开来,分到不同的组里,取这个次数的最大值,一定能容纳其他较小值的出现次数对应的所有区间。(反正这是直观上很显然的事情,我就不严谨证明了)

class Solution {
    static int len = (int)1e6+1;
    public int minGroups(int[][] intervals) {
        int[] diff = new int[len + 1];
        for (int[] interval : intervals) {
            diff[interval[0]]++;
            diff[interval[1]+1]--;
        }
        int ans = 1;
        // 原地更新
        for (int i = 1; i < diff.length; i++) {
            diff[i] += diff[i-1];
            ans = Math.max(diff[i],ans);
        }
        return ans;
    }
}

5. LC 2772 使数组中的所有元素都等于零

这题我感觉自己思路很混(或者说很玄学),不知道严谨不严谨。

这道题是只能对某一个区间中的每一个元素去进行-1操作的,因此是不可逆的,一旦一个元素减少了,那他就再也没法加回来了。

对于第一个元素,也就是0号索引的。如果最终返回true,那么它一定变成0。而包含了这第一个元素的子数组有且只有一个,也就是索引区间[ 0, k-1],那么我们就只能对这个区间进行nums[0]次-1操作。最终使nums[0]变为0。

这样我们就可以把nums[0]元素撇开完全不看了,因为之前说过,这个元素的变化是不可逆的,它没有机会再加上去,然后再减回0,因此一旦它变成0,就再也不能动了,所以我们可以把它撇开完全不管了。

这样整个数组就变成了[1,n-1]了,对于nums[1],同样的,因为nums[0]不在了,所以有且仅有索引区间[1,k]能够执行-1操作,使得nums[1]变成0。这个过程和之前是一模一样的。

因此我们要做的就是,从左往右遍历,把当前位置变成0的同时,也连带把它后面k-1个元素执行同样次数的-1操作。比如现在遍历到3,那么后面的k-1个元素全部-3。

这就可以用一个差分数组来记录。差分数组维护每个元素的增减量,通过前缀和还原,这个在代码实现上是可以原地计算的。

那么什么时候判断失败,简单想想,很显然有且仅有两种:

  1. 当前元素已经小于0了(由于不可逆,所以不会再有变回0的机会)
  2. 当前元素后面没有k-1个元素了,并且它还没变成0(那就执行不了那个k个元素-1的操作)
class Solution {
    public boolean checkArray(int[] nums, int k) {
        int n = nums.length;
        int[] diff = new int[n];
        diff[0] = nums[0];
        if(k<n){
            diff[k]-=nums[0];
        }
        int tmp = diff[0];
        for(int i=1;i<n;i++){
            tmp += diff[i];
            if(tmp>nums[i]||tmp<nums[i]&&i>n-k){
                return false;
            }
            int dis = nums[i]-tmp;
            if(dis!=0){
                tmp+=dis;
                if(i+k<n){
                    diff[i+k]-=dis;
                }
            }
        }
        return true;
    }
}

6. LC 2528 最大化城市的最小电量

这题有combo的,就是上面的第五题(乐

首先看到最大化最小值,很显然二分板子。

  1. 单调性:对于任意最小电量m
    1. 若建造k个电站可以满足最小电量为m,则显然也能满足<m
    2. 若建造k个电站不能满足最小电量为m,则显然也不能满足>m
  2. 上限:初始最小电量+k
  3. 下限:初始最小电量
  4. 检查:这个检查可以转换为:每次能选一个长度最长为2r+1的子数组,让里面的所有的数++,能否在用完k次操作前/时将数组中的每个值都补到>=mid?那么就立马变成了上面那道第五题。

初始我们要预计算每个电站的初始电量,不然不知道差每次二分的mid多少。这个很显然也是差分前缀和板子,和前面第一第二题很像,就是一个区间增量的板子。同时还可以在前缀和还原时维护上述所说的最小电量。

import java.util.ArrayList;
import java.util.List;

class Solution {
    static long min;
    public long maxPower(int[] stations, int r, int k) {
        min = Long.MAX_VALUE;
        long[] iP= calPower(stations, r);
        long lp,rp,mid,ans;
        lp = min;
        rp = min+k+1;
        ans = lp;
        while(lp<rp){
            mid = ((rp-lp)>>>1)+lp;
            if(check(iP,mid,r,k)){
                ans = mid;
                lp = mid+1;
            }else{
                rp = mid;
            }
        }
        return ans;
    }

    private boolean check(long[] iP,long mid,long r,long k){
        // 每次能选一个长度最长为2r+1的子数组,让里面的所有的数++,能否在用完k次操作前/时将数组中的每个值都补到>=mid?
        // 只能++不能--,说明操作不可逆,一旦达到>=mid,就可以立即丢弃了
        long[] diff = new long[iP.length];
        long dt = 2*r+1;
        if(iP[0]<mid){
            diff[0] = mid-iP[0];
            if(dt<iP.length){
                diff[(int) dt]-=diff[0];
            }
            k -= diff[0];
            if(k<0){
                return false;
            }
        }
        long tmp = iP[0]+diff[0];
        long dis;
        for (int i = 1; i < iP.length; i++) {
            tmp += diff[i]+iP[i]-iP[i-1];
            if(tmp<mid){
                dis = mid - tmp;
                k-=dis;
                if(k<0){
                    return false;
                }
                tmp+=dis;
                if(i+dt<diff.length){
                    diff[(int) (i+dt)]-=dis;
                }
            }
        }
        return true;
    }

    private long[] calPower(int[] stations,int radius){
        int n = stations.length;
        long[] res = new long[n];
        int lp,rp;
        for (int i = 0; i < stations.length; i++) {
            lp = Math.max(0,i-radius);
            res[lp] += stations[i];
            rp = i + radius + 1;
            if(rp <n){
                res[rp] -= stations[i];
            }
        }
        for (int i = 1; i < res.length; i++) {
            res[i] += res[i-1];
            if(res[i]<min){
                min = res[i];
            }
        }
        min = Math.min(min,res[0]);
        return res;
    }
}

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值