基本概念
前缀和
假设有数组/序列/列表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]}被称为差分数组。
差分的性质
-
差分数组的前缀和就是原数组在当前索引的值(前提是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 -
操作压缩
原数组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 非零段划分
题面:

样例:
- 输入
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
- 输出
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,求最大前缀和(维护可以增加的峰值数量,求最多几个峰值)
两个细节:
-
两次操作(上文提及过的)
就以上述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)
-
更新差分数组的时机
根据上述成为峰值的必要条件,更新时机在于:
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引出了个人对差分前缀和算法的理解:记账式算法。
回顾以下整个过程,从一开始:
- 0,3 记账:p=1:3可以让3露出来,这个p值能使总结果增益,diff(1)+=1;但p≥4的话3就露不出来了,这个值会使总结果损益,diff(4)-=1
- 3,1 记账:3,1无论如何1都不能成为峰值,不用记账
- 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
思路分析:
看到区间反复查询(每个阈值都要查询一遍所有挂科情况),基本上确定是差分+前缀和。这题还稍微有点不一样的点在于还多了一个排序。可以利用有序这个性质进行线性搜索。
样例给的比较好,直接看样例。
既然有区间范围内反复查询,那么就先把样例的区间查询结果列出来看看有啥规律没。下面用√表示预测正确,用×表示预测错误。
因为阈值从安全指数的值域中挑选,所以直接遍历所有安全指数:
| 实际值 | 0 | 1 | 3 | 5 | 7 |
| 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);
}
}
1603

被折叠的 条评论
为什么被折叠?



