差分+前缀和

基本概念

前缀和

假设有数组/序列/列表a={a[0],a[1],…,a[n]},定义前缀和b[i]:

b[i] = a[0] + a[1] + … + a[i]

b = {b[0],b[1],b[2],…,b[n]}被成为前缀和数组

差分

同样有数组a。

定义差分c[i]:

c[i] = a[i] - a[i-1] ,其中i≥1

c = {c[1],c[2],…,c[n]}被称为差分数组

差分的性质

  1. 差分数组的前缀和就是原数组在当前索引的值(前提是a[0]=0)

    这是比较好证明的。由定义:

    c[1] = a[1] - a[0]
    c[2] = a[2] - a[1]
    ...
    c[i] = a[i] - a[i-1],i>=1
    上式累加得
    sum(c,1,i) = a[i] - a[0] = a[i],a[0] = 0
    
  2. 操作压缩

    原数组a:(索引从0开始)

    a={0,1,2,3,4,5,6,7,8,9};
    

    则有差分数组diff:

    diff={0,1,1,1,1,1,1,1,1,1}
    

    按理说差分数组长度要比原数组少1,但是这里为了后续方便将c[0]定义为0了

    假设我们想要对a[2:7]进行操作+=5。如果循环操作就是6次操作,但如果利用差分数组,就可以压缩到2次。

    原理:差分数组代表每个元素相对于上一个元素的偏移量,修改一个偏移量,将对后续所有元素产生影响。

    将操作压缩到两次:

    diff[2]+=5;
    diff[8]-=5;
    

    注意这里之所以要diff[8]-=5,是因为我们只想让a[2:7]受到影响,2之前以及7之后都不要动。第一行保证了2以后的都加了5,第二行保证这个操作截止到7结束。

    新的差分数组:

    diff={0,1,6,1,1,1,1,1,-4,1}
    

    新数组:(利用前缀和计算)

    a={0,1,7,8,9,10,11,12,8,9};
    

csp202109_2 非零段划分

题面:

样例:

  1. 输入
11
3 1 2 0 0 2 0 4 5 0 2

14
5 1 20 10 10 10 10 15 10 20 1 5 10 15

3
1 0 0

3
0 0 0
  1. 输出
5

4

1

0

思路分析:

这个题有板子的,将题面转换为:

给出高度数组A,其中每一个元素代表x轴对应的y值,求p,使得当水面淹没p高度以下的陆地时,露出水面的峰值最高。

使用matlab画了个二维图。

x = 0:11
A = [0,3,1,2,0,0,2,0,4,5,0,2]

注意开始值不是3,是0(这一点在代码实现中也有所体现),因为如果把3按照初始值开始算,就漏掉了3这个峰值!

成为峰值的一个必要条件:前一个值比后一个值小。

定义差分diff(i)的含义为:在相邻两片陆地间,当水面淹没了高度低于i的陆地时,可以增加的峰值数量。

这样sum(diff,0,i)的含义为:当水面淹没了高度i以下的所有陆地时,总的峰值数量。

目标:更新diff,求最大前缀和(维护可以增加的峰值数量,求最多几个峰值)

两个细节:

  1. 两次操作(上文提及过的)

    就以上述A[0]和A[1]举例好了,这两个值分别为0,3。

    鉴于p的实际意义(淹没p高度以下的陆地),所以p≥1(陆地高度总要≥0)

    当p=1,2,3,会增加1个峰值

    但是,当p=4,就不会增加峰值了!

    因此,两次操作分别为diff(1)+=1和diff(4)-=1。(别忘了操作差分数组相当于操作了偏移量,操作一个偏移量对后续所有的元素都将产生同样的操作)

    注意比较两个值的时候,峰值的变化量至多为1(这里相当于操作是+-=1)

  2. 更新差分数组的时机

    根据上述成为峰值的必要条件,更新时机在于:

    A[i-1] < A[i]
    

    可能有人问了,为什么不是A[i-1] < A[i] && A[i] > A[i+1]?毕竟这个是充要条件啊?

    这里以A中的一个子数组{0,4,5}为例。

    先看0,4,p取1:4都没问题,+1;但p=5的时候4带来的峰值不复存在,-1。

    两次操作:

    diff(1)+=1;
    diff(5)-=1;
    

    再看4,5。两次操作:

    diff(5)+=1;
    diff(6)-=1;
    

    这两次更新对于diff(5)带来的结果是:不变。

    这是因为我们更新的东西是diff,它的含义是:当水面淹没了高度低于i的陆地时,可以增加的峰值数量。

    前一次更新,我们认为p>4时(p≥5),将导致0,4这两片陆地的4这个峰值不复存在。(因为它被淹没了)

    后一次更新,我们认为p≤5时,将导致4,5这两片陆地的5这个峰值得以显现。

    两次更新合起来就是:

    diff(1)+=1;
    diff(6)-=1;
    

    换句话说,前一次更新我们在账上记了,p≥5的时候峰值4不存在了;后一次我们在账上记了,p≤5时峰值5显现。也就是说,4和5带来的峰值数量的增/损益是一样的,你想让4露出来(p<5),5就不算一个峰值(diff(5)-=1);你想让5露出来(p≥5),4就成不了一个峰值(被淹没)(前一次操作不存在)。

    现在来看看如果我们按充要条件走,也就是{4,5,0}这个子数组。

    对于4,5,0来说,p=1:5都没问题,+1;p≥6不行,-1;

    两次操作为:

    diff(1) += 1;
    diff(6) -= 1;
    

    这和之前两次更新的四次操作合并后的结果完全一样。

    这不是一个数值上的巧合,是有现实意义的。

