老规矩,推荐一篇原理讲解清晰的博客!(树状数组(详细分析+应用),看不懂打死我!_树形数组_鲜果维他命的博客-CSDN博客)
相对于线段树,树状数组的优点就是代码简洁,容易修改。单缺点就是优点问题只有线段树才能解决,树状数组有一定的局限性。
1,模板
(1)单点修改,区间查询
int lowbit(int x) {
return x & (-x);
}
int add_dandian(int pos, int k)
{
for (int i = pos; i <= n; i += lowbit(i)) c[i] += k;
}
int search(int L, int R)
{
//利用前缀和相减的性质,[L, R] = [1, R] −[1, L − 1]
int ans = 0;
for (int i = L - 1; i; i -= lowbit(i)) ans -= c[i];
for (int i = R; i; i -= lowbit(i)) ans += c[i];
return 0;
}
(2)区间修改,单点查询
我们需要构造出原数组的差分数组b,然后用树状数组维护b数组即可
对于区间修改的话,我们只需要对差分数组进行操作即可,例如对区间[L,R]+k,那么我们只需要更
新差分数组add(L,k),add(R+1,-k),这是差分数组的性质.
int lowbit(int x) {
return x & (-x);
}
void update(int pos, int k)//pos表示修改点的位置,K表示修改的值也即+K操作
{
for (int i = pos; i <= n; i += lowbit(i)) c[i] += k;
}
void range_add(int L, int R, int k){
update(L, k);
update(R + 1, -k);
}
int ask(int pos)//返回区间pos到1的总和
{
int ans = 0;
for (int i = pos; i; i -= lowbit(i)) ans += c[i];
return ans;
}
(3)区间修改,区间查询
void add(ll p, ll x){
for(int i = p; i <= n; i += i & -i)
sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
add(l, x), add(r + 1, -x);
}
ll ask(ll p){
ll res = 0;
for(int i = p; i; i -= i & -i)
res += (p + 1) * sum1[i] - sum2[i];
return res;
}
ll range_ask(ll l, ll r){
return ask(r) - ask(l - 1);
}
2,题目练习
(1)【模板】树状数组 1 - 洛谷
AC代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n,m,a[N];
int lowbit(int x)
{
return x&(-x);
}
void add(int pos,int k);
int search(int l,int r);
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){
//cin>>a[i];
int x;
cin>>x;
add(i,x);
}
for(int i=0;i<m;i++){
int flag,l,r;
cin>>flag>>l>>r;
if(flag==1) add(l,r);
else cout<<search(l,r)<<endl;
}
return 0;
}
void add(int pos,int k)
{
for(int i=pos;i<=n;i+=lowbit(i)){
a[i]+=k;
}
}
int search(int l,int r)
{
int sum=0;
for(int i=r;i;i-=lowbit(i)){
sum+=a[i];
}
for(int i=l-1;i;i-=lowbit(i)){
sum-=a[i];
}
return sum;
}
(2)【模板】树状数组 2 - 洛谷
AC代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n,m,a[N],b[N];
int lowbit(int x)
{
return x&(-x);
}
void add(int pos,int k);
int find(int pos);
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]-a[i-1]);//差分
}
for(int i=0;i<m;i++){
int flag,l,r,k;
cin>>flag;
if(flag==1){
cin>>l>>r>>k;
add(l,k);
add(r+1,-k);
}
else{
cin>>l;
cout<<find(l)<<endl;
}
}
return 0;
}
void add(int pos,int k)
{
for(int i=pos;i<=n;i+=lowbit(i)) b[i]+=k;
return;
}
int find(int pos)
{
int sum=0;
for(int i=pos;i;i-=lowbit(i)) sum+=b[i];
return sum;
}
(3)逆序队 逆序对 - 洛谷
AC代码
#include <bits/stdc++.h>
using namespace std;
int n, a[5000001], b[5000001];
long long ans;
inline void msort(int l, int r)//归并排序
{
int mid = (l + r) / 2;//取中间
if(l == r)//若l == r了,就代表这个子序列就只剩1个元素了,需要返回
{
return;
}
else
{
msort(l, mid);//分成l和中间一段,中间 + 1和r一段(二分)
msort(mid + 1, r);
}
int i = l;//i从l开始,到mid,因为现在排序的是l ~ r的区间且要二分合并
int j = mid + 1;//j从mid + 1开始,到r原因同上
int t = l;//数组b的下标,数组b存的是l ~ r区间排完序的值
while(i <= mid && j <= r)//同上i,j的解释
{
if(a[i] > a[j])//如果前面的元素比后面大(l ~ mid中的元素 > mid + 1 ~ r中的元素)(逆序对出现!!!)
{
ans += mid - i + 1;//由于l ~ mid和mid + 1 ~ r都是有序序列所以一旦l ~ mid中的元素 > mid + 1 ~ r中的元素而又因为第i个元素 < i + 1 ~ mid那么i + 1 ~ mid的元素都 > 第j个元素。所以+的元素个数就是i ~ mid的元素个数,及mid - i + 1(归并排序里没有这句话,求逆序对里有)
b[t++] = a[j++];//第j个元素比i ~ mid的元素都小,那么第j个元素是目前最小的了,就放进b数组里
//++j;//下一个元素(mid + 1 ~ r的元素小,所以加第j个元素)
}
else
{
b[t++] = a[i++];//i小,存a[i]
//++i;//同理
}
}
while(i <= mid)//把剩的元素(因为较大所以在上面没选)
{
b[t++] = a[i++];//存进去
//++i;
}
while(j <= r)//同理
{
b[t++] = a[j++];
//++j;
}
for(int i = l; i <= r; ++i)//把有序序列b赋值到a里
{
a[i] = b[i];
}
return;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
{
scanf("%d", &a[i]);
}
msort(1, n);//一开始序列是1 ~ n
printf("%lld", ans);
return 0;
}
(4)康托展开 【模板】康托展开 - 洛谷
AC代码
#define _CRT_SECURE_NO_WARNINGS
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 10;
const int mod = 998244353;
int a[N], w[N]={1,1},tr[N], n,ans;
int lowbit(int x) {
return x & (-x);
}
void update(int pos, int k) {
for (int i = pos; i <= n; i += lowbit(i)) tr[i] += k;
return;
}
int query(int pos)
{
int sum = 0;
for (int i = pos; i; i -= lowbit(i)) sum+=tr[i];
return sum;
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; i++) {//求阶乘
w[i] = (i * w[i - 1]) % mod;
update(i, 1);
}
for (int i = 1; i <= n; i++) {
cin >> a[i];
ans = (ans + ((query(a[i]) - 1) * w[n-i]) % mod) % mod;
update(a[i], -1);//减1后就变成0了
}
cout << ans+1 << endl;
return 0;
}
(5)二维树状数组 上帝造题的七分钟 - 洛谷
思想:和二维前缀和的思路很相似
AC代码
#define _CRT_SECURE_NO_WARNINGS
#include <bits/stdc++.h>
using namespace std;
const int N = 3000;
int board[N][N];
int n, m;
// 定义树状数组结构(树状数组三件套)
struct BIT
{
int tr[N][N]; // 树状数组
int lowbit(int x) {
return x & (-x); // 返回 x 的最低位的 1 所在位置
}
// 在坐标 (x, y) 处添加值 k
void add(int x, int y, int k)
{
for (int i = x; i <= n; i += lowbit(i)) {
for (int j = y; j <= m; j += lowbit(j)) {
tr[i][j] += k; // 在 (i, j) 处加上值 k
}
}
}
// 查询坐标 (x, y) 处的前缀和
int query(int x, int y)
{
int sum = 0;
for (int i = x; i; i -= lowbit(i)) {
for (int j = y; j; j -= lowbit(j)) {
sum += tr[i][j]; // 查询 (1, 1) 到 (x, y) 的前缀和
}
}
return sum;
}
} A, Ai, Aj, Aij; // 定义四个不同的树状数组
void Add(int x, int y, int k);
int Ans(int x, int y);
int main()
{
char ch;
cin >> ch >> n >> m; // 读取矩阵大小
while (cin >> ch) {
int x1, x2, y1, y2;
cin >> x1 >> y1 >> x2 >> y2;
if (ch == 'L') {
int num;
cin >> num;
Add(x1, y1, num); // 在指定区域添加值 num
Add(x1, y2 + 1, -num);
Add(x2 + 1, y1, -num);
Add(x2 + 1, y2 + 1, num);
}
else {
cout << Ans(x2, y2) - Ans(x1 - 1, y2) - Ans(x2, y1 - 1) + Ans(x1 - 1, y1 - 1) << endl;
// 查询并输出给定矩形区域的和
}
}
return 0;
}
// 计算 (x, y) 处的结果
int Ans(int x, int y)
{
return A.query(x, y) * (x * y + x + y + 1) - Ai.query(x, y) * (y + 1) - Aj.query(x, y) * (x + 1) + Aij.query(x, y);
}
// 在坐标 (x, y) 处添加值 num
void Add(int x, int y, int num)
{
A.add(x, y, num);
Ai.add(x, y, num * x);
Aj.add(x, y, num * y);
Aij.add(x, y, num * x * y);
}