前缀和的作用是降低时间复杂度来计算一个区间的数值总和,比如计算S[l]到S[r]区间所有数值的总和,一般的方法就是一个个遍历相加,从l加到r
对于单次求和不影响,但是如果多次求和时间复杂度就会差别很大
遍历的时间复杂度是O(n);
前缀和时间复杂度是O(1);
说前缀和是一种算法不如说是一种公式:
S[i]=a[1]+a[2]+a[3]+…+a[i]
区间l到r的和就是a[l]+a[l+1]+…+a[r]=S[r]-S[l-1]
为了方便不用进行下标的判断,默认前缀和数据存储从下标1开始,S[0]=0;
这样a[1]+a[2]+…+a[r]=S[r]-S[0]
上代码 (代码简短又易懂)
#include
#include
#define N 1000005
using namespace std;
long long a[N], S[N];//默认S[0]=0
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
S[i] = S[i - 1] + a[i];
}
int l, r;
for (int i = 0; i < m; i++)
{
cin >> l >> r;
cout << S[r] - S[l - 1] << endl;
}
return 0;
}
类似一维,主要推导两个式子,
画个图就看出来了
#include
#include
#define N 1005
using namespace std;
long long a[N][N], S[N][N];
int main()
{
int n, m, q;
cin >> n >> m >> q;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
cin >> a[i][j];
S[i][j] = S[i][j - 1] + S[i - 1][j] - S[i - 1][j - 1] + a[i][j];
}
int x1, y1, x2, y2;
for (int i = 0; i < q; i++)
{
cin >> x1 >> y1 >> x2 >> y2;//(x1,x2)是包含在矩阵里面的
cout << S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1] << endl;
}
return 0;
}
什么是差分?
两个数组,b[],a[],a[]是b[]的前缀和数组,b[]是a[]的差分数组
差分就是将数列中的每一项分别与前一项数做差。
首先一个数组 :1 2 5 4 7 3
那么差分之后 :1 1 3 -1 3 -4 -3
其实就是 b [ i ] = a [ i ] - a [ i − 1 ]
a[i]=b[1]+b[2]+b[3]+…+b[i]
注意得到的差分序列第一个数和原来的第一个数一样(相当于第一个数减0)
差分序列最后比原序列多一个数(相当于0减最后一个数)
(截取自:https://blog.csdn.net/weixin_44668898/article/details/104281102)
差分一般使用场景:
给出 n 个数,再给出 m 个询问,每个询问给出 l,r,c,要求你在 l 到 r 上每一个值都加上 c,而只给你 O(n) 的时间范围,怎么办?
如果暴力,时间复杂度就是 O(n*m)
如果线段树或者树状数组,时间复杂度就是 O(mlogn)
所以这里用差分,时间复杂度就是 O(n)
题目链接-差分
题目链接-差分矩阵
记两个关键的公式:
#include
#include
using namespace std;
int a[100005], b[100005];
int main()
{
int n, m, l, r, c;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
b[i] = a[i] - a[i - 1];//构造差分数组
}
while (m--)
{
cin >> l >> r >> c;
b[l] += c; b[r + 1] -= c;
//a[r+1]=b[1]+b[2]+..b[l]+c+...+b[r]+b[r+1]+c;
//a[r]=b[1]+b[2]+..b[l]+c+...+b[r];
}
for (int i = 1; i <= n; i++)
{
a[i] = b[i] + a[i - 1];//前缀和运算
cout << a[i] << " ";
}
}
给个链接,感觉讲的挺清楚的了:
差分矩阵
主要记住四个点加一个公式:
四点:
这里可以总结成子矩阵内部有多少b[i][j]加上或减上c,对应的前缀和就加上其总和。
#include
#include
using namespace std;
int a[1005][1005], b[1005][1005];
void Insert(int x1, int y1, int x2, int y2, int c);
int main()
{
int n, m, q;
cin >> n >> m >> q;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> a[i][j];
//写法1:
/*for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
Insert(i, j, i, j, a[i][j]);
和下面的个写法本质一样*/
//写法2:
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1] ;
int x1, y1, x2, y2, c;
while (q--)
{
cin >> x1 >> y1 >> x2 >> y2 >> c;
Insert(x1, y1, x2, y2, c);
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
a[i][j] = a[i][j - 1] + a[i - 1][j] - a[i - 1][j - 1] + b[i][j];
//法二:b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
cout << a[i][j] << " ";
//法二:cout<
cout << endl;
}
}
void Insert(int x1, int y1, int x2, int y2, int c)
{
b[x1][y1] += c, b[x2 + 1][y2 + 1] += c;
b[x2 + 1][y1] -= c; b[x1][y2 + 1] -= c;
}
离散化通过映射将稀疏区间变得密集,再通过前缀和求区间和
例题来源
(注意一下二分查找的写法,果然太久没用二分查找有的点竟然忘了)
关于pair的用法参考STL
重点在于离散化的处理
#include
#include
#include
#include
using namespace std;
typedef long long ll;
ll a[300005], S[300005];
vector<ll>pos;
vector<pair<ll, ll>>P1, P2;//用来存储映射关系(坐标与c+区间左右边界)
int find(ll x)
{
int l = 0, r = pos.size() - 1,mid;
while (l <= r)//*****是小于等于
{
mid = l + r >> 1;
if (pos[mid] == x) return mid + 1;//因为要构造前缀和,所以数组下标从1开始
else if (x < pos[mid]) r = mid - 1;
else l = mid + 1;
}
//return -1;肯定是找得到边界的所以这一步不要也行
}
int main()
{
//本题的思路:
//通过对数据的分析x的范围远超n,m的范围,有意义的坐标一共n+2m,范围大概1-3*100000
//可以看出数据非常离散,这里再直接用前缀和就没法实现了,所以先对数组下标进行映射,去掉那些无意义的数组下标
//映射采用pair,first表示原数组下标,second表示添加的c,
//离散化结束后缩短了范围,始数据变得密集,接着通过前缀和求区间和,这是一道前缀和+离散化的题目
ll n, m, x, c, l, r;
cin >> n >> m;
for (ll i = 0; i < n; i++)
{
cin >> x >> c;
pos.push_back(x);
P1.push_back({ x,c });
}
//为了方便找到区间边界,所以再把区间边界映射存入P2
for (ll i = 0; i < m; i++)
{
cin >> l >> r;
pos.push_back(l);
pos.push_back(r);
P2.push_back({ l,r });//留着后面要用来查询
}
sort(pos.begin(),pos.end());
pos.erase(unique(pos.begin(), pos.end()), pos.end());
//unique函数去重,然后返回最后一个元素,这时候的就可以通过在pos确定x小标的元素在300000大小的数组里面的下标了
for (auto x : P1)
{
int p = find(x.first);
if (p != -1) a[p] += x.second;
}
for (int i = 1; i <= pos.size(); i++)
{
S[i] = S[i - 1] + a[i];
}
//查询
for (auto x : P2)
{
l = find(x.first); r = find(x.second);
cout << S[r] - S[l - 1] << endl;
}
return 0;
}
说一下思路:先将每个区间的左右端点打包成pair类型存入vector,根据区间的左端点排序,接着进行合并,在上一个区间的beg和end不变的情况下比较上一个区间与当前区间的位置关系,一共有三种情况,新的子区间在上一个区间的内部,在上一个区间的外部,与上一个区间有交集(不可能在上一个区间的前面,因为先对所有区间的左端点进行排序过了)。当新区间的first大于未更新的end,说明两个区间没用交际,更新beg和end,将上一个区间存入re(新的pair类型的vector),如果end>=first,有两种情况,一种是有交集,一种是包含,所以只要对end进行更新,end=max(end,seg.end),前一中是包含,后一种是交集。
对于segs中所有pair处理结束后,最后一个区间还没入re数组,因为入数组的操作是当判断出现新的区间时才进行的。
所以当所有pair处理完,在对beg和end进行一次判断,如果end!=-2e9,就入数组,如果是-2e9肯一开始就是空vector。
例题链接–区间合并
#include
#include
#include
#include
using namespace std;
typedef pair<int, int> PII;
void Merge(vector<PII>&segs);
int main()
{
int n, l, r;
vector<PII>segs;
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> l >> r;
segs.push_back({ l,r });
}
Merge(segs);
cout << segs.size();
return 0;
}
void Merge(vector<PII>&segs)
{
sort(segs.begin(),segs.end());
vector<PII>re;
int beg = -2e9, end = -2e9;
for (auto seg : segs)
{
if (seg.first > end)//不包含,不想接
{
if (end != -2e9) re.push_back({ beg,end });
beg = seg.first;
end = seg.second;
}
else end = max(end, seg.second);
}
if (end != -2e9) re.push_back({ beg,end });
segs = re;
}
关键代码:
void Merge(vector<PII>&segs)
{
sort(segs.begin(),segs.end());//pair的排序是先比较first,后比较second
vector<PII>re;
int beg = -2e9, end = -2e9;
for (auto seg : segs)
{
if (seg.first > end)//不包含,不想接
{
if (end != -2e9) re.push_back({ beg,end });
beg = seg.first;
end = seg.second;
}
else end = max(end, seg.second);
}
if (end != -2e9) re.push_back({ beg,end });
segs = re;