【C++例题/训练】:前缀和&&差分

发布于:2025-06-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

✨                                                       海压竹枝低复举,风吹山角晦还明      🌏 

📃个人主页island1314

🔥个人专栏:算法训练

🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


目录


🚀 前言:

前面我们已经通过  【算法/学习】前缀和&&差分-CSDN博客  学习了前缀和&&差分的效相关知识,现在我们开始进行相关题目的练习吧

1. 校门外的树

思路:给[0, n]的数组都标记为1,然后输出m行范围,[l, r]都标记为0。

AC代码如下:

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main()
{
    int n, m;
    cin>>n>>m;
    for(int i = 0; i <= n; i++) a[i] = 1;
    int l, r;
    for(int i = 1; i <= m; i++)
    {
        cin>>l>>r;
        for(int j = l; j <= r; j++) a[j] = 0;
    }
    int cnt = 0;
    for(int i = 0; i <= n; i++) {
        if(a[i] == 1) cnt++;
    }
    cout<<cnt<<endl;
    return 0;
}

2. 值周

思路:

该题与上题意思相同,但是数据范围更大,因此我们不能使用上面的方法,我们可以使用前缀和,使让数组a内的数据先初始化为0,然后对[ l, r ]进行操作,a[l]--, a[r + 1]++,然后操作M次后,再 a[i] += a[i - 1];这样可以使得每个区域内的数都小于0.

AC代码如下:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e7 + 10;

int a[N];

int main()
{
    int n, m;
    cin >> n >> m;
    int l, r;
    for (int i = 0; i < m; i++){
        cin >> l >> r;
        a[l]--, a[r + 1]++;  //前缀和,l-r标记为-1,r + 1又标记为1
    }
    for (int i = 1; i <= n; i++)
    {
        a[i] += a[i - 1];
    }
    int ans = 0;
    for (int i = 0; i <= n; i++)
    {
        if (a[i] >= 0) ans++;
    }
    cout << ans << endl;
    return 0;
}

3. 中位数图

思路:

🔥1.由于是求中位数是b的连续子序列,那么我们只要找到一串连续的数,里面包含数b,且大于b的数的数目与小于b的数的数目相等,才是我们要找的序列。
🔥2.由于数据范围给的比较大,我们可以简化一下,把比b大的数直接赋值为1,小的就赋值为-1.
同时,我们再用一个sum来求和,sum往一边统计的时候,当sum==0的时候说明大于b的数的数目与小于b的数的数目相等,也就是我们找到了一个序列。
🔥3.那么两边都有的情况怎么考虑呢?我们可以往一边记录,用一个num数组来记录sum的加减情况。
我们可以来看一下题目的样例:
{5,7,2,4,3,1,6}->{1,1,-1,4,-1,-1,1}
🔥往左遍历:
      1.sum+=-1-->sum==-1,可以这样,num[n+sum]+=1,也就是左边有一个比b小的情况加1.
       2.sum+=1->sum==0,num[n+sum]+=1,左边刚好找到一个成功的序列,ans++.
      3.sum+=1-->sum==1,num[n+sum]+=1,左边有一个比b大的情况加一。
🔥现在往右遍历:
      先初始化sum=0.
      然后sum+=-1->sum==-1,右边找到了一个比b小的数,而num[n+1]表示左边有一个比b小的情况的数目,也就是num[n-sum],我们可以用ans+=num[n-sum]。
后面同理,最后ans==4.(ans初始值为1,因为自己本身也是一个序列)

#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;

const int N = 1e6 + 10;
ll a[N], s[N];

