SKULL

一言(ヒトコト)

【寻迹#7】树状数组

树状数组

一、简介

树状数组是一种支持 单点修改区间查询 的,代码量小的数据结构。

普通树状数组维护的信息及运算要满足 结合律可差分,如加法(和)、乘法(积)、异或等。

事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小。

树状数组能快速求解信息的原因:我们总能将一段前缀 \([1, n]\)拆成 不多于 \(\log n\) 段区间,使得这段区 \(\log n\) 间的信息是已知的

1.管辖区间

树状数组其中的一位会管辖原数组的一位或多位并维护相应的信息

那么每一位具体的管辖区间是多少?

树状数组中,规定 \(c[x]\) 管辖的区间长度为 \(2^k\) ,其中:

  • 设二进制最低位为第 \(k\) 位,则 \(k\) 恰好为 \(x\) 二进制表示中,最低位的 1 所在的二进制位数;
  • \(2^k\)c[x]\(c[x]\) 的管辖区间长度)恰好为 \(x\) 二进制表示中,最低位的 1 以及后面所有 0 组成的数。

我们记 \(x\) 二进制最低位 1 以及后面的 0 组成的数为 \(\operatorname{lowbit}(x)\) ,那么 \(c[x]\) 管辖的区间就是 \([x-\operatorname{lowbit}(x)+1,x]\)

这里注意:\(\operatorname{lowbit}(x)\) 指的不是最低位 1 所在的位数 \(k\) ,而是这个 1 和后面所有 0 组成的 \(2^k\)

怎么计算 lowbit?根据位运算知识,可以得到 lowbit(x) = x & -x。‘’

1.1怎么计算 \(\operatorname{lowbit}(x)\)

首先我们要知道负数在存储的时候是用的补码,也就是反码 \(+1\)

设原先 \(x\) (正数)的二进制编码是 \((...)10...00\) ,全部取反后得到 \([...]01...11\) ,再加一得到 \([...]10...00\) ,即 \(-x\) 的补码,这两个做一下运算,前面的 \(()\)\([]\) 里的部分都变成 \(0\) ,后面的部分正好为 \(10...00\) ,即 \(\operatorname{lowbit}(x)\)

2.区间查询

任何一个区间查询都可以这么做:查询 \(a[l...r]\) 的和,就是的 \(a[1...r]\) 和减去 \(a[1...l-1]\) 的和 ,从而把区间问题转化为前缀问题,更方便处理。

我们可以写出查询 \(a[1...x]\) 的过程:

  • \(c[x]\) 开始往前跳,有 \(c[x]\) 管辖 \(a[x-\operatorname{lowbit}(x)+1...x]\)
  • \(x \gets x - \operatorname{lowbit}(x)\) ,如果 \(x=0\) 说明已经跳到尽头了,终止循环;否则回到第一步。
  • 将跳到的 \(c\) 合并。
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
} 

3.树状数组与其树形态的性质

具体树状数组的其它性质可以参见OI-WIKI这篇文章

我们在这里就可以把树状数组的树形态理解为 \(x\)\(x+\operatorname{lowbit}(x)\) 连边得到的图,其中 \(x+\operatorname{lowbit}(x)\)\(x\) 的父亲。

4.单点修改

目标是快速正确地维护 \(c\) 数组,所以我们只需要遍历管辖了 \(a[x]\) 的所有 \(c[y]\)

管辖 \(a[x]\)\(c[y]\) 一定包含了 \(c[x]\) ,在树形态上, \(y\)\(x\) 的祖先。因此我们可以从 \(x\) 开始不断跳父亲,直到跳得超过了原数组长度为止。

