树状数组一般用来维护区间的前缀和,而线段树其实更适合用来解决RMQ问题。但是因为现在还没有复习线段树,所以先用着树状数组。(主要是好写)
树状数组基本的操作:区间和和单点修改,都可以在刘汝佳的训练指南里找到,这里不做详细说明。
主要总结以下几点:
1、树状数组解题的常规套路。(其实也就是前缀和的套路)
2、树状数组的区间最值查询
3、树状数组的区间修改以及区间修改后的区间和查询
4、离散化,由于树状数组用到都是+lowbit所以数组会开到很大,对于一些问题就需要离散化
uvaLIve 4329
题意:参照训练指南*
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<iostream>
#define ll long long
using namespace std;
const int maxn=1e6+50;
int t,a[maxn],n,c[maxn],ans1[maxn],ans2[maxn],max_loc;
ll answer;
int lowbit(int x){
return x&(-x);
}
int sum(int x){
int ret=0;
while(x>0){
ret+=c[x];
x-=lowbit(x);
}
return ret;
}
void add(int x,int d){
while(x<=100005){
c[x]+=d;
x+=lowbit(x);
}
}
int main(){
//freopen("out.txt","r",stdout);
while(scanf("%d",&t)!=EOF){
for(int i=1;i<=t;i++){
memset(a,0,sizeof(a));
memset(c,0,sizeof(c));
memset(ans1,0,sizeof(ans1));
memset(ans2,0,sizeof(ans2));
max_loc=answer=0;
scanf("%d",&n);
for(int j=1;j<=n;j++){
scanf("%d",&a[j]);///先输入的肯定在a[i]的左边
max_loc=max(max_loc,a[j]);
add(a[j],1);///每次放入一个a[j]
ans1[j]=sum(a[j]-1);///求左边比a[j]小的个数
}
memset(c,0,sizeof(c));
for(int j=n;j>=1;j--){
add(a[j],1);///先输入的肯定在a[j]右边
ans2[j]=sum(a[j]-1);///求右边比a[j]小的数
}
for(int j=2;j<=n;j++){
answer+=ans1[j]*(n-j-ans2[j])+ans2[j]*(j-1-ans1[j]);
}
printf("%lld\n",answer);
}
}
return 0;
}
atcoder3733
题意:移动最小次数使得一个字符串变成回文串,输出移动最小次数
这题之前用链表做过,TLE,后来看到别人的做法是运用树状数组,方法很巧妙。
参考:https://www.cnblogs.com/onioncyc/p/8099459.html
思路:对于一个字符串,如果奇数个的字母出现1次以上肯定是不可以的。然后考虑任意一个左边出现偶数次数的字母,移动次数最少,必然是把还未处理的最接近右边对应位置的那个字母移动到对应位置(从对应位置的左边找起)。所以输入的时候,按顺序记录下每个字母出现的位置,然后把树状数组中对应每个字母的位置标注为1。处理时,从左边第一个字母开始,找到对应的最右的位置 i,把该位置的树状数组的值置为0(相当于删除掉该元素),然后用sum(n)-sum(i-1)即为每次移动的次数。如果只有一个的话可以想象成两个放在一个位置,然后移动求和再除以二。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#define ll long long
#define debug cout<<"debug"<<endl;
using namespace std;
const int maxn=2e5+50;
char s[maxn];
int c[maxn],cnt[maxn],ok,n;
bool vis[maxn];
vector<int> loc[30];///vector要放在main函数外 不然tle
int lowbit(int x){
return x&(-x);
}
void add(int x,int d){
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=d;
}
}
int sum(int x){
int ret=0;
for(int i=x;i>0;i-=lowbit(i)){
ret+=c[i];
}
return ret;
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
memset(c,0,sizeof(c));
memset(cnt,0,sizeof(cnt));
memset(vis,0,sizeof(vis));
ok=0;
for(int i=1;i<=n;i++){
cnt[s[i]-'a']++;
loc[s[i]-'a'].push_back(i);///安顺序存放字母的位置
}
for(int i=0;i<26;i++){
if(cnt[i]%2) ok++;
}
if(ok>1){
printf("-1\n");
return 0;
}
for(int i=1;i<=n+1;i++){
add(i,1);
}
ll ans=0;
for(int i=1;i<=n;i++){
if(vis[i]) continue;
int c=s[i]-'a';
int loc_right=loc[c].back();
if(loc_right==i){
ans+=(sum(n)-sum(loc_right))>>1;///当遍历到只有一个字母的时候 根据对称性得
}
else{
ans+=sum(n)-sum(loc_right);
}
loc[c].pop_back();
add(i,-1);
add(loc_right,-1);
vis[i]=vis[loc_right]=true;
}
printf("%lld\n",ans);
//memset(s,0,sizeof(s));
return 0;
}
poj2299 求冒泡排序交换的次数(求逆序数,可以用归并排序的思想去做)
思路:首先对于每一个位置的数而言,交换的次数就是在它前面比它大的数,所以,仿照第一题的思路,记录每个数前比它大的数求和就可以,但是本题需要离散化
离散化:就是把一些大的数,变成相邻而比较小的数。一般思路:首先输入的时候除了记录数值大小本身外,还要记录数值出现的位置,然后对数值排序,再按原来记录的顺序输入回一个新数组。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>
#define ll long long
#define debug cout<<"debug"<<endl;
using namespace std;
const int maxn=1e6+50;
int n,a[maxn],c[maxn];
ll ans;
struct data{
int order;
ll val;
}d[maxn];
bool cmp(data a,data b){
return a.val<b.val;
}
int lowbit(int x){
return x&(-x);
}
void add(int x,int d){
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=d;
}
}
int sum(int x){
int ret=0;
for(int i=x;i>0;i-=lowbit(i)){
ret+=c[i];
}
return ret;
}
int main(){
while(scanf("%d",&n)!=EOF){
if(!n) break;
memset(d,0,sizeof(d));
memset(a,0,sizeof(a));
memset(c,0,sizeof(c));
ans=0;
for(int i=1;i<=n;i++){
scanf("%lld",&d[i].val);
d[i].order=i;///记录下顺序
}
sort(d+1,d+n+1,cmp);///对输入得数按值排序
for(int i=1;i<=n;i++){
a[d[i].order]=i;///再按顺序把离散化的值放入a数组
}
for(int i=1;i<=n;i++){
add(a[i],1);
ans+=i-sum(a[i]-1)-1;
}
printf("%lld\n",ans);
}
return 0;
}
思路: