树状数组实现的名次树
这道题所用到的数据结构应该是名次树,名次树可以由线段树实现也可以由树状数组实现。而对于类似这道题这样的一个只需要删除和查询操作的名次树而言,可以用树状数组实现,因为用树状数组实现无论效率还是代码复杂度都较线段树而言更优,虽然算法较线段树而言更加巧妙。
首先,这个数据结构需要一个用于查询第k名是什么的函数select以及一个用于删除第k名的函数del。
先假设我们这两个函数已经拥有了,理一下主要的解题思路。
读入n和m的值(这句是废话)
然后我们第一个想到的应该是记录我们这一次所删除的名次,然后对于剩下的进行二分查找,但是这样的复杂度是nlog2n的,虽然对于这样的弱数据还是能过的,但是并不是最好的算法。
我们可以如下优化:记录这次我们删除的名次之前有几个人。因为通过记录删除的之前有几个人,我们就可以知道当下应当删除的名次。
本来应该是在这一次删除的名次之后再找第m个人,但是这样有可能之后的人数小于m,那样就要绕到头开始。所以我们可以做如下变换:
before=(before+m) % 剩余总人数。 注:%为c语言中取余运算符,等价于pascal中的mod。
之后我们调用select函数找到我们的指针所指向的名次所储存的东西并将返回值保存起来。
输出这个东西。
然后调用del函数删除这个名次上所储存的内容。
理清了这样一个思路以后,我们开始考虑这两个函数以及如何用树状数组实现。
树状数组的初始值应该是对于每一个t[i]=i&-i,这样的话我们就可以通过倍增法去查找对应的名次中所储存的内容。
i&-i是树状数组中求二进制最低位的一种技术,这里不过多赘述。
而这样的话,对于del函数其实可以用树状数组最基本的add函数所替代,因为del(k)要做的事就是add(k,-1);
add函数原型:
void add(int k,int d)。这里k∈[1,n]。
关于select函数,原型可定义为
int select(int k);
在这里,k∈[0,n)。为什么这么定义呢?其实是为了方便before直接使用和便于查找。
int select(int k)
{
int c=0;这里c就是返回的那个值
int q=14;
int i=0;
for(int g=1<<q;q>=0;g=1<<(--q))倍增法查找
{
i=g+c;
if(i<=n&&t[i]<=k)
{
c=i;
k=k-t[i];
}
}
return c+1;因为区间开闭的原因,所以应该返回的是c+1。
}
完整的程序:
#include<cstdio>
const int max=30002;
int t[max],n,m,before,p;
int select(int k)
{
int c=0;
int q=14;
int i=0;
for(int g=1<<q;q>=0;g=1<<(--q))
{
i=g+c;
if(i<=n&&t[i]<=k)
{
c=i;
k=k-t[i];
}
}
return c+1;
}
void add(int k,int d)
{
while(k<=n)
{
t[k]+=d;
k+=k&-k;
}
}
int main()
{
scanf("%d%d",&n,&m);
m--;
for(int i=1;i<=n;i++)
{
t[i]=i&-i;
}
for(int a=n;a>=1;a--)
{
before=(before+m)%a;
p=select(before);
add(p,-1);
printf("%d ",p);
}
return 0;
}