官方题解对于思路的讲述已经非常的清晰了.
分享一个个人心得:
线段树对于左右区间的运用
线段树主要就是妙在它的合并区间功能以及对于一个整块进行分治的思想.你会发现,每次线段树的更新基本上都是基于左右儿子这两个左右区间的.对于可以区间维护的信息(基本上也就是可以分裂成子问题的问题),我们总是每次更新后就递归回溯根据当前点的左右儿子节点维护信息.线段树最妙的地方正是在于此处,不管是合并信息(合并答案),或者是分治处理信息(修改 or 查询),实际上我们都是得用到左右区间的.
例如这个题目:
要求我们维护一段区间内"IOI"的个数,实际上我们可以把问题转化为左右两个区间来解决.假设我们要查询的区间是\([L,R]\),我们可以先处理\([L,mid]\)的答案以及\([mid+1,R]\) 的答案.
同时我们发现有一部分答案是横跨了左右两个区间的,这时候我们就可以处理出左边的"I"的数量以及"IO"的数量(显然左区间的"OI"对于这种横跨左右两边的答案没有贡献),同时也处理出右区间"OI"、"I"的数量,这时候我们根据乘法原理(具体可以看官方题解)递归处理问题即可
而线段树是可以很好的适应这个问题的,因为其实线段树运用的也是分治的思想,它刚好也有左右区间,而且可以维护区间信息,并且有着优秀的\(logn\)(尽管常数比较大......)的时间复杂度,所以线段树就可以解决这道题目了.
贴上这道题的AC代码:
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n,Q;
char A[500005];
int a[500005];
struct node{
int i,o,oi,io,ioi,l,r;
void clean()
{
i = o = io = oi = ioi = 0;
return ;
}
}T[2000005];
node updata(node x,node l,node r)
{
x.oi = l.o * r.i + l.oi + r.oi;
x.io = l.i * r.o + l.io + r.io;
x.i = l.i + r.i;
x.o = l.o + r.o;
x.ioi = l.i * r.oi + l.io * r.i + l.ioi + r.ioi;
return x;
}
void build_tree(int x,int l,int r)
{
T[x].clean();
T[x].l = l , T[x].r = r;
if(l == r)
{
if(a[l] == 0)T[x].o = 1;
else T[x].i = 1;
return ;
}
int mid = (l + r) >> 1;
build_tree(x << 1 , l , mid);
build_tree(x << 1 | 1 , mid + 1 , r );
T[x] = updata(T[x],T[x << 1] , T[x << 1 | 1]);
return ;
}
node getans(int x,int l,int r)
{
if(T[x].l >= l && T[x].r <= r)return T[x];
int mid = (T[x].l + T[x].r) >> 1;
node Sum ;
Sum.clean();
if(l <= mid)Sum = updata(Sum,Sum,getans(x << 1, l , r));
if(r > mid)Sum = updata(Sum,Sum,getans(x << 1 | 1, l , r ));
return Sum;
}
void change(int x,int pos,int k)
{
if(T[x].l == T[x].r && T[x].l == pos)
{
if(k == 1)T[x].i = 1, T[x].o = 0;
else T[x].i = 0 , T[x].o = 1;
return ;
}
int mid = (T[x].l + T[x].r) >> 1;
if(pos <= mid)change(x << 1 , pos , k);
else change(x << 1 | 1 , pos , k );
T[x] = updata(T[x],T[x << 1] , T[x << 1 | 1]);
return ;
}
signed main()
{
cin >> n >> Q;
cin >> A;
for(int i = 0 ; i < n ; i ++)
{
if(A[i] == 'I')
a[i + 1] = 1;
else a[i + 1] = 0;
}
build_tree(1 , 1 , n);
while(Q)
{
int op,l,r;
char pos;
cin >> op >> l;
if(op == 2)
{
cin >> r;
cout << getans(1,l,r).ioi << endl;
}
else
{
cin >> pos;
if(pos == 'I')
change(1,l,1);
else change(1,l,0);
}
Q--;
}
return 0;
}
小结
线段树的题目我们不应该一开始就直接线段树暴力搞,而是应该先分析问题为什么能用线段树做,线段树做这道题优越性在哪里,这样子收获也许会多一点,也可以加深自己对于线段树的理解,线段树不仅仅是一个数据结构,或许也可以说成是一种思想