树状数组题单记录

树状数组题单笔记

[SDOI2009] HH的项链

题目

思路

普通的 scanf 已经承受不住了,请使用关闭流同步的 cin 和 cout 或者经典快读

以下假设你已经使用了标准命名空间:

参考快读:

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;
}

参考关闭流同步:

ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);

暴力很好想,直接枚举区间内部即可。

但是但看题目,这道题似乎没有修改,只有查询,那么它是怎么和树状数组扯上关系的呢?

先把查询区间按照左端点排个序,然后看样例:

6
1 2 3 4 3 5
3
1 2
3 5
2 6

看起来区间 3 到 5 中,出现了重复的 3,2 到 6 中也有。此时,我们可以忽略其中一个 3。如果按照左端点排序,我们就可以得到以下查询序列:

3
1 2
2 6
3 5

此时,很明显能看出来 2 到 6 区间是包含 3 到 5 区间的,此时,我们可以通过更换排序方式来进一步找方法,这次以右端点为关键字排序:

3
1 2
3 5
2 6

此时,如果我们在查询排完序后的第 2 和第 3 个区间时,我们就可以忽略掉第一个 3,进而只关注第二个 3,这样,我们就可以发现,按照右端点为关键字递增排序后,我们可以依次遍历区间更新某种贝壳是否存在,如果存在,就把它的位置更新掉,然后区间求和,复杂度能过 不是因为我不会证

Code

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
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;
}
class Binary_Tree{
 
 private:
 
 int t[N],n;
 
 int lowbit(int x){ return x & (-x) ; }
 
 public:
 
 void init(int x){ n = x ; }
 
 void add(int x,int y){
  while(x<=n){
   t[x]+=y;
   x+=lowbit(x);
  }
 }
 
 int sum(int x){
  int reu=0;
  while(x>0){
   reu+=t[x];
   x-=lowbit(x);
  }
  return reu;
 }
 
}t;
struct ques{
 int l,r;
 int xb;
};
int n,m;
int a[N],ans[N];
int pl[N];
vector <ques> pro;
bool cmp(ques a,ques b){ return a.r == b.r ? a.l > b.l : a.r < b.r ; }
signed main(){
 n=read();
 for(int i=1;i<=n;i++)
  a[i]=read();
 m=read();
 pro.reserve(m);
 t.init(n);
 for(int i=1;i<=m;i++)
  pro.push_back({read(),read(),i});
 sort(pro.begin(),pro.end(),cmp);
 int l=1;
 for(ques q:pro){
  for(int i=l;i<=q.r;i++){
   if(pl[a[i]])
    t.add(pl[a[i]],-1);
   t.add(i,1);
   pl[a[i]]=i;
  }
  l=q.r+1;
  ans[q.xb]=t.sum(q.r)-t.sum(q.l-1);
 }
 for(int i=1;i<=m;i++)
  cout<<ans[i]<<endl;
 return 0;
}

三元上升子序列

题目

思路

这道题有两个条件:

  1. \(A_i < A_j < A_k\)
  2. $ i < j < k$

乍一看很难。以下约定:

  1. 三个数字分别为 \(A\) \(B\) \(C\)
  2. 这三个数字已经按照原题目默认顺序排好,即三个数字的位置以次递增其实不用排

但是,如果我们从中间的数字 \(B\) 入手,条件就可以拆成:

  1. \(B\) 前面且与 \(B\) 构成正序对的数量
  2. \(B\) 后面且与 \(B\) 构成正序对的数量

这个问题就好解多了。然后,根据乘法定理可知结果为它俩相乘。

记得 #define int long long

Code

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
#define int long long
class Binary_Tree{
 
 private:
 
 int t[N];
 int n;
 
 int lowbit(int x){ return x & (-x) ; }
 
 public:
 
 void init(int x){ n = x ; }
 
 void add(int x,int y){
  while(x<=n){
   t[x]+=y;
   x+=lowbit(x);
  }
 }
 
 int sum(int x){
  int reu=0;
  while(x>0){
   reu+=t[x];
   x-=lowbit(x);
  }
  return reu;
 }
 
}tl,tr;
int n;
int a[N],l[N],r[N];
signed main(){
 cin>>n;
 tl.init(n);
 tr.init(n);
 vector <int> u;
 u.reserve(n);
 for(int i=1;i<=n;i++)
  cin>>a[i],u.push_back(a[i]);
  
 sort(u.begin(),u.end());
 auto end=unique(u.begin(),u.end());
 for(int i=1;i<=n;i++)
  a[i]=lower_bound(u.begin(),end,a[i])-u.begin()+1;
 
 int ans=0;
 for(int i=1;i<=n;i++){
  l[i]=tl.sum(a[i]-1);
  tl.add(a[i],1);
 }
 for(int i=n;i>=1;i--){
  tr.add(a[i],1);
  r[i]=n-i-tr.sum(a[i])+1;
 }
 for(int i=2;i<n;i++)
  ans+=l[i]*r[i];
 cout<<ans;
 return 0;
}