int main()
{
	int n, b;
	cin >> n >> b;
	int pos, x;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		if (a[i] > b)a[i] = 1;
		else if (a[i] < b) a[i] = -1;
		else pos = i;//标记中位数的位置
	}

	ll sum = 0, ans = 1; //自己也算一个
	for (int i = pos - 1; i >= 1; i--) {
		sum += a[i];
		s[n + sum]++; //记录左边和为sum的数量
		if (!sum) ans++; //sum为零就说明小于b的数的数量于大于b的数量相同
	}
	sum = 0; //再往右遍历,同时与左边比较
	for (int i = pos + 1; i <= n; i++) {
		sum += a[i];
		ans += s[n - sum]; //加上左边与其互补的数量
		if (!sum) ans++;
	}
	cout << ans << endl;
}

4. 激光炸弹

思路:

先用二维前缀和的公式,求出g[x][y]的矩阵内的价值,然后就开始记录哪个正方形内的价值最大即可

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

typedef long long ll;
int n, R;
int g[5005][5005];

int main()
{
	cin >> n >> R;
	memset(g, 0, sizeof g);
	int xx = R, yy = R; //xx和yy表示边界,初始化为最小的R
	for (int i = 0; i < n; i++)
	{
		int x, y, v;
		cin >> x >> y >> v;
		g[x][y] = v;
		xx = max(xx, x); //记录输入的最大边
		yy = max(yy, y); 
	}

	for (int i = 1; i <= xx; i++){
		for (int j = 1; j <= yy; j++){
			g[i][j] = g[i - 1][j] + g[i][j - 1] - g[i - 1][j - 1] + g[i][j];
		}
	}

	int ans = 0;
	for (int i = R; i <= xx; i++){
		for (int j = R; j <= yy; j++) {
			ans = max(ans, g[i][j] - g[i - R][j] - g[i][j - R] + g[i - R][j - R]);
		}
	}
	
	cout << ans << endl;
	return 0;
}

5. 二分

思路:

题意是求裁判最多说对了几次。

对于每次猜数,如果猜的数是𝑎a,那么根据题目有三种情况:

  • 数大了:说对的范围是(−inf⁡,𝑎](−inf,a]

  • 数小了:说对的范围是[𝑎,+inf⁡)[a,+inf)

  • 数相等:说对的范围是[𝑎,𝑎+1)[a,a+1)

序列上操作,那么差分可以满足;然后把上面这些区间中,重叠次数最多的区间求出来,就是数所在的区间;这个区间重叠的次数就是我们要的答案。

由于数字较大,所以用了map搞离散化。

#include <iostream>
#include <map>
#include <limits.h>
using namespace std;

const int inf = INT_MAX;//int型数据最大值,因为是对每个数进行统计

const int N = 1e7 + 10;
int a[N];
char s[N];
map<int, int> mp;

int main() {
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i] >> s[i];
	for (int i = 1; i <= n; i++){
		if (s[i] == '.') mp[a[i]]++, mp[a[i] + 1]--;
		if (s[i] == '+') mp[a[i]]--, mp[-inf]++;
		if (s[i] == '-')mp[inf]--, mp[a[i] + 1]++;
	}

	int ans = -inf, h = 0;
	for (auto e : mp){
		h += e.second;
		ans = max(h, ans);
	}

	cout << ans << endl;
}

6. 货仓选址

思路:

如下图,选择n个仓库位置得中位数即可,

该问题的本质就是求 C = \sum_{1}^{n} |a_{i} - p| 的最小总和即可,当我们以后遇到该类问题时,只要使得p为所有ai的中位数即可

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main(){
	int n;
	cin >> n;
	vector<int> a;
	for (int i = 0; i < n; i++){
		int x;
		cin >> x;
		a.push_back(x);
	}
	sort(a.begin(), a.end());
	int p = a[a.size() / 2];
	int ans = 0;
	for (int i = 0; i < n; i++) ans += abs(a[i] - p);
	cout << ans << endl;
	return 0;
}

7. 最大子矩阵

思路:

求出其对应的前缀和矩阵,然后由于数据 是从 1- 109,直接枚举即可。

