算法刷题day21:前缀和

目录

引言

昨天写二分题写的我非常的郁闷,差点否认了自己,幸好昨天死磕找回了信心,今天做前缀和使信心大增,感觉还是很好的,所以只要坚持下来还是会有很好的结果的。加油!本篇博客讲的是前缀和的知识,只要基本概念清楚题目都比较简单,也没什么弯弯绕绕的,想说还是拼的是记忆力和毅力,加油!


概念

前缀和:使用 O ( N ) O(N) O(N) 的时间复杂度去预处理,用 O ( 1 ) O(1) O(1) 的时间复杂度求出 [ l , r ] [l,r] [l,r] 的区间和,具体公式为 s [ r ] − s [ l − 1 ] s[r] - s[l-1] s[r]−s[l−1], 需要详解的可以参考我之前写过的博客前缀和与差分


一、壁画

标签:前缀和

思路:这道题首先我一看这个壁画首先要连续,所以说我想无非就是下图的这三种情况,然后我看这三种情况都满足题目说的条件:"必须与一段没有摧毁的墙壁相连",我想求的是最优情况,那么只要是从两边开始连续摧毁肯定是满足的,所以就是求最优的一段区间的和,那么就是前缀和了。然后第一时间想到的肯定是枚举所有的区间,然后根据要求可得,区间的长度为 l e n g t h = ( n + 2 − 1 ) / 2 length = (n + 2 - 1) / 2 length=(n+2−1)/2,也就是上取整,关于上取整可以参考我之前的博客算法竞赛常用的库函数中的 c m a t h cmath cmath 模块,然后就可以用 O ( N ) O(N) O(N) 的时间复杂度来遍历所有的可能,最后取最大值即可。

题目描述:

cpp 复制代码
Thanh 想在一面被均分为 N 段的墙上画一幅精美的壁画。

每段墙面都有一个美观评分,这表示它的美观程度(如果它的上面有画的话)。

不幸的是,由于洪水泛滥,墙体开始崩溃,所以他需要加快他的作画进度!

每天 Thanh 可以绘制一段墙体。

在第一天,他可以自由的选择任意一段墙面进行绘制。

在接下来的每一天,他只能选择与绘制完成的墙面相邻的墙段进行作画,因为他不想分开壁画。

在每天结束时,一段未被涂颜料的墙将被摧毁(Thanh 使用的是防水涂料,因此涂漆的部分不能被破坏),且被毁掉的
墙段一定只与一段还未被毁掉的墙面相邻。

Thanh 的壁画的总体美观程度将等于他作画的所有墙段的美观评分的总和。

Thanh想要保证,无论墙壁是如何被摧毁的,他都可以达到至少 B 的美观总分。

请问他能够保证达到的美观总分 B 的最大值是多少。

输入格式
第一行包含整数 T,表示共有 T 组测试数据。
每组数据的第一行包含整数 N。
第二行包含一个长度为 N 的字符串,字符串由数字 0∼9 构成,第 i 个字符表示第 i 段墙面被上色后能达到的美观评分。

输出格式
每组数据输出一个结果,每个结果占一行。
结果表示为 Case #x: y,其中 x 为组别编号(从 1 开始),y 为 Thanh 可以保证达到的美观评分的最大值。

数据范围
1≤T≤100,存在一个测试点N=5∗106,其他测试点均满足2≤N≤100
输入样例:
4
4
1332
4
9583
3
616
10
1029384756
输出样例:
Case #1: 6
Case #2: 14
Case #3: 7
Case #4: 31
样例解释
在第一个样例中,无论墙壁如何被破坏,Thanh都可以获得 6 分的美观总分。在第一天,他可以随便选一个美观评分3的
墙段进行绘画。在一天结束时,第一部分或第四部分将被摧毁,但无论哪一部分都无关紧要。在第二天,他都可以在另一
段美观评分 3 的墙段上作画。

在第二个样例中,Thanh 在第一天选择最左边的美观评分为 9 的墙段上作画。在第一天结束时唯一可以被毁掉的墙体是最
右边的那段墙体,因为最左边的墙壁被涂上了颜料。在第二天,他可以选择在左数第二段评分为 5 的墙面上作画。然后
右数第二段墙体被摧毁。请注意,在第二天,Thanh不能选择绘制第三段墙面,因为它不与任何其他作画墙面相邻。这样
可以获得 14 分的美观总分。