由上述细节2引出了个人对差分前缀和算法的理解:记账式算法。

回顾以下整个过程,从一开始:

  1. 0,3 记账:p=1:3可以让3露出来,这个p值能使总结果增益,diff(1)+=1;但p≥4的话3就露不出来了,这个值会使总结果损益,diff(4)-=1
  2. 3,1 记账:3,1无论如何1都不能成为峰值,不用记账
  3. 1,2 记账:p=2:2可以让2露出来,这个p值能使总结果增益,diff(2)+=1;但p≥3的话2就露不出来了,这个值会使总结果损益,diff(3)-=1

假设一个人在一个酒馆打工(打工能赚钱),但他同时也在酒馆喝酒(喝酒要花钱),每次他日结,我们就在他账上记一笔(打工赚的钱);每次他喝酒,我们就在他账上减一笔。这样我们就得到了一个他在酒馆的行为记录了(横轴是不同次的操作,可能是打工,也可能是喝酒,但都是一次操作;纵轴是每次操作带来的增/损益)。如果某一天我们希望能结算这个人在酒馆的总负债,那么把这些操作对应的增/损益进行加和,就知道他在酒馆赚了多少钱了。

非零段划分/峰值数量,也是一个道理,每遇到2个值,我们就尝试记录这两个值在一定范围(p)内为峰值带来的增/损益,这个增/损益用差分数组记录,为了得到每个p对应的峰值数量,进行前缀求和即可。

代码实现:

package com.csp202109.csp202109_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        Integer[] narr = brReadLine(br);
        Integer n = narr[0];
        Integer[] temp = brReadLine(br);
        Integer[] nums = new Integer[n+1];
        nums[0]=0;
        for (int i = 1; i < nums.length; i++) {
            nums[i]=temp[i-1];
        }
        output(
                maxNumOfNonZeroSegment(n,nums)
        );
    }

    private static Integer[] brReadLine(BufferedReader br) throws IOException {
        return Arrays.stream(br.readLine()
                .split(" "))
                .map(Integer::parseInt)
                .toArray(Integer[]::new);
    }

    private static int maxNumOfNonZeroSegment(int n,Integer[] nums){
        int diffLen = 0;
        for (Integer num : nums) {
            diffLen = Math.max(num,diffLen);
        }
        int[] diff = new int[diffLen+2];
        for (int i = 1; i <= n; i++) {
            if(nums[i-1]<nums[i]){
                add(nums[i-1]+1,nums[i],1,diff);
            }
        }
        int max = 0,pre = 0;
        for(int p=0;p<=diffLen+1;p++){
            pre+=diff[p];
            max = Math.max(max,pre);
        }
        return max;
    }

    private static void add(int l,int r,int offset,int[] diff){
        diff[l]+=offset;
        diff[r+1]-=offset;
    }

    private static void output(int ans){
        System.out.println(ans);
    }

}

csp202012_2 期末预测之最佳阈值

题面:

简单来说:就是从一堆安全指数中挑一个出来作为阈值,在有监督的情况下(是否真的挂科就是监督)用这个阈值预测这些安全指数是否挂科。选取预测最准的一个阈值作为结果输出(同样准选较大者)

样例:

input:
6
0 0
1 0
1 1
3 1
5 1
7 1
output:
3

input:
8
5 1
5 0
5 0
2 1
3 0
4 0
100000000 1
1 0
output:
100000000

思路分析:

看到区间反复查询(每个阈值都要查询一遍所有挂科情况),基本上确定是差分+前缀和。这题还稍微有点不一样的点在于还多了一个排序。可以利用有序这个性质进行线性搜索。

样例给的比较好,直接看样例。

既然有区间范围内反复查询,那么就先把样例的区间查询结果列出来看看有啥规律没。下面用√表示预测正确,用×表示预测错误。