P3253 [JLOI2013] 删除物品

题目

思路

首先想一下模拟:

对于两个栈,由于每次删除只能删除当前优先度最大的物品 \(A\),因此每次移动时就将 \(A\) 移动到另一个栈的顶端并使顺序反过来,然后删除 \(A\) 并寻找下一个 \(A\)。循环直到所有物品被删完。

代码:

纯手写的奇妙的支持查询和离散化的栈
十年 OI 一场空,不开 long long 见祖宗

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
#define int long long
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;
}
class SpecialStack{
 
 private:
 
 int num[N];
 int topx,len;
 bool vis[N];
 
 public:
 
 inline void init(){
  topx=0 , len = 0;
  memset(vis,0,sizeof(vis));
 }
 inline void invis_push(int x,int i){
  topx = max(topx,i);
  num[i] = x ;
  len ++ ;
 }
 inline void push(int x){
  num[++topx] = x ;
  vis[num[topx]]=1;
  len ++ ;
 }
 inline int top(){ return num[topx] ; }
 inline void pop(){
  vis[num[topx]] = 0 , topx -- , len -- ;
 }
 inline void lsh(auto begin,auto end,int &mx){
  for(int i=1;i<=topx;i++){
   num[i]=lower_bound(begin,end,num[i])-begin+1;
   vis[num[i]]=1;
   mx=max(mx,num[i]);
  }
 }
 inline void clear(){ topx = len = 0 ; }
 inline bool find(int x){ return vis[x] ; }
 inline bool empty(){ return len ; }
}s1,s2;
int n1,n2;
int ans=0;
vector <int> l;
signed main(){
 cin>>n1>>n2;
 s1.init();
 s2.init();
 l.reserve(n1+n2);
 int num;
 for(int i=n1;i>=1;i--)
  s1.invis_push(num=read(),i),l.push_back(num);
 for(int i=n2;i>=1;i--)
  s2.invis_push(num=read(),i),l.push_back(num);
 sort(l.begin(),l.end());
 auto len=unique(l.begin(),l.end());
 int mx=0;
 int ans=0;
 s1.lsh(l.begin(),len,mx);
 s2.lsh(l.begin(),len,mx);
 while(mx)
  if(s1.find(mx)){
   while(s1.top()!=mx)
    s2.push(s1.top()),s1.pop(),ans++;
   s1.pop();
   mx--;
  }
  else{
   while(s2.top()!=mx)
    s1.push(s2.top()),s2.pop(),ans++;
   s2.pop();
   mx--;
  }
 cout<<ans;
 return 0;
}

然后你就会 T 掉一半的点。

现在我们来看一下两个栈之间来回交换物品的情况:

现在我们手造一组数据(从栈顶到栈底):

3 3
1
2
3

4
5
6

两个栈的物品优先度分别为 1 2 3 和 4 5 6。现在我们要删除 6,看一下删除 6 之前两个栈的情况:

5 1
5
4
1
2
3

6

删除 6 之后,我们要删除 5,假如我们将 4 和 5 放回到原来的栈里:

3 2
1
2
3

4
5

可以发现,4 和 5 的相对位置没有变。

假设 4 5 我们已经删除掉了,现在我们只剩 1 2 3 需要删除:

3 0
1
2
3

3 在栈底,我们将 1 和 2 拿到另一个栈里:

1 2
3

2
1

现在可以直接依次删除 3 2 1 了。这个过程,我们很容易发现:数个物品一起在栈中来回拿的时候,它们的相对位置保持不变。因此,我们可以将两个栈的头相对接,使用一个变量来保存当前的头在哪里,就不用真的模拟来回移动物品的行为了。

那么这里和树状数组有什么关系呢?

删除掉一个物品时,我们要将这个物品的存在抹去,以防止对下次操作时计算步数产生影响。但是,如果每次删除物品都要把数据来回移动的话就和没有优化没什么区别。因此,我们可以使用树状数组进行单点修改(删除物品存在)和区间查询(统计所需步数)。

在开始时,先将所有物品添加到数组里,然后每删除一个就将它的存在抹去(加上 -1),此操作是 \(O(logN)\) 的,总时间复杂度是 \(O(NlogN)\) 的。

Code

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
#define int long long
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;
}
struct node{
 int val,id;
}a[N];
bool cmp(node a,node b){ return a.val > b.val ; }
int ans=0;
int n1,n2,top,mx,n;
int t[N];
int lowbit(int x){ return x & (-x) ; }
void add(int x,int y){
 while(x<=n){
  t[x]+=y;
  x+=lowbit(x);
 }
}
int sum(int x){
 int reu=0;
 while(x){
  reu+=t[x];
  x-=lowbit(x);
 }
 return reu;
}
signed main(){
 cin>>n1>>n2;
 n=n1+n2;
 top=n1;
 for(int i=n1;i>=1;i--)
  a[i].val=read(),a[i].id=i,add(i,1);
 for(int i=n1+1;i<=n;i++)
  a[i].val=read(),a[i].id=i,add(i,1);
 sort(a+1,a+1+n,cmp);
 for(int i=1;i<=n;i++){
  mx=a[i].id;
  if(top<mx)
   ans+=sum(mx-1)-sum(top),top=mx-1,add(a[i].id,-1);
  else
   ans+=sum(top)-sum(mx),top=mx,add(a[i].id,-1);
 }
 cout<<ans;
 return 0;
}