\(n\) 表示 \(a\) 的大小,单点修改过程如下:

  • 初始令 \(x'=x\)
  • 修改 \(c[x']\)
  • \(x'\gets x'+\operatorname{lowbit}(x')\) ,如果 \(x'>n\) 说明已经跳到尽头了,终止循环;否则回到第二步。
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	} 
}

5.建树

也就是根据最开始给出的序列,将树状数组建出来( \(c\) 全部预处理好)。

一般可以直接转化为 \(n\) 次单点修改,时间复杂度 \(O(n\log n)\)

但是也有 \(O(n)\) 建树的方法,如下

inline void build()
{
	for(int i=1;i<=n;i++)
	{
		t[i]+=a[i];
		int j=i+lowbit(i);
		if(j<=n) t[j]+=t[i];
	}
}

6.权值树状数组

6.1权值数组

首先来了解权值数组的定义,一个数组 \(a\) 的权值数组 \(b\) ,满足 \(b[x]\) 表示 \(x\)\(a\) 中的出现次数,显然 \(b\) 的大小和 \(a\) 的值域有关,若原数据过大,可以将原数组离散化后再建立权值数组。

6.2 单点修改,查询全局第 \(k\)

运用权值树状数组,我们可以解决一些经典问题。

首先这个问题还是只需要数据之间的相对大小,所以可以离散化。只不过需要注意的是,要把单点修改中涉及到的值一起离散化。

对于单点修改,可以转化为对权值数组的单点修改,即当原数组 \(a[x]\)\(y\) 变成 \(z\) 的时候,转化为对权值数组 \(b\) 的单点修改,即 \(b[y]\) 单点减一, \(b[z]\) 单点加一。

对于查询第 \(k\) 小,考虑二分 \(x\) ,查询权值数组中 \([1,x]\) 的前缀和,找到 \(x_0\) 使得 \([1,x_0]\) 的前缀和 \(<k\)\([1,x_0+1]\) 的前缀和 \(\geq k\) ,则第 \(k\) 大的数是 \(x_0+1\) ,但这样做的复杂度是 \(O(\log^2n)\) 的。

考虑一个更快的做法——倍增。

\(x=0,sum=0\) ,枚举 \(i\)\(\log_2n\) 降为 \(0\)

  • 查询权值数组中 \([x+1...x+2^i]\) 的区间和 \(t\)
  • 如果 \(sum+t<k\) ,则拓展成功, \(x\gets x+2^i\)\(sum\gets sum+t\) ;否则拓展失败不操作

这样得到的 \(x\) 是满足 \([1...x]\) 的前缀和 \(<k\) 的最大值,所以最终 \(x+1\) 就是答案。

为什么这样做更快?

因为这里的前缀和只需要访问 \(c[x+2^i]\) 的值即可。

考虑 \(\operatorname{lowbit}(x+2^i)\) 一定等于 \(2^i\) ,因为 \(i\) 是从 \(\log_2n\) 递减下来的,也就是说 \(x\) 之前只累加过 \(2^j\) 满足 \(j>i\) ,因此 \(c[x+2^i]\) 表示的区间就是 \([x+1...x+2^i]\)

这样的话直接访问 \(c[x+2^i]\) 复杂度变为 \(O(1)\) ,所以倍增下时间复杂度就变为 \(O(\log n)\)

二、题单

1. 【模板】树状数组 1

传送门

思路:

经典的单点修改,区间查询,用模板即可

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 500050
int n,m,a[N];
int t[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;
}
inline int lowbit(int x) { return x&-x; }
inline void build()
{
	for(int i=1;i<=n;i++)
	{
		t[i]+=a[i];
		int j=i+lowbit(i);
		if(j<=n) t[j]+=t[i];
	}
}
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	} 
}
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
} 
int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	build();
	for(int i=1;i<=m;i++)
	{
		int opt,x,y;
		opt=read();x=read();y=read();
		if(opt==1) add(x,y);
		else cout<<query(y)-query(x-1)<<endl;
	}
	return 0;
} 

2.【模板】树状数组 2

传送门

思路:

本题关键在于转化,想到我们已经会了单点修改区间查询,那就要往这上面转化,

考虑维护一个 \(a\) 数组的差分数组 \(b\) ,那么 \(b\) 的前缀和就是 \(a\) 中对应的元素,