int getMaxMatrix(vector<vector<int>>& matrix) {
	int n = matrix.size();
	// 1.预处理前缀和
	vector<vector<int>> s(n + 1, vector<int>(n + 1));
	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] + matrix[i - 1][j - 1];
		}
	}
	vector<int> ans(4);
	int ret = -1e6 + 10;
	// 2.找最大子矩阵 (四重for循环)
	for (int x1 = 1; x1 <= n; x1++) {
		for (int y1 = 1; y1 <= n; y1++) {
			for (int x2 = x1; x2 <= n; x2++) {
				for (int y2 = y1; y2 <= n; y2++) {
					ret = max(ret, s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
				}
			}
		}
	}
	return ret;
}

int main()
{
	int n;
	cin >> n;
	vector<vector<int>> a(n, vector<int>(n));
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			cin >> a[i][j];
		}
	}
	cout << getMaxMatrix(a) << endl;

	return 0;
}

8. 主持人调度(二)

思路:

差分数组,题目等同于求当前位置最大被多少个区间包围。

//方法一:差分数组的映射
int minmumNumberOfHost(int n, vector<vector<int> >& startEnd) {
	map<int, int> mp;
	for (int i = 0; i < startEnd.size(); i++) {
		mp[startEnd[i][0]]++; //左边 ++
		mp[startEnd[i][1]]--; //右边 --
	}
	int ans = 0, res = 0;
	for (auto ip : mp) {
		res += ip.second;
		ans = max(ans, res);
	}
	return ans;
}

9. 寻找数组的中心下标

题目描述:给你一个整数数组 nums ,请计算数组的 中心下标 

数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。

如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。

如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 

思路:

假设 两个 数组 f,g

f [ i ] 表示:[0, i - 1] 区间内所有元素的和 (前缀和数组)

  • f [ i ] = f [ i - 1 ] + nums[ i - 1 ]

g[ i ] 表示:[i + 1, n - 1] 区间内所有元素的和(后缀和数组)

  • g[ i ] = g[ i + 1]  + nums[ i + 1 ]

则此时问题转化为求 [0, n] 内存在 i 使得 f [ i ] = g[ i ]

注:为避免越界访问,则 f [ 0 ] = g[ n - 1 ] = 0

填表顺序:f 表从左往右,g表从右往左 

10. 除自身以外数组的乘积

题目描述:给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

思路:

该题与上题思路相同。

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> ans(n);
        vector<int> f(n, 1), g(n, 1);
        
        for (int i = 1; i < n; i++)
            f[i] = f[i - 1] * nums[i - 1];

        for (int i = n - 2; i >= 0 ; i--)
            g[i] = g[i + 1] * nums[i + 1];

        for (int i = 0; i < n; i++)
        {
            ans[i] = f[i] * g[i];
        }
        return ans;
    }
};

11. 和为 K 的子数组

题目描述:给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。子数组是数组中元素的连续非空序列。(注:子数组内的数字可能是负数)

思路:

以 i 位置内为结尾的所有子数组,在[0,i - 1] 区间内,找有多少个前缀和等于 sum[ i ] - k

我们可以用一个哈希 <前缀和,该前缀和出现次数>来记录。

细节处理:

  • 在计算 i 位置之前,哈希表里面只保存 [0,i - 1] 位置的前缀和
  • 用一个变量 sum 标记前一个位置的前缀和
  • 假设枚举到 0 位置时此时的整个前缀和为 k,那么就会去[0,-1]区间内找前缀和为0的,因此先需要初始化:hash[ 0 ] = 1
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> hash; // 统计前缀和出现次数
        hash[0] = 1;

        int sum = 0, ret = 0;
        for (auto x : nums)
        {
            sum += x; // 计算当前位置前缀和
            if (hash.count(sum - k))  // 如果在当前位置,哈希表内存在某个和等于 sum - k
                ret += hash[sum - k]; //统计个数
            hash[sum]++; 
        }
        return ret;
    }
};