因为阈值从安全指数的值域中挑选,所以直接遍历所有安全指数:

实际值01357
0 挂科×
1 挂科××
1 没挂×××
3 没挂××
5 没挂×
7 没挂

样例给的好的原因也就在这里:他按照安全指数升序排列的。因此一个阈值只可能影响比它低的安全指数,比它高的它影响不到。因此我们在进行差分前缀前也要注意按安全指数升序。

比如,如果将3作为阈值。那么它只会改变前一列(也就是阈值为1的时候)对区间中{1,1}两个元素的查询结果,它本身以及后续的元素都是不受影响的。

同时,它也不会对0产生影响,这是很重要的一点,这一点导致算法是O(n)的,而非O(n²)的(不然对于每一个阈值,我们都要查询他前方所有的元素,那就变成n(n+1)/2的步数了)。

之所以不对0产生影响,是因为1已经修改完0了,比如1这一列修改了0作为√,意思就是阈值≥1时安全指数为0代表挂科,那么3自然也是挂科的,所以阈值为3时根本不用查询这一项。这就对应了差分的操作压缩:操作偏移量将对后续所有元素产生影响。

那么差分数组中diff(i)的意义就是:当阈值为safety(i)时,相对于safety(i-1),将多产生diff(i)个正确的预测结果。(当然可以≤0,也就是少了多少个正确的预测结果)

比如,diff(1) = 1。(0改对了,前一个1没改对,或者说就没改,后一个1没改)

那怎么统计这个差分呢?以{0,1}为例,如果把阈值改为1,那么0的挂科与否的判断就要修改了,从不挂科修改为挂科,所以0的预测结果的正确性是可能得到改变的,但修改的方向是定死的,也就是一定会把前面元素的挂科预测结果修改为挂科。

那么我们直接查询0对应的真实考试结果,判断它是否符合预测,也即

result[0] == 0

如果上式为真,那说明我们改对了,diff(1)+1;否则没改对,diff(1)不变。

需要注意的是,正如之前提到的,3修改1的预测结果时,是不用管0的,因此我们每次查询判断时查询的是一个子区间,而不是前面所有的元素。比如3修改时查询判断的是{1,1}两个元素。这是因为后面一个1不会修改前面一个1的预测结果(同一个阈值预测结果不变),所以这两个1都需要等到3来修改。

也就是说,每次我们尝试一个阈值时,都要保证它是一个新阈值(和之前那个阈值不一样),保证了这个阈值有更新前面预测结果的权力后,才能安心查询判断。

可以用一个寄存器idx来保存上一个阈值的起始为止,在上述例子中(3更新1时),idx=1,而3的索引为3,因此需要查询idx=1:2两个元素,也就是{1,1},逐一判断他们是否修改对了,对了就对diff进行累加。

最后,前缀和,得到准确度最高的预测结果对应的阈值。

代码如下:

package com.csp202012.csp202012_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        Integer[] n = brReadLine(br);
        Integer[][] yResult = new Integer[n[0]][2];
        for (int i = 0; i < n[0]; i++) {
            yResult[i] =  brReadLine(br);
        }
        output(
                bestSita(yResult,n[0])
        );
    }

    private static Integer[] brReadLine(BufferedReader br) throws IOException {
        return Arrays.stream(br.readLine()
                .split(" "))
                .map(Integer::parseInt)
                .toArray(Integer[]::new);
    }

    private static int bestSita(Integer[][] yResult, int n){
        int ans = -1;
        int cur = 0;
        int max = 0;
        Arrays.sort(yResult,(o1,o2)->{
            if(o1[0]!=o2[0]){
                return Integer.compare(o1[0],o2[0]);
            }else{
                return Integer.compare(o1[1],o2[1]);
            }
        });
        Integer[] diff = new Integer[yResult[yResult.length-1][0]+1];
        Arrays.fill(diff,null);
        for (Integer[] integers : yResult) {
            diff[integers[0]]=0;
        }
        for (Integer[] nums : yResult) {
            if(nums[1]==1){
                cur++;
            }
        }
        int idx=0;
        for(int i=1;i<n;i++){
            int sita = yResult[i][0];
            if(yResult[i][0]==yResult[i-1][0]){
                continue;
            }
            for(int j=i-1;j>=idx;j--){
                if(yResult[j][1]!=0){
                    diff[sita]--;
                }else{
                    diff[sita]++;
                }
            }
            idx = i;
        }
        for (int i = 1; i < diff.length; i++) {
            if(diff[i]!=null){
                cur+=diff[i];
                if(cur>=max){
                    ans = i;
                    max = cur;
                }
            }
        }
        return ans;
    }

    private static void output(int ans){
        System.out.println(ans);
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值