于是我们发现单点查询已经搞定了(已经变成了区间和),

那单点修改呢,

我们发现如果维护一个与 \(b\) 有关的树状数组,

那么对于 \(a\) 数组,要修改区间 \([l,r]\) 内的元素时,其实只需要更改 \(b\)\(b[l]\)\(b[r+1]\) 即可。也把区间修改转化为了单点修改

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 500050
int n,m,a[N];
int b[N],t[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;
}
inline int lowbit(int x) { return x&-x; }
inline void build() 
{
	for(int i=1;i<=n;i++)
	{
		t[i]+=b[i];
		int j=i+lowbit(i);
		if(j<=n) t[j]+=t[i];
	}
}
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
int main()
{
	n=read();
	m=read();
	memset(a,0,sizeof(a));
	memset(b,0,sizeof(b));
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		b[i]=a[i]-a[i-1];
	}
	build();
	for(int i=1;i<=m;i++)
	{
		int opt,x,y,k;
		opt=read();x=read();
		if(opt==1)
		{
			y=read();k=read();
			add(x,k);add(y+1,-k);
		}
		else cout<<query(x)<<endl;
	}
	return 0;
}

3.简单题

传送门

思路:

诶这道题其实就是上面的模板题的小变式, \(0\)\(1\)\(1\)\(0\) 的操作很容易想到异或,

然后自己推导一下,发现异或运算满足结合律,并且可差分,

那就说明可以用树状数组维护,

又看到是区间修改,单点查询,那直接维护一个差分树状数组即可,

只不过这里的差分并不是加法的逆运算,而是异或的逆运算。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,m,a[N];
int t[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;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x)
{
	while(x<=n)
	{
		t[x]^=1;
		x+=lowbit(x);
	}
}
inline int query(int x)
{
	int sum=t[x];x-=lowbit(x);
	while(x>0)
	{
		sum^=t[x];
		x-=lowbit(x);
	} 
	return sum;
}
int main()
{
	n=read();m=read();
	memset(a,0,sizeof(a));
	for(int i=1;i<=m;i++)
	{
		int opt,x,y;
		opt=read();x=read();
		if(opt==1)
		{
			y=read();
			add(x);add(y+1);
		}
		else printf("%d\n",query(x));
	}
	return 0;
} 

4.约瑟夫问题

传送门

方法一:

思路:有一个用链表做的比较好想的思路,就是开一个 \(Next\) 数组记他的后继,当这个人出局以后,就把他的后继改成他后继的后继。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 1050
int m,n;
int Next[N];
int main()
{
	cin>>n>>m;
	for(int i=0;i<n;i++) Next[i]=i+1;
	Next[n]=1;
	int p=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<m;j++) p=Next[p];
		cout<<Next[p]<<" ";
		Next[p]=Next[Next[p]];
	}
	return 0;
} 

方法二:

思路:

用权值树状数组维护,初始所有权值数组均为 \(1\) ,都出现了一次,

然后去查全局第 \(k\) 小,只不过 \(k\) 在变化,

每次查完 \(k\) ,就接着去查 \(k+m-1\) 即可,注意取模,

取模可以写成 \([(k+m-1-1)\mod (n-i)]+1\) (注意模数不能为 \(0\) !),这样去掉了模出来是 \(0\) 的结果,

取模完把这个人删掉即可。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 150
int n,m;
int t[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;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline int kth(int k)
{
	int x=0,sum=0;
	for(int i=log2(n);i>=0;i--)
	{
		if(x+(1<<i)>n||sum+t[x+(1<<i)]>=k) continue;
		x+=(1<<i);
		sum+=t[x];
	}
	return x+1;
}
int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++) add(i,1);
	int k=m;
	for(int i=1;i<=n;i++)
	{
		int x=kth(k);
		cout<<x<<" ";
		if(i==n) break;
		add(x,-1);
		k=(k+m-1-1)%(n-i)+1;
	}
	return 0;
} 

5.Sleepy Cow Sorting

传送门

思路:

首先先考虑最小操作次数,发现是从右向左第一个上升的位置(手玩一下不难发现),

然后考虑前面每一个数的操作数,

由题意可知第 \(i\) 头牛的排名就是 \(a[i]\)

我们用 \(cnt\) 记录已排序的数的个数,那么未排序的个数为 \(n-cnt\)

每次移动队列最前面的那头牛要考虑,

前面有多少头比这头牛编号小的已经排好序了,

那么操作数就是 \(n-cnt+query(a[i])-1\)

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,a[N];
int t[N],cnt,tot,ans[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;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x); 
	}
}
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=n;i>=1;i--)
	{
		add(a[i],1);cnt++;
		if(a[i]<a[i-1]) break;
	}
	tot=n-cnt;
	for(int i=1;i<=tot;i++)
	{
		ans[i]=n-cnt+query(a[i])-1;
		add(a[i],1);
		cnt++;
	}
	cout<<tot<<endl;
	for(int i=1;i<=tot;i++) cout<<ans[i]<<" ";
	return 0;
}

6.逆序对

传送门

思路:

一道权值树状数组题目。

想了好久终于想明白了,首先看到数据范围比较大,

并且我们要维护的是逆序对,所以数字实际大小其实没什么意义,只需要相对大小就可以了,那我们也可以离散化了,

离散化以后,每个数就变成了它所在数组中的排名,

以从小到大排序为例,题目中的 \(、、、、、5、4、2、6、3、1\) 就变为 \(、、、、、5、4、2、6、3、1\) 确实没啥变化,但在这里每个数是它的相对排名,

然后我们考虑建树,

从左向右,将树状数组中 \(t[x]+1\)\(x\) 为所有的排名,

然后我们去查询,

查询是向下查询的,即 \(x'=x-\operatorname{lowbit}(x)\) ,它的排名 \(x'\) 要小于 \(x\)