示例代码:

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 5e6+10;

string str;
int T, n;
int s[N];
int cnt = 1;

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> T;
    while(T--)
    {
        cin >> n;
        cin >> str;
        
        for(int i = 1; i <= n; ++i) s[i] = s[i-1] + str[i-1] - '0';
        
        int c = (n + 1) / 2;
        int res = 0;
        for(int i = 1; i + c - 1 <= n; ++i)
        {
            int j = i + c - 1;
            res = max(res, s[j] - s[i-1]);
        }
        printf("Case #%d: %d\n", cnt++, res);
    }
    
    return 0;
}

二、前缀和

标签:前缀和、模板题

思路:就是一个模板题,练练手。

题目描述:

cpp 复制代码
输入一个长度为 n 的整数序列。

接下来再输入 m 个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。

输出格式
共 m 行,每行输出一个询问的结果。

数据范围
1≤l≤r≤n,1≤n,m≤100000,−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10

示例代码:

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 1e5+10;

int n, m;
int s[N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> m;
    for(int i = 1; i <= n; ++i)
    {
        cin >> s[i];
        s[i] += s[i-1];
    }
    
    while(m--)
    {
        int l, r;
        cin >> l >> r;
        cout << s[r] - s[l-1] << endl;
    }
    
    return 0;
}

三、子矩阵的和

标签:前缀和、模板题

思路:模板题没什么说的,用来熟悉熟悉练练手。

题目描述:

输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角
坐标。

对于每个询问输出子矩阵中所有数的和。

输入格式
第一行包含三个整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式
共 q 行,每行输出一个询问的结果。

数据范围
1≤n,m≤1000,1≤q≤200000,1≤x1≤x2≤n,1≤y1≤y2≤m,−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21

示例代码:

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 1010;

int n, m, q;
int s[N][N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> m >> q;
    for(int i = 1; i <= n; ++i)
    {
        for(int j = 1; j <= m; ++j)
        {
            cin >> s[i][j];
            s[i][j] += s[i][j-1] + s[i-1][j] - s[i-1][j-1]; 
        }
    }
    
    while(q--)
    {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] << endl;
    }
    
    return 0;
}

四、K倍区间

标签:前缀和

思路1:首先这道题第一反应就是直接暴力枚举每个子区间,然后计算符合条件的,时间复杂度为 O ( N 2 ) O(N^2) O(N2), N = 1 0 5 N = 10^5 N=105, 所以直接做肯定会超时,只能过一半的数据。

思路2:我们可以从公式入手 s [ j ] − s [ i − 1 ] = k s[j] - s[i-1] = k s[j]−s[i−1]=k,即 s [ j ] ≡ s [ i − 1 ] ( m o d k ) s[j] \equiv s[i-1] \pmod k s[j]≡s[i−1](modk),这里的 j j j 为右边界, i i i 为左边界,并且 i ∈ [ 1 , j ] i \in [1,j] i∈[1,j],所以 i − 1 ∈ [ 0 , j − 1 ] i-1 \in [0,j-1] i−1∈[0,j−1],所以我们只用算在 j j j 之前的跟 s [ j ] m o d    k s[j] \mod k s[j]modk的余数相同的 s [ i − 1 ] s[i-1] s[i−1] 的数量即可。那么我们可以从前往后求出 s [ i − 1 ] s[i-1] s[i−1] 的余数,用一个数组当作哈希表,下标即为余数,元素为个数,这样就可以了,注意因为 i − 1 ∈ [ 0 , j − 1 ] i-1 \in [0,j-1] i−1∈[0,j−1],所以预处理时 s [ 0 ] = 1 , c n t [ 0 ] = 1 s[0] = 1, cnt[0] = 1 s[0]=1,cnt[0]=1,因为定义是从 0 0 0 开始的,而 s [ 0 ] = 0 s[0] = 0 s[0]=0,所以当 j = 1 j = 1 j=1时, c n t [ 0 ] = 1 cnt[0] = 1 cnt[0]=1。

