st表树状数组入门题单

预备知识

  1. st表(Sparse Table)

    主要用来解决区间最值问题(RMQ)以及维护区间的各种性质(比如维护一段区间的最大公约数)。

  2. 树状数组

    • 单点更新
    • 数组前缀和的查询

    拓展:原数组是差分数组时,可进行区间更新,单点查询,当然想区间查询也有办法(维护两个树状数组即可)。

区别

树状数组用来维护一个具有区间可减(加)性质的工具,所以可以用来维护区间前缀和。

区间最值不具有区间可减性,所以不能使用树状数组进行维护,而使用st表。

st表的思想

  • 初始化

    预处理数组, f[x][i] 表示区间[x, x + (1 << i) ) 的最值

    f[x][i] = min/max{f[x][i - 1], f[x + (1 << i - 1)][i - 1]}; // 1 << i - 1表示2的i-1次方

    其实这里就是一个动态规划的思想 :

    从x开始2的i次方的最值等于从 x 开始 (1 << i - 1) 范围内的最值加上从x + (1 << i - 1) 开始 (1 << i - 1)的最值

    类似于2 = 1 + 1
  • 询问区间内最值

    对于询问区间[l, r], 设 d = [log2(r - l + 1)]

    答案即为 : min/max{f[l][d], f[r - (1 << d) + 1][d]}

    至于为什么,很显然从左边开始(1 << d)区间内,要是够不到,就再从右开始

树状数组

数组模拟树形结构, 没啥好说的

题单

ST表

(Poj 3264 Balanced Lineup)

对应知识点

  • st表维护最值问题
想不出来查看思路
  利用st表开两个数组维护最大值和最小值

代码

ヾ(•ω•`)o还是写不出来就点我吧
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm> using namespace std;
typedef long long LL; const int N = 5e4 + 10;
int n, q; // n : 数的数量 q : 询问数量
int dp_max[N][20]; // 存储区间最大值
int dp_min[N][20]; // 存储区间最小值 // st表初始化
void st_init()
{
for(int j = 1; j <= 21; j ++)
for(int i = 1; i + (1 << j) - 1 <= n; i ++) {
dp_max[i][j] = max(dp_max[i][j - 1], dp_max[i + (1 << (j - 1))][j - 1]);
dp_min[i][j] = min(dp_min[i][j - 1], dp_min[i + (1 << (j - 1))][j - 1]);
}
} // st表询问区间内最大值及其最小值,得到它们的差
int query(int l, int r)
{
// int k = log2(r - l + 1);
int k=(int)(log(double(r-l+1)) / log(2.0));
int x = max(dp_max[l][k], dp_max[r - (1 << k) + 1][k]);
int y = min(dp_min[l][k], dp_min[r - (1 << k) + 1][k]);
return x - y;
} // 快读模板,过题用
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
} int main()
{
// ios::sync_with_stdio(false);
// cin >> n >> q;
n = read(), q = read();
for(int i = 1; i <= n; i ++)
{
int x;
x = read(); dp_max[i][0] = x;
dp_min[i][0] = x;
}
st_init();
for(int i = 1, l, r; i <= q; i ++)
{
l = read(), r = read();
printf("%d\n",query(l, r));
} return 0; }

(CF 1547F Array Stabilization (GCD version))

对应知识点

  • st表维护区间内最大公约数 + 二分答案

题目大意

给定一个序列a,每次对每一对相邻的数求他们的最大公约数,并且保存为新的序列。Latex不会打,可能题意不清楚,可以移步去洛谷

解题核心

gcd(a,b,c)=gcd(gcd(a,b),c)

  • 断环成链
  • 二分查询区间最大长度
ヾ(•ω•`)o还是写不出来就点我吧
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath> using namespace std; typedef long long LL;
const int N = 400010; int T, a[N];
int n;
LL g;
LL st[N][25];
LL gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
} void st_init(){
for(int i = 1; i <= 2 * n; i ++) st[i][0] = a[i]; // 赋初值 + 断环成链 // st表维护区间最大公约数
for(int j = 1; (1 << j) <= 2 * n; j ++){
for(int i = 1; i + (1 << j) <= 2 * n; i ++){
st[i][j] = gcd(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
}
}
} LL query(int l, int r){
int k = (int)(log(((double)(r - l + 1)))/log(2.0));
return gcd(st[l][k], st[r - (1 << k) + 1][k]);
} bool check(int x){
for(int i = 1; i <= n; i ++){
if(g != query(i, i + x)) return false;
}
return true;
}
int main()
{
scanf("%d", &T);
while(T --)
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++)
{
scanf("%d", &a[i]);
a[i + n] = a[i];
if(i == 1) g = a[i];
else g = gcd(g, a[i]);
}
st_init();
int l = 0, r = n - 1;
while(l < r){
int mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l); } return 0;
}