12. 和可被 K 整除的子数组

题目描述:给定一个整数数组 nums 和一个整数 k ,返回其中元素之和可被 k 整除的非空 子数组 的数目。子数组 是数组中 连续 的部分。

思路:

该题与上题思路类型。

我们先来补充几个知识。

同余定理:

  • (a - b)% p = 0  --- > a % p = b % p

【负数 % 正数】的结果以及修正:

  • 负数 % 正数 = 负数 --> (修正) a % p + p --> (正负统一) (a % p + p) % p

看完上面两个补充知识之后,只需在[0,i - 1] 区间内,找有多少个前缀和余数等于 sum % k,注:需要避免为负数要记得修正。

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        hash[0] = 1;

        int sum = 0, ret = 0;
        for (auto x : nums)
        {
            sum += x;
            int r = (sum % k + k) % k; //修正后余数
            if (hash.count(r))
                ret += hash[r];
            hash[r]++;
        }
        return ret;
    }
};

13. 连续数组

题目描述:给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。

思路:

我们可以数组中所有的 0 修改成 -1,这题就转化成找出最长的子数组,使得子数组的所有元素的和为0即可,那么该题就与 和为 K 的子数组类似,和为K的子数组 --> 和为0的子数组

但是我们这题需要求的是长度。

那么我们这里的哈希表应该是<前缀和,前缀和下标>

细节处理:

  • 当前位置使用完之后就丢进哈希表。
  • 如果出现了重复的 <sum,i>,只保留前面的那一对<sum,i>
  • 规定空的前缀的结束下标为 −1,由于空的前缀的元素和为 0,因此在遍历之前,首先在哈希表中存入键值对 (0,−1):hash[ 0 ] = -1
  • 长度计算:在遍历过程中,对于每个下标 i,如果 sum 的值在哈希表中已经存在,则取出 sum 在哈希表中对应的下标 j,nums 从下标 j + 1 到下标 i 的子数组中有相同数量的 0 和 1,因此该子数组的长度为 i − j,(不包括  j 那个格子)使用该子数组的长度更新最长连续子数组的长度;如果 counter 的值在哈希表中不存在,则将当前余数和当前下标 i 的键值对存入哈希表中。
class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        // 哈希<前缀和,前缀和下标>
        unordered_map<int, int> hash;
        hash[0] = -1; //默认有一个前缀和为 0 的情况

        int sum = 0, ret = 0;
        for (int i = 0; i < nums.size(); i++)
        {
            //计算当前位置的前缀和
            sum += nums[i] == 0 ? -1 : 1; //为 0 时则处理为 -1
            if (hash.count(sum)) //判断当前前缀和是否存在
                ret = max(ret, i - hash[sum]);
            else hash[sum] = i; // 不存在才存,因为要存该前缀和对应的最小的下标
        }
        return ret;
    }
};

14. 矩阵区域和

题目描述:给你一个 m x n 的矩阵 mat 和一个整数 k ,请你返回一个矩阵 answer ,其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和:

思路:

  用二维前缀和求法:sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[ i ][ j ];求得该矩阵的前缀和

  answer[ i ][ j ] 的求法:则相当于是以[i,j]为中心从[i - k,j - k] 到 [i + k,j + k] 的矩阵,注意越界的问题,因此 我们应该做一些特殊处理 如 x1 = max (0,i - k) ,x2 = min (m - 1,i + k)

  还要注意下标的映射问题:我们在填前缀和表时,需要多加一行一列,此时前缀和上的下标[i,,j] 在mat矩阵上的映射应该为[i - 1,j - 1],然后我们使用前缀和表去填answer时,answer表上的下标[i,j]在 前缀和 表上的映射应该为[i + 1,j + 1]

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> sum(m + 1, vector<int>(n + 1));
        for (int i = 1; i <= m; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        vector<vector<int>> answer(m, vector<int>(n));
        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                int x1 = max(0, i - k) + 1, y1 = max(0, j - k) + 1;
                int x2 = min(m - 1, i + k) + 1, y2 = min(n - 1, j + k) + 1;
                // 填表
                answer[i][j] = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
            }
        }
        return answer;
    }
};

