前缀和

                     

贡献者: 有机物

   前缀和算法分为一维和二维,一维前缀和可以很快速的求序列中某一段的和。而二维前缀和可以快速求一个矩阵中某个子矩阵的和(以下讲的是以 $x_1, y_1$ 为左上角,$x_2, y_2$ 为右下角的子矩阵)。

1. 一维前缀和

   一维前缀和的朴素做法是一个 for 循环求和,时间复杂度为:$\mathcal{O}(n \times m)$。$m$ 为询问次数。

   而前缀和的时间复杂度为:预处理 $\mathcal{O}(n)$,查询 $\mathcal{O}(m)$。

   一维前缀和:

\begin{equation} \sum^{r}_{i=l}a_i~, \end{equation}

   预处理:

\begin{equation} S_i=\sum^{i}_{k=1}a_k~. \end{equation}

   如何预处理?

\begin{equation} \because \sum^{i}_{k=1}a_k-\sum^{i-1}_{k=1}a_{k}=a_k \\ \therefore \sum^{i}_{k=1}a_k=\sum^{i-1}_{k=1}a_{k}+a_k ~,\\ \end{equation}

   即:

\begin{equation} \\S_k-S_{k-1}=a_k\\ \\S_k=S_{k-1}+a_k~.\\ \end{equation}

   如何求答案?

\begin{equation} \sum^{r}_{i=l}a_i=\sum^{r}_{i=1}a_i-\sum^{l-1}_{i=1}a_i=S_r-S_{l-1}~. \end{equation}

   还有要注意的是 $S_0 = 0$,因为如果想求 $1 \sim n$ 一段数的和可以直接输出 $S_{10} - S_0 = S_{10} $。因此可以从下标为 $1$ 开始存数,这样就不需要做复杂的处理。

   朴素代码

const int N = 100010;
int a[N];

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];
    
    while (m -- )
    {
        int l, r;
        cin >> l >> r;
        
        int res = 0;
        for (int i = l; i <= r; i ++ ) res += a[i];
        cout << res << endl;
    }
    
    return 0;
}

   前缀和

const int N = 100010;
int n, m, a[N], s[N];

int main() 
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];
    for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
    
    while (m -- )
    {
        int l, r;
        cin >> l >> r;
        cout << s[r] - s[l - 1] << endl;    // 前缀和公式,O(1)
    }
    
    return 0;
}

2. 二维前缀和

   二维前缀和的朴素做法是两重循环暴力求和。时间复杂度为:$\mathcal{O}(q \times nm)$。$q$ 为询问次数,$n$、$m$ 为矩阵大小。

   而二维前缀和的时间复杂度为 $\mathcal{O}(n \times m)$

   二维前缀和:

\begin{equation} \sum^{x2}_{i = x1}\sum^{y2}_{j = y1} a_{ij}~, \end{equation}

   预处理:

\begin{equation} S_{ij} = \sum^{i}_{k_1 = 1} \sum^{j}_{k_2 = 1} a_{k_1 k_2}~. \end{equation}

   如何预处理?

   在预处理 $S_{ij}$ 时,要注意 $S_{i - 1, m}$ 和 $S_{i, j - 1}$ 都已经被算过了,因为预处理是逐行逐列来计算的(重复地方不重复计算)。

\begin{equation} S_{ij} = \sum^{i}_{k_1 = 1} \sum^{j}_{k_2 = 1} a_{k_1 k_2} = \sum^{i - 1}_{k_1 = 1} \sum^{j}_{k_2 = 1} + \sum^{i}_{k_1 = 1} \sum^{j - 1}_{k_2 = 1} - \sum^{i- 1}_{k_1 = 1} \sum^{j - 1}_{k_2 = 1} + a_{i, j}~, \end{equation}

   即:

\begin{equation} S_{i, j} = S_{i - 1, j} + S_{i, j - 1} - S_{i - 1, j - 1} + a_{i, j}~. \end{equation}

   如何求答案?

\begin{equation} \sum^{x_2}_{i = x_1}\sum^{y_2}_{j = y_1} a_{ij} = \sum^{x_2}_{k_1 = 1} \sum^{y_2}_{k_2 = 1} a_{k_1 k_2} - \sum^{x_1 - 1}_{k_1 = 1} \sum^{y_2}_{k_2 = 1} a_{k_1 k_2} - \sum^{x_2}_{k_1 = 1} \sum^{y_1 - 1}_{k_2 = 1} a_{k_1 k_2} + \sum^{x_1 - 1}_{k_1 = 1} \sum^{y_1 - 1}_{k_2 = 1} a_{k_1 k_2}~, \end{equation}

   即:

\begin{equation} S_{x_2, y_2} - S_{x_1 - 1, y_2} - S_{x_2, y_1 - 1} + S_{x_1 - 1, y_1 - 1}~. \end{equation}

   二维前缀和可能有点抽象,可以根据几张图片来对二维前缀和有个更好的理解。

图
图 1:预处理 $1$
图
图 2:预处理 $2$
图
图 3:预处理 $3$
图
图 4:预处理 $4$
图
图 5:预处理 $5$
图
图 6:预处理 $6$

   求前缀和的公式类似推导预处理的过程。

   朴素代码

const int N = 1010;
int n, m, q, a[N][N], s[N][N];

int main()
{
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &a[i][j]);
    
    while (q -- ) 
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        
        int res = 0;
        for (int i = x1; i <= x2; i ++ )
            for (int j = y1; j <= y2; j ++ )
                res += a[i][j];
        
        cout << res << endl;
    }
    
    return 0;
}

   二维前缀和代码:

const int N = 1010;
int n, m, q, a[N][N], s[N][N];

int main()
{
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            scanf("%d", &a[i][j]);
            s[i][j] = s[i - 1][j] + s[i][j - 1] 
            - s[i - 1][j - 1] + a[i][j];    // 预处理公式
        }
        
    while (q -- )
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", 
        s[x2][y2] - s[x1 - 1][y2] 
        - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);  // 前缀和公式
    }
    
    return 0;
}


致读者: 小时百科一直以来坚持所有内容免费无广告,这导致我们处于严重的亏损状态。 长此以往很可能会最终导致我们不得不选择大量广告以及内容付费等。 因此,我们请求广大读者热心打赏 ,使网站得以健康发展。 如果看到这条信息的每位读者能慷慨打赏 20 元,我们一周就能脱离亏损, 并在接下来的一年里向所有读者继续免费提供优质内容。 但遗憾的是只有不到 1% 的读者愿意捐款, 他们的付出帮助了 99% 的读者免费获取知识, 我们在此表示感谢。

                     

友情链接: 超理论坛 | ©小时科技 保留一切权利