题目描述:

cpp 复制代码
给定一个长度为 N 的数列,A1,A2,...AN,如果其中一段连续的子序列 Ai,Ai+1,...Aj 之和是 K 的倍数,我们就称这个区间 
[i,j] 是 K 倍区间。

你能求出数列中总共有多少个 K 倍区间吗?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含一个整数 Ai。

输出格式
输出一个整数,代表 K 倍区间的数目。

数据范围
1≤N,K≤100000,1≤Ai≤100000
输入样例:
5 2
1
2
3
4
5
输出样例:
6

示例代码1: 暴力 6/12

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 1e5+10;

int n, k;
LL s[N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> k;
    for(int i = 1; i <= n; ++i)
    {
        cin >> s[i];
        s[i] += s[i-1];
    }
    
    LL res = 0;
    for(int i = 1; i <= n; ++i)
    {
        for(int j = i; j <= n; ++j)
        {
            if((s[j] - s[i-1]) % k == 0) res++;
        }
    }
    
    cout << res << endl;
    
    return 0;
}

示例代码2: 12/12

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 1e5+10;

int n, k;
LL s[N];
int cnt[N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> k;
    for(int i = 1; i <= n; ++i)
    {
        cin >> s[i];
        s[i] += s[i-1];
    }
    
    LL res = 0;
    cnt[0]++;
    for(int i = 1; i <= n; ++i)
    {
        res += cnt[s[i] % k];
        cnt[s[i] % k]++;
    }
    
    cout << res << endl;
    
    return 0;
}

五、统计子矩阵

标签:前缀和

思路1:暴力枚举每个子矩阵,再判断条件,这里做了个优化,因为 A i j A_{ij} Aij 非负,所以一旦超出了 K K K 那就直接 b r e a k break break 即可。

思路2: 因为这道题是单调的,所以可以用双指针来做,可以将时间复杂度从 O ( N 4 ) O(N^4) O(N4) 降到 O ( N 3 ) O(N^3) O(N3), 因为 N , M ≤ 100 N,M \leq 100 N,M≤100,加上常数小并且真实的时间复杂度肯定是低于 O ( N 3 ) O(N^3) O(N3) 的,所以可以勉强通过。可以前缀和行,列用双指针枚举,即可,具体细节见代码。

题目描述:

cpp 复制代码
给定一个 N×M 的矩阵 A,请你统计有多少个子矩阵 (最小 1×1,最大 N×M) 满足子矩阵中所有数的和不超过给定的整数 
K?

输入格式
第一行包含三个整数 N,M 和 K。
之后 N 行每行包含 M 个整数,代表矩阵 A。

输出格式
一个整数代表答案。

数据范围对于 30% 的数据,N,M≤20,
对于 70% 的数据,N,M≤100,
对于 100% 的数据,1≤N,M≤500;0≤Aij≤1000;1≤K≤2.5×108。

输入样例:
3 4 10
1 2 3 4
5 6 7 8
9 10 11 12
输出样例:
19
样例解释
满足条件的子矩阵一共有 19,包含:
大小为 1×1 的有 10 个。大小为 1×2 的有 3 个。大小为 1×3 的有 2 个。大小为 1×4 的有 1 个。大小为 2×1 的有 
3 个。

示例代码1: 暴力 7/10

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 510;

int n, m, k;
LL s[N][N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> m >> k;
    for(int i = 1; i <= n; ++i)
    {
        for(int j = 1; j <= m; ++j)
        {
            cin >> s[i][j];
            s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
        }
    }
    
    LL res = 0;
    for(int x1 = 1; x1 <= n; ++x1)
    {
        for(int x2 = x1; x2 <= n; ++x2)
        {
            for(int y1 = 1; y1 <= m; ++y1)
            {
                for(int y2 = y1; y2 <= m; ++y2)
                {
                    LL t = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
                    if(t <= k) res++; 
                    else break;  // 因为Aij非负
                }
            }
        }
    }
    
    cout << res << endl;
    
    return 0;
}

示例代码2: 双指针 10/10

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 510;