15. 求x路径上最大和值

题目描述

有一张网格地图,我们使用(i, j)表示网格中从上往下数第i行和从左往右数第 j 列的单元格。地图的每一个格子中都有一些怪物,我们使用 a(i, j) 表示坐标(i,j)处的怪物数量。

现在有条龙,它可以对着地图的任意一个坐标吐出火焰来打败怪物。火焰会打败坐标 (x,y) 内的所有怪物,并向4个方向:(x - 1,y - 1),(x - 1,y + 1), (x + 1,y - 1),(x + 1,y + 1),并且一直延申即火焰会在矩形中会形成一个"X'形状。火焰会打败经过的每个坐标中的所有怪物。

例如,有一个图,数据如下:

9 6 3 2 2
8 5 10 2 8
2 2 8 5 3
9 6 10 5 2

我们对坐标(2,3)使出 火焰,能打败6+2+10+2+5+9+2= 36 只怪物。

现在释放一次火焰最多能打败多少只怪物?

输入描述:

每个测试文件均包含多组测试数据。第一行输入一个整数 T(1 ≤ T ≤ 10^4)代表数据组数,每组测试数据描述如下:

第一行输入两个整数 n,m(1 ≤ n,m ≤ 10^3)代表地图的大小。

此后 n 行,第 i 行输入 m 个整数 a(i, 1),a(i,2),…..,a(i,m) (1 ≤ a(i,j) ≤ 10^9),代表地图中每个坐标上的怪物数量。

除此之外,保证单个测试文件的 n * m之和不超过 10^6.

输出描述:

对于每一组测试数据,新起一行。输出一个整数,代表一次“龙之吐息"最多能打败的怪物数量。

样例输入

3
4 5
9 6 3 2 2
8 5 10 2 8
2 2 8 5 3
9 6 10 5 2
1 3
1 2 3
3 1
3
2
1

样例输出

41
3
3

思路:

  • 利用前缀和即可

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;

typedef long long ll;
const int N = 1005;
ll dp1[N][N], dp2[N][N], dp3[N][N], dp4[N][N];

void solve()
{
    int n, m;
    cin >> n >> m;
    // 前缀和,需要多出上下左右各一行一列
    vector<vector<ll>> a(n + 2, vector<ll>(m + 2, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) cin >> a[i][j];
    }

    // 初始化
    for (int i = 0; i <= n + 1; i++) {
        for (int j = 0; j <= m + 1; j++) {
            dp1[i][j] = dp2[i][j] = dp3[i][j] = dp4[i][j] = 0;
        }
    }

    // 左上角
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            dp1[i][j] = a[i][j] + dp1[i - 1][j - 1];
        }
    }
    // 右上角
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= 1; j--) {
            dp2[i][j] = a[i][j] + dp2[i - 1][j + 1];
        }
    }

    // 左下角
    for (int i = n; i >= 1; i--) {
        for (int j = 1; j <= m; j++) {
            dp3[i][j] = a[i][j] + dp3[i + 1][j - 1];
        }
    }
    // 右下角
    for (int i = n; i >= 1; i--) {
        for (int j = m; j >= 1; j--) {
            dp4[i][j] = a[i][j] + dp4[i + 1][j + 1];
        }
    }

    ll cnt = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            ll t = dp1[i][j] + dp2[i][j] + dp3[i][j] + dp4[i][j] - 3 * a[i][j];
            cnt = max(cnt, t);
        }
    }
    cout << cnt << "\n";
}



int main() 
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T = 1;
    cin >> T;
    while (T--) {
        solve();
    }
    return 0;
}



网站公告

今日签到

点亮在社区的每一天
去签到