一、前缀和
1.一维前缀和
简单理解,前缀和数组的特点:b[1]=a[1]、b[2]=a[1]+a[2]、b[n]=a[1]+…a[n]。
实现也很简单:
int a[n+1],b[n+1];
for(int i=1;i<n+1;i++)
{
scanf("%d",&a[i]);
b[i]=b[i-1]+a[i];
}
作用:在求数组某范围内所有数的和时不用去通过循环遍历,可直接利用前缀和数组,时间复杂度为O(1),但是在空间上会多开一个等大小的数组,用空间换时间。
例如在求a数组内范围为[l,r]的所有数之和:
sum=b[r]-b[l-1];
給a数组[l,r]范围的数之和加上C时可以:
但是解决让序列中某段区间 [l,r]的数加上一个常数 c的问题用差分
a[l]=a[l]+C;
a[r+1]=a[r+1]-C;
//这样求前缀和时[l,r]范围的b[i]就会都加上C;
最后就注意一点无论前缀还是差分在定义数组时要考虑到下标从1开始赋值给之后数组带来的影响,一定预防数组越界。
2.二维前缀和
1.构造二维前缀数组:二维原数组为a,二维前缀和数组为b;
如和实现b呢?
根据二维前缀和数组的特点,b[i][j]就等于a[0][0]与a[i][j]形成的矩阵的数据总和;
我们可以发现规律:
b[2,1]=a[2,1]+b[1,1]+b[2,0]-b[1,0];
b[3,2]=a[3,2]+b[2,2]+b[3,1]-b[2,1];
…
b[i,j]=a[i,j]+b[i-1,j]+b[i,j-1]-b[i-1,j-1];
int a[N+1][M+1],b[N+1][M+1];
for(int i=1;i<N+1;i++)
{
for(int j=1;j<M+1;j++)
{
scanf("%d",&a[i][j]);
b[i][j]=a[i][j]+b[i][j-1]+b[i-1][j]-b[i-1][j-1];
}
}
那么求二维数组的区间和就可以利用前缀和二维矩阵来快速的计算了
int x1,x2,y1,y2; //子矩阵范围 x1<x2,y1<y2 (左上角和右下角)
scanf("%d %d %d %d",&x1,&y1,&x2,&y2);
L=b[x2][y2]-b[x1-1][y2]-b[x2][y1-1]+b[x1-1][y1-1];
cout<<L<<endl;
二、差分
1.一维差分
拿数组a和数组b举例,所谓差分数组就是指以a为b的前缀和数组去构建b;
所以 b 数组每个元素就是 a 数组的每一个元素与其前一个元素的差.
构造差分数列:
int a[n+1];
int b[n+1];
void DifferenceMethod(int l,int r,int k,int b[])
{
b[l]+=k;
b[r+1]-=k;
}
for(int i=1;i<n+1;i++)
{
scanf("%d",&a[i]);
DifferenceMethod(i,i,a[i],b);
}
解决让序列中某段区间 [l,r]的数加上一个常数 c的问题适合用差分:
因为对于差分数组:
b[1]=a[1]
b[2]+b[1]=a[2]
b[3]+b[2]+b[1]=a[3]
b[n]+…b[1]=a[n]
那么先要实现在数组某段区间 [l,r]的每个数加上一个常数 C,就可以让b[l]+=C;就可以让从L开始的每一个a[L+n](n>=0)都加上C;在b[r+1]-=C就能将+C的范围控制在[l,r];
void DifferenceMethod(int l,int r,int k,int b[])
{
b[l]+=k;
b[r+1]-=k;
}
int l,r,c;
scanf("%d %d %d",&l,&r,&c);
DifferenceMethod(l,r,c,b);
for(int i=1;i<n+1;i++)
{
b[i]+=b[i-1]; //遍历b[i]得+c后的a[i]
//b[1]=b[1]+b[0] //b[0]=0;
//b[2]=b[2]+b[1]
//b[3]=b[3]+b[2] //(b[2]=b[2]+b[1])
}
2.二维差分
假定 a 为原二维数组,b为二维差分数组,此时 b为 a的差分数组,a 为 b的前缀和数组;
根据这一特点:a[i][j]=b[1][1] +… b[i][j] // 矩形范围和 // = a[i][j-1] + a[i-1][j] - a[i-1][j-1] + b[i][j]
那么b[i][j]=a[i][j]+a[i-1][j-1]-a[i-1][j]-a[i][j-1]
构建差分矩阵也可以:
b[i][j]+=a[i][j]; //a[i][j]的值为b[1][1]到b[i][j]的矩形范围和故在b[i][j]处+a[i][j]
b[i+1][j]-=a[i][j];
b[i][j+1]-=a[i][j];
b[i+1][j+1]+=a[i][j];
在纸上画个图就能看出这样操作b数组就能使插入值a[i][j]在超出b数组[1,1]到[i,j]矩阵范围外包含b[i][j]的任何矩阵和都不受插入值的影响;
构建差分矩阵模板:
int a[N+1][M+1];
int b[N+1][M+1];
for(int i=1;i<N+1;i++)
{
for(int j=1;j<M+1;j++)
{
scanf("%d",&a[i][j]);
b[i][j]+=a[i][j];
b[i+1][j]-=a[i][j];
b[i][j+1]-=a[i][j];
b[i+1][j+1]+=a[i][j];
}
}
//或者在构建好a数组后以这种方法来构建差分数组
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++)
b[i][j]=a[i][j]+a[i-1][j-1]-a[i-1][j]-a[i][j-1];
解决让序列中 [x1][y1]到[x2][y2]矩阵范围的数加上一个常数 c的问题适合用差分
二维差分插入模板:
int x1,y1,x2,y2,c;
scanf("%d %d %d %d %d",&x1,&y1,&x2,&y2,&c);
b[x1][y1]+=c;
b[x2+1][y1]-=c;
b[x1][y2+1]-=c;
b[x2+1][y2+1]+=c;
///打包成函数既能进行构建差分二维数组也可以用来进行二维差分插入
void insert(int x1, int y1, int x2, int y2, int c){
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++)
insert(i, j, i, j, a[i][j]);//初始化矩阵a的差分矩阵
int x1, y1, x2, y2, c;
scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
insert(x1, y1, x2, y2, c);//在各个子矩阵的区间上加上相应值
最后差分矩阵求和得完成原数组特定范围矩阵中的每个元素的值加上 c后的矩阵
假定 a 为原二维数组,b为二维差分数组,此时 b为 a的差分数组,a 为 b的前缀和数组;
for(int i=1;i<N+1;i++)
{
for(int j=1;j<M+1;j++)
{
b[i][j]+=b[i][j-1]+b[i-1][j]-b[i-1][j-1]; //想象矩阵b大部分已经变成矩阵a了
}
}
三、数据离散化
离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。其基本思想就是在众多可能的情况中,只考虑需要用的值。
简单理解就是说问题涉及的数据在原空间里太过于分散,我们将它们放到一个小的空间里面进行处理。(不用桶子去装一瓶矿泉水)
离散化后的数据不仅能节约空间,在一些排序、查找、求前缀和等问题中还能提高算法的效率,节约时间;
核心思路是:排序,再删除重复元素,最后就是索引元素离散化后对应的值。
借用一道例题来深度了解:
分析一下就可以发现:在这个无限长的数轴中进行n次插入值,n最大为10^5.
进行m次询问,每次询问有两个边界[L,R],L<=R,m最大为10^5次,那么这个问题最多要利用到的坐标点也就 300000个 //n+m*2(实际操作为预防数组越界可以加一点空间);
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N];
vector<int> alls; //所有坐标点
vector<PII> add, query; ///add 所有插入了值的点 //query所有要查询的点
int find(int x)
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; //注意加了1
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int x, c;
cin >> x >> c;
add.push_back({x, c}); //记录所有要插入值的点
alls.push_back(x); //记录所有坐标
}
for (int i = 0; i < m; i ++ )
{
int l, r;
cin >> l >> r;
query.push_back({l, r}); //记录要查询的范围
alls.push_back(l); //记录所有坐标
alls.push_back(r); //记录所有坐标
}
// 去重
sort(alls.begin(), alls.end()); //坐标排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); //erase 删除指针指向的数据项
//vector<int>::iterator unique(vector<int> &a) {
// int j = 0;
// for (int i = 0; i < a.size(); ++i) {
// if (!i || a[i] != a[i - 1])//如果是第一个元素或者该元素不等于前一个元素,即不重复元素,我们就把它存到数组前j个元素中
// a[j++] = a[i];//每存在一个不同元素,j++
// }
// return a.begin() + j;//返回的是前j个不重复元素的下标
//}
// 处理插入
for (int i=0;i<add.size();i++)
{
int x = find(add[i].first); //找到坐标在排序去重后的alls中的下标位置+1
a[x] += add[i].second; //从小标1开始插入值方便后续求前缀和
}
//a[x]即为原始数据离散化后的数组;
// 预处理前缀和
for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];
// 处理询问
for (int i=0;i<query.size();i++)
{
int l = find(query[i].first), r = find(query[i].second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
#最后再挂个区间合并:
例如:(1,3) , (4 , 5) ,(1 ,2) 3个区间合并后变为2个(1,3) , (4 , 5);
步骤:区间接收->区间排序->区间合并
#include <stdio.h>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int,int> pii;
vector<pii> nums,res;
int st=-2e9; //相当于无穷小
int ed=-2e9;
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
int l,r;
scanf("%d %d",&l,&r); //输入的左右边界范围均为-10^9<l<r<10^9
nums.push_back({l,r});
}
sort(nums.begin(),nums.end()); //根据l来排序
for(int i=0;i<nums.size();i++)
{
if(nums[i].first>ed)
{
if(ed!=-2e9) res.push_back({st,ed});
st=nums[i].first;
ed=nums[i].second;
}
else if(nums[i].second>ed)
{
ed=nums[i].second; //如果下一个区间l在上一个区间内的话就要重新确定一下右边界也就是合并区间后再将其push到res;
}
}
for(int i=0;i<res.size();i++)
{
cout<<res[i].first<<res[i].second<<" ";
}
res.push_back({st,ed}); //把最后一个区间补进去
printf("%d",res.size()); //res.size为最后区间的个数
return 0;
}