int n, m, k;
LL s[N][N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> m >> k;
    for(int i = 1; i <= n; ++i)
    {
        for(int j = 1; j <= m; ++j)
        {
            cin >> s[i][j];
            s[i][j] += s[i-1][j];  // 至于处理行
        }
    }
    
    LL res = 0;
    for(int i = 1; i <= n; ++i)
    {
        for(int j = i; j <= n; ++j)
        {
            for(int l = 1, r = 1, sum = 0; r <= m; ++r)
            {
                sum += s[j][r] - s[i-1][r];
                while(sum > k)
                {
                    sum -= s[j][l] - s[i-1][l];
                    l++;
                }
                res += r - l + 1;
            }
        }
    }
    
    cout << res << endl;
    
    return 0;
}

六、递增三元组

标签:二分、前缀和

思路1: 二分要求 A i < B j < C k A_i < B_j < C_k Ai<Bj<Ck,可以分开来看,分别求出 A i < B j A_i < B_j Ai<Bj 和 B j < C k B_j < C_k Bj<Ck的个数,然后两个组合也就是相乘,最后把每个 B j B_j Bj 的结果相加即可。怎么求 A i < B j A_i < B_j Ai<Bj 中 A i A_i Ai 的个数呢,可以把 A i A_i Ai 排好序,然后用二分来求最后一个小于 B j B_j Bj 的下标,即可知道数量,同理 B j < C k B_j < C_k Bj<Ck 的数量也可通过二分来求出来,详情见代码。

思路2:前缀和:整体思路就是定义一个 c n t [ i ] cnt[i] cnt[i] ,意为在 A [ i ] A[i] A[i] 中元素为 i i i 的个数,然后用前缀和 s [ i ] s[i] s[i] 一统计,意为在 A A A 中的元素小于等于 i i i 的元素个数,然后再定义一个数组 a s [ i ] as[i] as[i] ,遍历每一个 b j b_j bj ,求出在 A A A 中小于 b j b_j bj 的个数,同理可求出 c s [ i ] cs[i] cs[i] ,意为对于每个 b j b_j bj ,在 C C C 中大于 b j b_j bj 的元素个数,最后遍历每个 b j b_j bj , r e s = r e s + a s [ j ] ∗ c s [ j ] res = res + as[j] * cs[j] res=res+as[j]∗cs[j]。

题目描述:

cpp 复制代码
给定三个整数数组

A=[A1,A2,...AN],
B=[B1,B2,...BN],
C=[C1,C2,...CN],

请你统计有多少个三元组 (i,j,k) 满足:
1≤i,j,k≤N
Ai<Bj<Ck

输入格式
第一行包含一个整数 N。
第二行包含 N 个整数 A1,A2,...AN。
第三行包含 N 个整数 B1,B2,...BN。
第四行包含 N 个整数 C1,C2,...CN。

输出格式
一个整数表示答案。

数据范围
1≤N≤105,0≤Ai,Bi,Ci≤105
输入样例:
3
1 1 1
2 2 2
3 3 3
输出样例:
27

示例代码1: 二分

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 1e5+10;

int n;
int a[N], b[N], c[N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n;
    for(int i = 1; i <= n; ++i) cin >> a[i];
    for(int i = 1; i <= n; ++i) cin >> b[i];
    for(int i = 1; i <= n; ++i) cin >> c[i];
    
    sort(a+1, a+1+n);
    sort(c+1, c+1+n);
    
    LL res = 0;
    for(int i = 1; i <= n; ++i)
    {
        int key = b[i];
        
        int r1 = 0, r2 = 0;
        int l = 1, r = n;
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            if(a[mid] < key) l = mid;
            else r = mid - 1;
        }
        
        if(a[r] < key) r1 = r;
        
        l = 1, r = n;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(c[mid] > key) r = mid;
            else l = mid + 1;
        }
        
        if(c[r] > key) r2 = n - r + 1;
        
        res += (LL)r1 * r2;
    }
    
    cout << res << endl;
    
    return 0;
}

示例代码2: 前缀和

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 1e5+10;

