算法技巧——差分、前缀和

一.差分

差分数组是解决某类区间的有力工具,当我们面临需要频繁对原数组的某个区间内的元素进行增减操作时,直接操作会导致时间复杂度过高。这时,差分数组就能发挥巨大作用。

基本概念

差分数组d[ ]用于表示相邻元素之间的差值,对于任意的数组a[ ],其差分数组d[ ]可以表示为:

d[i]=a[i]-a[i-1];

而对于数组的第一个元素,我们可以定义d[0]=a[0];

构建差分数组

假设我们有一个原始数组a[ ],我们可以按照以下方式构建差分数组d[ ];

int[] a={5,3,6,4}; //原始数组
int[] d=new int[a.length];

//构建差分数组
d[0]=a[0];//第一个元素不变
for(int i=1;i<a.length;i++){
    d[i]=a[i]-a[i-1];
}

原始数组的第一个元素不变,后面的每一个元素减前一个元素,所得到的数组即为差分数组 

区间增减操作

差分数组最大的优势在于处理区间操作,如果我们想对a[l~r]区间内所有元素增加一个值x,我们只需要对差分数组做以下操作:

d[l]+=x;
if(r+1<a.length){
    d[r+1]-=x;
}    

这样,当我们通过差分数组重新计算原数组时,区间[l,r]内所有元素都增加了x,而其他元素保持不变。(简单来说就是若要使区间内的所有元素都增加一个数x,则区间内第一个数(l)增加x,区间后的第一个数(r+1)减去x

还原原数组

通过对差分数组的的一系列操作,我们可以通过以下代码来还原出修改后的原数组:

a[0]=d[0];//第一个元素依然是差分数组的第一个元素
for(int i=1;i<a.length;i++){
    a[i]=d[i]+a[i-1];
}

修改后的原数组,第一个元素等于差分数组中的第一个元素,之后的每一个元素等于修改后原数组的前一个元素a[i-1]加上差分数组中对应的元素(d[i]) 

示例:

//nums数组是之前已经定义的一个数组(原数组)

int diff=new int[nums.length]; //定义差分数组

//构造差分数组
diff[0]=num[0];
for(int i=1;i<nums.length;i++){
    diff[i]=nums[i]-nums[i-1];
}

//对原数组进行加操作
int left=1; //区间左端点:索引1
int right=4; //区间右端点:索引4
int value=2; //区间内所有元素加的数值

diff[left]+=value;
if(right+1<nums.length){
    diff[right+1]-=value;
}

//根据差分数组回复原数组
int [] res=new int[nums.length];
res[0]=diff[0];
for(int i=1;i<diff.length;i++){
    res[i]=res[i-1]+diff[i];
} 

二.前缀和

        前缀和是一种常见且实用的算法技巧,它主要用于处理数组区间求和问题。简而言之,前缀和就是一个数组的某个下标之前的所有元素之和。

应用场景:

前缀和算法适用于数组不经常变动而频繁查询某个区间内元素的总和的场景。如处理多个查询请求,每个请求要求输出数组中某个区间的元素和

和差分算法的区别

前缀和算法和差分算法从某种程度上来说互为逆运算,其在代码上也十分相似,但主要在于目的不同;前缀和算法是直接基于原数组,算出某个区间的所有元素之和。而差分算法是在原数组上进行操作,最后得到一个新的数组。

前缀和的计算方法

int[] arr={a1,a2,a3,...,an};
int[] sum=new int[n+1];//前缀和数组

sum[0]=0;//前缀和数组第一个直接赋值为0
for(int i=1;i<=n;i++){
    sum[i]=sum[i-1]+arr[i-1];
}

使用前缀和查询区间和

要查询数组arr从下标L到下标R的区间和(即[L,R]),可以用两个前缀和相减的方式实现:

int sum_L_to_R=sum[R+1]-sum[L];

快速生成前缀和数组方法

在算法题目中,我们会遇到不需要原数组,只需要前缀和数组的情况。我们可以使用以下代码:

//正常读取用户输入的数字
for(int i=1;i<=n;i++)
    s[i]=scanner.nextInt();

//生成前缀和数组
for(int i=1;i<=n;i++)
    s[i]+=s[i-1];

注意事项

·前缀和数组大小是原数组大小加一,以便处理从数组首元素开始的区间查询

·如果数组发生更新,则需要重新计算前缀和数组,或者进行差分处理

·对于多位数组,可以使用类似的方法计算多维前缀和

示例:

public class PrefixSum2D {
    private int[][] prefixSum;

    // 构建二维前缀和
    public PrefixSum2D(int[][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;
        prefixSum = new int[row + 1][col + 1];

        for (int i = 1; i <= row; i++) {
            for (int j = 1; j <= col; j++) {
                prefixSum[i][j] = matrix[i - 1][j - 1] + prefixSum[i - 1][j] + prefixSum[i][j - 1] - prefixSum[i - 1][j - 1];
            }
        }
    }

    // 查询子矩阵的和
    public int querySubmatrixSum(int row1, int col1, int row2, int col2) {
        return prefixSum[row2 + 1][col2 + 1] - prefixSum[row1][col2 + 1] - prefixSum[row2 + 1][col1] + prefixSum[row1][col1];
    }

    // 打印二维前缀和数组,用于验证
    public void printPrefixSum() {
        for (int i = 1; i < prefixSum.length; i++) {
            for (int j = 1; j < prefixSum[0].length; j++) {
                System.out.print(prefixSum[i][j] + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        int[][] matrix = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };

        PrefixSum2D prefixSum2D = new PrefixSum2D(matrix);

        // 查询从(1, 1)到(2, 2)子矩阵的和
        int submatrixSum = prefixSum2D.querySubmatrixSum(1, 1, 2, 2);
        System.out.println("Sum of submatrix from (1,1) to (2,2): " + submatrixSum);

        // 验证前缀和数组是否正确
        System.out.println("Prefix Sum Array:");
        prefixSum2D.printPrefixSum();
    }
}

前缀和差分是两种非常实用的数据处理技巧,在解决数组、矩阵等结构上的查询和修改问题上尤为高效。 ### 一、前缀和 **概念** 对于一个给定的序列 `a[0], a[1], ..., a[n]`,其前缀和是指从序列开始到当前位置所有元素之和所形成的新的序列 `sum[i] = sum(a[0] + ... + a[i])`。构建这个新序列的过程就是计算前缀和的操作。 - **应用场景**:例如在一个包含大量数据的日志文件里快速求某段连续时间内的总流量;或者是在图像处理中用于加速区域求和操作。 - **优点**:通过预先计算并存储每个位置截至当前的所有值累加结果(即前缀和),可以将原本需要O(n)复杂度才能完成的一系列区间求和任务降低至常数级别的时间开销。 #### 示例说明: 假设有一个整型列表 `[1, 2, 3, 4, 5]` ,我们先构造它的前缀和数组: ``` 原始数组 : [1 , 2 , 3 , 4 , 5] 前缀和 : [1 , 3 (=1+2), 6 (=1+2+3), 10(=1+...+4), 15 ] ``` 如果想要知道原数组第2位到第4位之间数字相加之和,则可以直接利用已有的前缀和信息得出答案为 `(prefixSum[4]-prefixSum[1]=15 - 3 = 12)` 而不需要再次遍历这部分数值了。 --- ### 二、差分 **概念** 若有一组初始状态的离散点集合 `{x₁,x₂,...xn}` 和对应的目标变化量集{d₁,d₂,...dn},则可通过一次性的批量更新达到预期效果的一种算法思想就叫做“差分”。通常情况下会涉及到两个步骤: 1. 对于每次指定范围[l,r]内增加一定增量delta而言,并不是直接对每一个元素都加上该增值而是只改变边界处的位置:`diff[l]+= delta; diff[r+1]-= delta`; 2. 当完成了所有的调整指令之后再基于上述得到的临时差异表去恢复最终的结果。 简单来说,“差分”的核心在于它允许我们在不真正访问那些待变动节点的情况下间接地影响它们——这大大减少了不必要的重复工作量同时也提高了效率。 #### 应用实例: 考虑这样一个场景 —— 您拥有一条直线上若干站点组成的公交线路图示以及每站上下车人数统计表格。现在要求能够即时响应如下的两类请求:“在某一特定时间段内有多少名乘客登上了公交车?”、“向系统提交一份计划书表明未来几天某些班次将会额外增加载客容量。” 这种时候就可以借助差分技术轻松解决问题啦! --- 综上所述,这两种方法都能有效简化复杂的累积运算过程并且大幅提升了程序运行性能,因此掌握好这两项技能对于我们编写高效的代码有着重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值