(CF1549D Integers Have Friends)

知识点同上题

这题需要用差分消去余数,剩下还是用st表维护最大公约数

ヾ(•ω•`)o还是写不出来就点我吧
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath> using namespace std;
typedef long long LL;
const LL N = 200010; LL a[N], b[N];
LL st[N][25];
LL n, T;
LL gcd(LL a, LL b){
return b ? gcd(b, a % b) : a;
} LL query(LL l, LL r){
LL k = (LL)(log((double)(r - l + 1)) / log(2.0));
return gcd(st[l][k], st[r - (1 << k) + 1][k]);
}
int main()
{
scanf("%d", &T);
while(T --)
{
scanf("%d", &n);
for(LL i = 1; i <= n; i ++) scanf("%lld", &a[i]);
if(n == 1)
{
printf("1\n");
continue;
}
for(LL i = 1; i < n; i ++){
b[i] = a[i] - a[i + 1];
if(b[i] < 0) b[i] = - b[i];
}
n--;
for(LL i = 1; i <= n; i ++) st[i][0] = b[i];
for(LL j = 1; (1 << j) <= n; j ++)
for(LL i = 1; i + (1 << j) - 1 <= n; i ++)
st[i][j] = gcd(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
LL ans = 0;
for(LL i = 1, l, r; i <= n; i ++){
l = i, r = n;
while(l < r){
LL mid = l + r + 1 >> 1;
if(query(i, mid) == 1) r = mid - 1;
else l = mid;
}
if(b[i] != 1) ans = max(ans, l - i + 1);
} printf("%lld\n", ans + 1);
} return 0;
}

树状数组

(HDU 1166 敌兵布阵)

对应知识点

  • 树状数组裸题(模板的使用)
ヾ(•ω•`)o还是写不出来就点我吧
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm> using namespace std; const int N = 50010;
int T, n;
char s[8];
int l, r;
int tr[N], a[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
} int lowbit(int x){
return x & -x;
}
void add(int k, int c){
while(k <= n) tr[k] += c, k += lowbit(k);
} int ask(int x){
int res = 0;
while(x) res += tr[x], x -= lowbit(x);
return res;
}
int main()
{
T = read();
int cnt = 1;
while(T -- )
{
n = read();
printf("Case %d:\n", cnt ++);
for(int i = 1; i <= n; i ++){
a[i] = read();
add(i, a[i]);
} while(1){
scanf("%s", s);
if(s[0] == 'Q'){
l = read(), r = read();
printf("%d\n", ask(r) - ask(l - 1));
}else if(s[0] == 'A'){
l = read(), r = read();
add(l, r);
}else if(s[0] == 'S'){
l = read(), r = read();
add(l, -r);
}else if(s[0] == 'E'){
break;
}
}
memset(tr, 0, sizeof(tr));
memset(a, 0, sizeof(a));
} return 0;
}

(HDU 1754 I Hate It)

对应知识点

  • 树状数组维护最大值(本来准备用st表的,但是有修改操作)

    具体做法就是把树状数组维护的区间和变成区间最大值,一开始实在没有想到这么写

    如果看不懂可以把这题的操作和树状数组最基本的两个操作代码对比起来看