P3660 [USACO17FEB] Why Did the Cow Cross the Road III G

题目

思路

题目条件很明显:

\(1~N\) 每个数字出现过两遍,第一次记为 \(a_i\),第二次记为\(b_i\),求 \(a_i<a_j<b_i<b_j\) 的对数,即求两个数构成的数对,其中数 \(i\) 每次出现的位置都比数 \(j\) 出现的位置早但是 \(i\) 第二次出现的位置比 \(j\) 第一次出现的位置晚,这样的数对的数量。

为什么题目不写翻译直接写题目要求的结果啊啊啊啊

首先,我们将输入的牛按照第一次出现的位置排序,这样从头遍历时就满足了 \(a_i<a_j\) 的条件。

然后我们来求 \(b_i<b_j\) 的数量。这里可以直接用树状数组来求,每次更新完后将第二次出现的位置累加进树状数组即可。

最后我们来满足 \(a_j<b_i\)。现在树状数组里的点都是前面已经遍历过的第二次出现的位置。我们只需要在答案里减去在当前牛第一次出现位置前面的位置即可。

Code

#include <bits/stdc++.h>
using namespace std;
const int N=50005;
#define ipi pair <int,int> 
int t[N*2];
int n;
ipi pl[N];
bool cmp(ipi x,ipi y){ return x.first < y.first ; }
int lowbit(int x){ return x & (-x) ; }
void add(int x,int y){
 while(x<=n*2){
  t[x]+=y;
  x+=lowbit(x);
 }
}
int sum(int x){
 int reu=0;
 while(x>0){
  reu+=t[x];
  x-=lowbit(x);
 }
 return reu;
}
signed main(){
 cin>>n;
 int num;
 for(int i=1;i<=n;i++)
  pl[i].first=pl[i].second=0;
 for(int i=1;i<=n*2;i++){
  cin>>num;
  if(!pl[num].first)
   pl[num].first=i;
  else
   pl[num].second=i;
 }
 sort(pl+1,pl+1+n,cmp);
 int ans=0;
 for(int i=1;i<=n;i++){
  ans+=sum(pl[i].second)-sum(pl[i].first-1);
  add(pl[i].second,1);
 }
 cout<<ans;
 return 0;
}

P3605 [USACO17JAN] Promotion Counting P

P3605 [USACO17JAN] Promotion Counting P

思路

这道题问题是求一个上司手底下的下属中有多少人比他能力值高,数据保证是一棵树也就是说,你 DFS 时不会死循环

你让我一个没学过 DFS 的人怎么写

首先,1 号牛是根节点,我们就从他开始深搜,每次深搜结束后更新该头牛满足条件的下属数量。

由于牛牛们的能力值有亿点大,所以我们离散化一下。

Code

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
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 n;
int p[N],ans[N];
vector <int> g[N];
vector <int> l;
int t[N];
int lowbit(int x){ return x & (-x) ; }
int sum(int x){
 int reu=0;
 while(x){
  reu+=t[x];
  x-=lowbit(x);
 }
 return reu;
}
void add(int x,int y){
 while(x<=n){
  t[x]+=y;
  x+=lowbit(x);
 }
}
void dfs(int x){
 ans[x]-=sum(n)-sum(p[x]);
 for(int v:g[x])
  dfs(v);
 ans[x]+=sum(n)-sum(p[x]);
 add(p[x],1);
}
signed main(){
 n=read();
 l.reserve(n);
 for(int i=1;i<=n;i++)
  p[i]=read(),l.push_back(p[i]);
 sort(l.begin(),l.end());
 auto len=unique(l.begin(),l.end());
 for(int i=1;i<=n;i++)
  p[i]=lower_bound(l.begin(),len,p[i])-l.begin()+1;
 for(int i=2;i<=n;i++)
  g[read()].push_back(i);
 dfs(1);
 for(int i=1;i<=n;i++)
  cout<<ans[i]<<endl;
 return 0;
}

补充

可能有人会问,为什么在搜索前要先减掉 \(sum(n)-sum(p[x])\)?首先,我们要知道,这个表达式的意思是有多少牛牛的能力值比当前牛的能力值高。

现在想像一下,假如说我们已经搜索到了一头牛,那么它前面搜索到的牛也存在树状数组里。如果我们直接不管不顾搜完它的下属就加和,就可能把它的能力值比它高的上司给计入答案中,所以我们要先把能力值比它高的上司减去,这样再加和时会把它的上司再加进来,正负抵消等于没记。

posted @ 2024-06-02 14:54  CLydq  阅读(17)  评论(0编辑  收藏  举报