如果 \(t[x']\) 不为空,那么它一定是在之前进入的树状数组,那么我们就可以推断这两个数不是逆序的(排名小且靠前进入),

当第 \(i\) 个数进入树状数组时它会与前面 \(i-1\) 个数产生 \(i-1\) 个数对,这其中有 \(\left(query(x)-1\right)\) 个不是逆序对的(要去掉刚加进去的自己),两者相减即为逆序对个数,即 \(i-query(x)\) 个。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 500050
typedef long long ll;
int n,a[N];
int t[N],tmp[N],len;
ll ans;
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;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read(),tmp[i]=a[i];
	sort(tmp+1,tmp+1+n);
	len=unique(tmp+1,tmp+1+n)-(tmp+1);
	for(int i=1;i<=n;i++) 
        a[i]=lower_bound(tmp+1,tmp+len+1,a[i])-(tmp);
	for(int i=1;i<=n;i++)
	{
		add(a[i],1);
		ans+=i-query(a[i]);
	}
	printf("%lld",ans);
	return 0;
} 

实际上,逆序对求解部分还有另一种写法如下:

for(int i=n;i>=1;i--)
	{
		ans+=query(a[i]-1);
		add(a[i],1);
	}

\(n\)\(1\) 倒着枚举,

如果 \(t[x']\) 不为空,那么它一定是在之前进入的树状数组,因为是倒序相当于更靠后,那么小且靠后就是一个逆序对。

7.递增

传送门

思路:

正常的 dp 比较好想,设 \(f[i]\) 表示截止到第 \(i\) 个位置,最长上升子序列的长度,

\(O(n^2)\) 的做法就是对于每一个位置 \(i\) ,枚举其前面的位置 \(j\) ,若 \(a[j]<a[i]\) ,那么 \(f[i]=\max(f[i],f[j]+1)\) ,但是对于这道题的数据范围肯定过不了。

虽然树状数组维护区间最大值是 \(O(\log^2n)\) 的,但是维护前缀最大值是 \(O(\log n)\) 的,

所以我们对数据离散化以后,直接树状数组维护前缀最大值即可,

简单说说 \(f[i]\) 更新的过程,离散化后 \(a[i]\) 中存储的是在原数组中的排名

所以 \(f[i]=query(a[i]-1)+1\) ,因为查询的是比 \(a[i]\) 排名小的并且先进入树状数组的也就是说, \(a[i]\) 可以与他们构成最大上升子序列,而 \(query\) 返回的又是最大值,所以直接 \(+1\) 即可, 然后再 \(add(a[i],f[a[i]])\) 即可,表示 \(a[i]\) 也已经进入树状数组。

最后答案就是 \(n-f[n]\) 。时间复杂度 \(O(n\log n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,a[N];
int len,tmp[N];
int f[N],t[N],ans;
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;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]=max(t[x],k);
		x+=lowbit(x);
	}
}
inline int query(int x)
{
	int maxx=0;
	while(x>0)
	{
		maxx=max(maxx,t[x]);
		x-=lowbit(x);
	}
	return maxx;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read(),tmp[i]=a[i];
	sort(tmp+1,tmp+1+n);
	len=unique(tmp+1,tmp+1+n)-tmp-1;
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(tmp+1,tmp+1+len,a[i])-tmp;
	for(int i=1;i<=n;i++)
	{
		f[a[i]]=query(a[i]-1)+1;
		add(a[i],f[a[i]]);
	}
	ans=n-query(n);
	cout<<ans<<endl;
	return 0;
}

8.火柴排队

传送门

思路:

首先还是发现,题目中我们只需要相对高度,所以还是先对两列火柴离散化,

然后对两列火柴分别操作和只对一列火柴操作是等价的,

所以我们不妨把第一列火柴看作是“模板”

因为每次只能交换相邻的火柴,

所以只需要求第二列火柴在第一列火柴意义下的逆序数(自己起的名字)即可。

下面我们以样例 \(2\) 为例,

第一列火柴为: \(、、、1、3、4、2\) ,离散化后数值不变

第二列火柴为: \(、、、1、7、2、4\) ,离散化后变为 \(、、、1、4、2、3\)

在第一列火柴意义下的逆序数就是把第一列火柴看为 \(、、、1、2、3、4\)

那么第二列火柴就被看为 \(、、、1、3、4、2\) ,此时第二列火柴中的逆序对个数为 \(2\) ,所以操作数为 \(2\)

所以如果第一列火柴用 \(a[i]\) 表示,那么我们可以离散化后 \(id[a[i]]=i\) ,然后求逆序对的时候查询 \(id[b[i]]\) ,至此本题结束。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 100050
#define MOD 99999997
typedef long long ll;
int n,a[N],b[N];
int len1,tmp1[N],len2,tmp2[N];
int q[N],t[N];
ll ans;
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;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline ll query(int x)
{
	ll sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read(),tmp1[i]=a[i];
	for(int i=1;i<=n;i++) b[i]=read(),tmp2[i]=b[i];
	sort(tmp1+1,tmp1+1+n);sort(tmp2+1,tmp2+1+n);
	len1=unique(tmp1+1,tmp1+1+n)-tmp1-1;
	len2=unique(tmp2+1,tmp2+1+n)-tmp2-1;
	for(int i=1;i<=n;i++)
	{
		a[i]=lower_bound(tmp1+1,tmp1+1+len1,a[i])-tmp1;
		b[i]=lower_bound(tmp2+1,tmp2+1+len2,b[i])-tmp2;
	}
	for(int i=1;i<=n;i++) q[a[i]]=i;
	for(int i=n;i>=1;i--)
	{
		ans=(ans%MOD+query(q[b[i]]-1)%MOD)%MOD;
		add(q[b[i]],1);
	}
	printf("%lld\n",ans);
	return 0;
}

9.Promotion Counting

传送门

思路:

题意简化为就树上的逆序对,还是考虑使用树状数组,

我们可以利用一个 dfs 来求解,从根开始遍历,一直到叶子节点,经过查询、更新等操作一直往父节点上传,直到求出最后的答案。

在这种情况下 \(\operatorname{query}(i)\) 就是查询比 \(i\) 小还要靠前进入树状数组的,先不考虑树的结构,体现在正常数组里就是所有比 \(i\) 弱的,所以 \(\operatorname{query}(n)-\operatorname{query}(i)\) 就是比 \(n\) 弱的减去比 \(i\) 弱的,也就是所有比 \(i\) 强的。

所以每一次 dfs 我们先把当前节点的答案初始值设为 \(ans1=-(\operatorname{query}(n)-\operatorname{query}(i))=\operatorname{query}(i)-\operatorname{query}(n)\) ,表示初始时有多少是比 \(i\) 强的,此时我们还没有考虑树上的结构,这个时候递归到当前节点的儿子节点,一直到叶子节点,我们开始加入到树状数组中,然后返回的时候统计当前节点加入儿子以后有多少比 \(i\) 强的,记为 \(ans2\) ,两者相减就是考虑了树形结构后,只有 \(i\) 的下属中比 \(i\) 强的个数 \(ans2-ans1\) ,统计完后再将当前节点加入树状数组中

代码:

#include<bits/stdc++.h>
#define N 200050
using namespace std;
typedef long long ll;
int n,p[N];
int len,tmp[N],t[N],ans[N];
int head[N],Next[N],ver[N],tot=-1;
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;
}
inline void add(int x,int y)
{
	ver[++tot]=y;
	Next[tot]=head[x];
	head[x]=tot;
}
inline int lowbit(int x) { return x&-x; }
inline void update(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
void dfs(int x,int fa)
{
	ans[x]=-(query(n)-query(p[x])); //初始化一开始有多少比 x 强的
	for(int i=head[x];~i;i=Next[i]) //如果叶子节点就不会进 for 循环
	{
		int y=ver[i];
		if(y==fa) continue;
		dfs(y,x); //添加下属会一直递归到叶子节点
	}
	ans[x]+=(query(n)-query(p[x])); //叶子节点或递归回来的父亲节点会执行
    //递归回来的父亲节点统计一下加了下属以后比 x 强的有多少 然后累加即可
	update(p[x],1); //叶子节点或父亲节点统计完以后添加到树状数组中
}
int main()
{
	memset(head,-1,sizeof(head));
	n=read();
	for(int i=1;i<=n;i++) p[i]=read(),tmp[i]=p[i];
	sort(tmp+1,tmp+1+n);
	len=unique(tmp+1,tmp+1+n)-tmp-1;
	for(int i=1;i<=n;i++) p[i]=lower_bound(tmp+1,tmp+1+len,p[i])-tmp;
	for(int i=2;i<=n;i++)
	{
		int fa=read();
		add(fa,i);add(i,fa);
	}
	dfs(1,-1);
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
	return 0;
}

10.上帝造题的七分钟 2 / 花神游历各国

传送门

思路:

本题的难点在于如何处理开平方根的操作,

没有数据结构可以维护根号操作,线段树的懒标记也不好操作,

所以我么就考虑最简单的暴力,但是要加点小技巧,

我们发现 \(10^{12}\) 的数开根 \(6\) 次左右就已经到 \(1\) 左右了,所以实际上每个数会被开方的次数很少,

于是我们可以加一个并查集维护, \(fa[i]\) 指向 \(i\) 后面第一个不为 \(1\) 的数的位置,初始值全为 \(i\)

所以修改的时候只需要修改 \(f[i]=i\) 的节点即可

代码:

#include<bits/stdc++.h>
#define N 100050
using namespace std;
typedef long long ll;
ll n,m,a[N];
ll k,l,r;
ll t[N],fa[N];
inline ll read()
{
	ll 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 Find(int x)
{
	if(x==fa[x]) return x;
	return fa[x]=Find(fa[x]);
}
inline ll lowbit(ll x) { return x&-x; }
inline void add(ll x,ll k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline ll query(ll x)
{
	ll sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
int main()
{
	n=read();
	for(ll i=1;i<=n;i++) 
	{
		fa[i]=i;
		a[i]=read();
		add(i,a[i]);
	}
	fa[n+1]=n+1;
	m=read();
	while(m--)
	{
		k=read();l=read();r=read();
		if(l>r) l^=r^=l^=r;
		if(k) printf("%lld\n",query(r)-query(l-1));
		else
		{
			for(ll i=l;i<=r;)
			{
				ll tmp=(ll)sqrt(a[i]);
				add(i,tmp-a[i]);
				a[i]=tmp;
				if(a[i]<=1) { fa[i]=i+1;i=Find(fa[i]); }
                //先令 fa[i]=i+1 这个时候并不知道 i+1 的具体情况
                //然后 i 变为第一个不是 1 的数还能顺便更新 fa[i]
				else { fa[i]=i;i++; } //正常 i++ 看下一位
			}
		}
	}
	return 0; 
}

11.HH的项链

传送门

思路:

这道题的树状数组跟前面几道又有点不太一样,首先我们先考虑权值树状数组,但是权值和序列是两个维度(仔细体会一下?)

换句话说,我们没法维护区间权值,这也就是为什么我们没法用树状数组维护区间众数,

那我们换个思路,我们发现对于一次右端点为 \(r\) 的查询,某个贝壳对答案的贡献一定是更靠近右端点的那个,离右端点远的那个同类贝壳是没有意义的,

所以我们可以维护某个位置上是否有数字,当一个贝壳重复出现的时候,把它原来的位置改成没有数字,把当前出现的位置改为有数字,

举个例子现在有 \(3\) 个贝壳: \(、、1、2、1\)

先查询 \([1,2]\) ,那么第一个位置和第二个位置上都有数字,所以现在数组状态为 \(、、1、1、0\) ,此时答案就是 \(\operatorname{query}(2)-\operatorname{query}(0)=2\)

再查询 \([1,3]\) ,此时第三个位置又出现了 \(1\) ,所以我们把上一次 \(1\) 出现的位置(第 \(1\) 位)先改为 \(0\) ,然后再把第三位改为 \(1\) ,所以此时数组状态为 \(、、0、1、1\) ,答案就是 \(\operatorname{query}(3)-\operatorname{query}(0)=2\)

所以我们的思路是离线所有查询,将其按照右端点排序,用 \(vis\) 数组记录某一类贝壳上次出现的位置,然后用变量 \(pos\) 表示当前处理到哪一位数字了,也就是上一个查询的右端点 \(+1\)

具体细节看代码

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 1000050
int n,m,a[N];
struct node{ int l,r,id; }q[N];
int pos,ans[N],vis[N],t[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;
}
inline bool cmp(node a,node b) { return a.r<b.r; }
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline int query(int x)
{
	int sum=0;
	while(x>0)
	{
		sum+=t[x];
		x-=lowbit(x);
	}
	return sum;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	m=read();
	for(int i=1;i<=m;i++)
	{
		q[i].l=read();
		q[i].r=read();
		q[i].id=i;//记录每个查询的编号,因为得按题目顺序输出
	}
	sort(q+1,q+1+m,cmp);
	pos=1;
	for(int i=1;i<=m;i++)
	{
		for(int j=pos;j<=q[i].r;j++)//从上一次处理的位置开始
		{
			if(vis[a[j]]) add(vis[a[j]],-1);//出现过就先把之前的删去
			vis[a[j]]=j;//记录本次出现的位置
			add(vis[a[j]],1);//把该位置改为 1
		}
		pos=q[i].r+1;
		ans[q[i].id]=query(q[i].r)-query(q[i].l-1);//前缀和查询答案
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

12.上帝造题的七分钟

传送门

思路:还没做完后面补上

代码:

posted @ 2024-12-13 17:30  A&K.SKULL  阅读(9)  评论(0编辑  收藏  举报