ヾ(•ω•`)o还是写不出来就点我吧
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm> using namespace std; const int N = 2e6 + 10; int lowbit(int x){
return x & -x;
}
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int h[N], a[N];
int n, m;
// 树状数组维护最大值 时间复杂度O(logn^2)
void update(int x)
{
while(x <= n){
h[x] = a[x];
for(int i = 1; i < lowbit(x);i <<= 1)
h[x] = max(h[x], h[x - i]);
x += lowbit(x);
}
} int query(int x, int y)
{
int ans = 0; // 如果y-lowbit(y) >= x即直接取已经维护的h[y]最大值
// 否则逐个逼近最大值
while (y >= x)
{
ans = max(a[y], ans);
y --; // 已经取了第一个值
for (; y-lowbit(y) >= x; y -= lowbit(y))
ans = max(h[y], ans);
}
return ans;
} char ch[3];
int main()
{
while(~scanf("%d%d", &n, &m)){
for(int i = 1; i <= n; i ++) a[i] = read(), update(i);
for(int i = 1, l, r; i <= m; i ++){
scanf("%s", ch);
l = read(), r = read();
if(ch[0] == 'Q')
{
printf("%d\n", query(l, r));
}else{
a[l] = r;
update(l);
}
}
}
return 0;
}

(Poj 3468 A Simple Problem with Integers)

对应知识点

  • 树状数组的应用

    这题较模板而言的不同之处是要进行区间修改以及区间和

    对于区间修改我们简单的用树状数组维护一个差分数组

    对于区间和,我们可以维护两个树状数组,b[i] 和 i * b[i] 利用这两个数组去取得区间和

    (x + 1) * sum(Tr1, x) - sum(tr2, x)
ヾ(•ω•`)o还是写不出来就点我吧
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath> using namespace std;
const int N = 100010;
typedef long long LL; int a[N];
LL Tr1[N], tr2[N];
int n, m;
int lowbit(int x){
return x & -x;
}
void add(LL tr[], int x, LL c){
while(x <= n) tr[x] += c, x += lowbit(x);
} LL sum(LL tr[], int x){
LL res = 0;
while(x) res += tr[x], x-=lowbit(x);
return res;
} LL prefix_sum(int x){
return (x + 1) * sum(Tr1, x) - sum(tr2, x);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++){
int b = a[i] - a[i - 1];
add(Tr1, i, b);
add(tr2, i, (LL)i * b);
} while(m --){
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if(*op == 'Q'){
printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
}else{
scanf("%d", &d);
add(Tr1, l, d),add(tr2, l, (LL)l * d);
add(Tr1, r + 1, -d),add(tr2, r + 1, (LL)(r + 1)*(-d));
}
} return 0;
}

(Poj 2182 Lost cows)

对应知识点

  • 树状数组 + 二分

思路

tr数组存每个序号是否还有牛,故初始化为1即可。从后往前,牛的序号为前面比他小的再加上1

二分得到答案,二分的是比他小的牛的数量和

ヾ(•ω•`)o还是写不出来就点我吧
#include <iostream>
#define lowbit(x) x & (-x) using namespace std; const int N = 1e5;
int tr[N], pre[N], ans[N];
int n; void add(int x, int k)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
} int ask(int x)
{
int sum = 0;
for(int i = x; i; i -= lowbit(i)) sum += tr[i];
return sum;
} int findx(int x)
{
int l = 1, r = n;
while(l < r)
{
int mid = l + r >> 1;
if(ask(mid) < x)
l = mid + 1;
else
r = mid;
} return l;
}
int main()
{
cin >> n;
pre[1] = 0;
for(int i = 2; i <= n; i ++) cin >> pre[i]; for(int i = 1; i <= n; i ++){
tr[i] = lowbit(i);
// cout << tr[i] << ' ';
} for(int i = n; i > 0; i --){
int x = findx(pre[i] + 1);
add(x, - 1);
ans[i] = x;
} for(int i = 1; i <= n; i ++) cout << ans[i] << endl;
return 0;
}

(Poj 2299 Ultra-QuickSort)

过题

直接用归并排序

树状数组的后面学一手再补上来

ヾ(•ω•`)o还是写不出来就点我吧
#include <cstdio>

typedef long long LL;

const int N = 500010;

int n;
LL q[N], w[N]; LL merge_sort(int l, int r)
{
if (l == r) return 0; int mid = l + r >> 1;
LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= r)
if (q[i] <= q[j]) w[k ++ ] = q[i ++ ];
else
{
res += mid - i + 1;
w[k ++ ] = q[j ++ ];
}
while (i <= mid) w[k ++ ] = q[i ++ ];
while (j <= r) w[k ++ ] = q[j ++ ]; for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = w[j]; return res;
} int main()
{
while (scanf("%d", &n), n)
{
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]); printf("%lld\n", merge_sort(0, n - 1));
} return 0;
}

写到最后

第一次写博客,如果有不足请提出,我很乐意听取意见。

现在处于一个大量知识的学习时间段,希望博客能够让我所学积淀下来。

感谢观看( ̄︶ ̄*)) 完结撒花

上一篇:Lightoj 1112 - Curious Robin Hood 【单点改动 + 单点、 区间查询】【树状数组 水题】


下一篇:Java多线程14:生产者/消费者模型