前缀数组和后缀数组

前缀数组和后缀数组是算法竞赛中非常实用的技巧,能够有效地优化许多问题的解法。下面我将详细讲解这两种技术,并结合5个例题进行分析。

一、前缀数组(Prefix Sum Array)

基本概念

前缀数组是一种预处理技术,通过预先计算并存储数组前i个元素的和(或其他累积操作结果),可以在O(1)时间内查询任意区间的和。

构建方法

对于一个数组a[0..n-1],其前缀数组prefix定义为:

  • prefix[0] = a[0]
  • prefix[i] = prefix[i-1] + a[i] (对于i > 0)

应用场景

  1. 快速计算区间和:sum(a[l..r]) = prefix[r] - prefix[l-1](注意边界条件)
  2. 解决子数组相关问题
  3. 结合其他算法优化时间复杂度

例题1:简单前缀和(LeetCode 303. 区域和检索 - 数组不可变)

问题描述:给定一个整数数组,多次查询区间和。

解法

class NumArray {
private:
    vector<int> prefix;
public:
    NumArray(vector<int>& nums) {
        prefix.resize(nums.size()+1, 0);
        for(int i=0; i<nums.size(); ++i) {
            prefix[i+1] = prefix[i] + nums[i];
        }
    }
    
    int sumRange(int left, int right) {
        return prefix[right+1] - prefix[left];
    }
};

二、后缀数组(Suffix Sum Array)

基本概念

后缀数组与前缀数组类似,但是是从数组末尾开始计算的累积和。

构建方法

对于一个数组a[0..n-1],其后缀数组suffix定义为:

  • suffix[n-1] = a[n-1]
  • suffix[i] = a[i] + suffix[i+1] (对于i < n-1)

应用场景

  1. 需要从后向前处理的问题
  2. 某些特定类型的区间查询
  3. 与前缀数组结合解决更复杂问题

例题2:后缀和简单应用

问题描述:计算从每个位置开始到数组末尾的所有元素的和。

解法

vector<int> calculateSuffixSum(vector<int>& nums) {
    int n = nums.size();
    vector<int> suffix(n);
    suffix[n-1] = nums[n-1];
    for(int i=n-2; i>=0; --i) {
        suffix[i] = nums[i] + suffix[i+1];
    }
    return suffix;
}

三、进阶例题

例题3:中等难度 - 最大子数组和(LeetCode 53. 最大子数组和)

问题描述:找到一个具有最大和的连续子数组。

解法(Kadane算法,可以看作前缀和思想的变种):

int maxSubArray(vector<int>& nums) {
    int max_sum = INT_MIN;
    int current_sum = 0;
    for(int num : nums) {
        current_sum = max(num, current_sum + num);
        max_sum = max(max_sum, current_sum);
    }
    return max_sum;
}

例题4:中等难度 - 除自身以外数组的乘积(LeetCode 238. 除自身以外数组的乘积)

问题描述:不使用除法,计算数组中每个元素除自身外的乘积。

解法(前缀积+后缀积):

vector<int> productExceptSelf(vector<int>& nums) {
    int n = nums.size();
    vector<int> res(n, 1);
    
    // 计算前缀积
    int prefix = 1;
    for(int i=0; i<n; ++i) {
        res[i] = prefix;
        prefix *= nums[i];
    }
    
    // 计算后缀积并与前缀积相乘
    int suffix = 1;
    for(int i=n-1; i>=0; --i) {
        res[i] *= suffix;
        suffix *= nums[i];
    }
    
    return res;
}

例题5:较难 - 寻找最小正数缺失(LeetCode 41. 缺失的第一个正数)

问题描述:找到数组中缺失的最小正整数。

解法(利用数组本身作为标记的前缀思想):

int firstMissingPositive(vector<int>& nums) {
    int n = nums.size();
    
    // 第一次遍历,将数字放到正确的位置上
    for(int i=0; i<n; ) {
        if(nums[i] > 0 && nums[i] <= n && nums[nums[i]-1] != nums[i]) {
            swap(nums[i], nums[nums[i]-1]);
        } else {
            i++;
        }
    }
    
    // 第二次遍历,找到第一个位置不对的数字
    for(int i=0; i<n; ++i) {
        if(nums[i] != i+1) {
            return i+1;
        }
    }
    
    return n+1;
}

四、前缀数组和后缀数组的高级应用

1. 二维前缀和

用于快速计算矩阵中子矩阵的和。

构建方法

vector<vector<int>> buildPrefix2D(vector<vector<int>>& matrix) {
    int m = matrix.size(), n = matrix[0].size();
    vector<vector<int>> prefix(m+1, vector<int>(n+1, 0));
    for(int i=1; i<=m; ++i) {
        for(int j=1; j<=n; ++j) {
            prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + matrix[i-1][j-1];
        }
    }
    return prefix;
}

int queryPrefix2D(vector<vector<int>>& prefix, int row1, int col1, int row2, int col2) {
    return prefix[row2+1][col2+1] - prefix[row1][col2+1] - prefix[row2+1][col1] + prefix[row1][col1];
}

2. 差分数组

前缀数组的逆操作,用于高效处理区间更新。

构建方法

vector<int> buildDiffArray(vector<int>& nums) {
    int n = nums.size();
    vector<int> diff(n);
    diff[0] = nums[0];
    for(int i=1; i<n; ++i) {
        diff[i] = nums[i] - nums[i-1];
    }
    return diff;
}

void rangeUpdate(vector<int>& diff, int l, int r, int val) {
    diff[l] += val;
    if(r+1 < diff.size()) diff[r+1] -= val;
}

vector<int> getOriginalArray(vector<int>& diff) {
    vector<int> res(diff.size());
    res[0] = diff[0];
    for(int i=1; i<diff.size(); ++i) {
        res[i] = res[i-1] + diff[i];
    }
    return res;
}

五、总结

前缀数组和后缀数组是算法竞赛中非常强大的工具,它们能够:

  1. 将许多O(n)的查询操作优化为O(1)
  2. 简化复杂问题的解法
  3. 与其他算法结合产生更高效的解决方案

掌握这些技术需要:

  • 理解其基本原理
  • 熟练编写构建和查询代码
  • 识别哪些问题可以应用这些技术
  • 多做练习,积累经验

通过这5个例题的讲解,你应该对前缀数组和后缀数组有了更深入的理解。在实际比赛中,要灵活运用这些技术,有时还需要进行适当的变通才能解决更复杂的问题。

posted @ 2025-05-26 17:40  Thin_time  阅读(261)  评论(0)    收藏  举报