int n;
int a[N], b[N], c[N];
int as[N], cs[N];
int s[N], cnt[N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n;
    for(int i = 1; i <= n; ++i) cin >> a[i], a[i]++;
    for(int i = 1; i <= n; ++i) cin >> b[i], b[i]++;
    for(int i = 1; i <= n; ++i) cin >> c[i], c[i]++;
    
    for(int i = 1; i <= n; ++i) cnt[a[i]]++;
    for(int i = 1; i < N; ++i) s[i] = s[i-1] + cnt[i];
    for(int i = 1; i <= n; ++i) as[i] = s[b[i] - 1];
    
    memset(s, 0, sizeof s);
    memset(cnt, 0, sizeof cnt);
    for(int i = 1; i <= n; ++i) cnt[c[i]]++;
    for(int i = 1; i < N; ++i) s[i] = s[i-1] + cnt[i];
    for(int i = 1; i <= n; ++i) cs[i] = s[N - 1] - s[b[i]];
    
    LL res = 0;
    for(int i = 1; i <= n; ++i) res += (LL)as[i] * cs[i];
    
    cout << res << endl;
    
    return 0;
}

七、激光炸弹

标签:前缀和

思路:这道题其实数据范围允许,直接拿前缀和枚举每一个子矩阵即可,详情见代码。注:在写前缀和的时候尽量把前缀和矩阵 s s s 当成初始矩阵,不要再开一个初始矩阵 a a a 了,因为这道题这样出现了 M e m o r y L i m i t E x c e e d e d Memory\ Limit\ Exceeded Memory Limit Exceeded ,所以还是要注意点。

题目描述:

地图上有 N 个目标,用整数 Xi,Yi 表示目标在地图上的位置,每个目标都有一个价值 Wi。

注意:不同目标可能在同一位置。

现在有一种新型的激光炸弹,可以摧毁一个包含 R×R 个位置的正方形内的所有目标。

激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y 轴平行。

求一颗炸弹最多能炸掉地图上总价值为多少的目标。

输入格式
第一行输入正整数 N 和 R,分别代表地图上的目标数目和正方形包含的横纵位置数量,数据用空格隔开。

接下来 N 行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,Wi,分别代表目标的 x 坐标,y 坐标和价值,数据用空格隔开。

输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。

数据范围
0≤R≤109,0<N≤10000,0≤Xi,Yi≤5000,0≤Wi≤1000
输入样例:
2 1
0 0 1
1 1 1
输出样例:
1

示例代码:

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 5010;

int n, r;
int s[N][N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> r;
    r = min(r, 5001);
    while(n--)
    {
        int x, y, z;
        cin >> x >> y >> z;
        x++, y++;
        s[x][y] += z;
    }
    
    for(int i = 1; i < N; ++i)  // 预处理前缀和
    {
        for(int j = 1; j < N; ++j)
        {
            s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
        }
    }
    
    LL res = 0;
    for(int x1 = 1; x1 + r - 1 <= 5001; ++x1)
    {
        for(int y1 = 1; y1 + r - 1 <= 5001; ++y1)
        {
            int x2 = x1 + r - 1, y2 = y1 + r - 1;
            res = max(res, (LL)s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
        }
    }
    
    cout << res << endl;
    
    return 0;
}
相关推荐
量子-Alex1 小时前
【多模态聚类】用于无标记视频自监督学习的多模态聚类网络
学习·音视频·聚类
吉大一菜鸡1 小时前
FPGA学习(基于小梅哥Xilinx FPGA)学习笔记
笔记·学习·fpga开发
xiaoshiguang34 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡4 小时前
【C语言】判断回文
c语言·学习·算法
别NULL4 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇4 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
小A1594 小时前
STM32完全学习——SPI接口的FLASH(DMA模式)
stm32·嵌入式硬件·学习
岁岁岁平安4 小时前
spring学习(spring-DI(字符串或对象引用注入、集合注入)(XML配置))
java·学习·spring·依赖注入·集合注入·基本数据类型注入·引用数据类型注入
武昌库里写JAVA4 小时前
Java成长之路(一)--SpringBoot基础学习--SpringBoot代码测试
java·开发语言·spring boot·学习·课程设计
qq_589568104 小时前
数据可视化echarts学习笔记
学习·信息可视化·echarts