老规矩,推荐一篇原理讲解清晰的博客!(树状数组(详细分析+应用),看不懂打死我!_树形数组_鲜果维他命的博客-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) 处添加值 kvoid 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); // 在指定区域添加值 numAdd(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);
}