众所周知,区间 kth 有很多种求法。
本文中的时间复杂度和分数均以实现 P3834 为准。
为了更好地贴合现实,本文代码将更加符合学此算法时的实际情况。
一、排序
通过选择 / 冒泡 / 插入排序,将区间排序后输出 k 小值。
时间复杂度 \(O(mn^2)\)
用时:7.81s
#include<cstdio>
using namespace std;
int n,m,l,r,k;
int a[200007],s[200007];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&l,&r,&k);
for(int i=l;i<=r;i++)
s[i-l+1]=a[i];
for(int i=r-l+1;i>0;i--)
for(int j=1;j<i;j++)
if(s[j]>s[j+1])
{int t=s[j];s[j]=s[j+1];s[j+1]=t;}
printf("%d\n",s[k]);
}
return 0;
}
二、sort
作为 C 党,有必要了解 STL。
通过 sort 将排序的时间复杂度降下来(当然也可以自己写)
时间复杂度 \(O(mn\log n)\)
用时:6.49s
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,l,r,k;
int a[200007],s[200007];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&l,&r,&k);
for(int i=l;i<=r;i++)
s[i-l+1]=a[i];
sort(s+1,s+r-l+2);
printf("%d\n",s[k]);
}
return 0;
}
三、分治
sort 的思想是二分,但我们要找的 kth 只可能出现在二分中的其中一个部分,那么就没有必要对两个部分都递归,而可以只对其中一部分递归。
时间复杂度 \(O(m(\dfrac n2+\dfrac n4+\dfrac n8+\cdots))=O(mn)\)
用时:6.44s
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,l,r,k;
int a[200007],s[200007];
void _sort(int l,int r,int k)
{
int i=l,j=r,v=s[l];
while(i<=j)
{
while(s[i]<v)i++;
while(s[j]>v)j--;
if(i<=j)swap(s[i],s[j]),i++,j--;
}
if(k<=j)_sort(l,j,k);
else if(i<=k)_sort(i,r,k);
else return;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&l,&r,&k);
for(int i=l;i<=r;i++)
s[i-l+1]=a[i];
_sort(1,r-l+1,k);
printf("%d\n",s[k]);
}
return 0;
}
四、nth_element
为了避免重复造轮子,STL 中已经收录了 \(O(n)\) 求 k 小值的函数 nth_element
(虽然还是重复造轮子了)。
时间复杂度 \(O(mn)\)
用时:6.43s
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,l,r,k;
int a[200007],s[200007];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&l,&r,&k);
for(int i=l;i<=r;i++)
s[i-l+1]=a[i];
nth_element(s+1,s+k,s+r-l+2);
printf("%d\n",s[k]);
}
return 0;
}
五、值域映射
一种思想叫做从值域考虑,把区间里的数在值域上进行一个映射,然后倒着扫,扫到一个数就 \(k\leftarrow k-1\),\(k=0\) 的时候输出。
时间复杂度 \(O(mn)\)
用时:6.42s
#include<cstdio>
#include<algorithm>
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=2e5+7;
int n,m;
int a[inf],bok[inf],T[inf];
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
bok[i]=a[i]=re();
sort(bok+1,bok+n+1);
int num=unique(bok+1,bok+n+1)-bok-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(bok+1,bok+num+1,a[i])-bok;
for(int i=1;i<=m;i++)
{
int l=re(),r=re(),k=re();
for(int j=l;j<=r;j++)
T[a[j]]++;
for(int j=1;j<=num;j++)
{
k-=T[j];
if(k<=0)
{
wr(bok[j]),putchar('\n');
break;
}
}
for(int j=l;j<=r;j++)
T[a[j]]--;
}
return 0;
}
六、归并树
用线段树套 vector,在 \(O(n\log n)\) 的时间里建立归并树。
查询时二分答案,每次二分一个 mid,求区间内有多少数小于 mid,即在线段树的对应节点里的 vector 中再二分。
时间复杂度 \(O(m\log^3n)\)
用时:5.84s
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
inline int re()
{
int s=0;char ch=getchar();
while(ch>'9'||ch<'0')ch=getchar();
while(ch>='0'&&ch<='9')
s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
return s;
}
inline void wr(int s)
{
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=2e5+7;
int n,m,num,a[inf],bok[inf];
struct Seg_Tree{
int le,ri;
vector<int>h;
}T[inf<<2];
inline void build(int i,int l,int r)
{
T[i].le=l,T[i].ri=r;
if(l==r)
{
T[i].h.push_back(a[l]);
return;
}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
vector<int>::iterator lc=T[i<<1].h.begin(),rc=T[i<<1|1].h.begin();
while(lc!=T[i<<1].h.end()&&rc!=T[i<<1|1].h.end())
{
if(*lc<*rc)T[i].h.push_back(*lc),lc++;
else T[i].h.push_back(*rc),rc++;
}
while(lc!=T[i<<1].h.end())
T[i].h.push_back(*lc),lc++;
while(rc!=T[i<<1|1].h.end())
T[i].h.push_back(*rc),rc++;
}
inline int judge(int i,int l,int r,int k)
{
if(l<=T[i].le&&T[i].ri<=r)
return lower_bound(T[i].h.begin(),T[i].h.end(),k)-T[i].h.begin();
int mid=(T[i].le+T[i].ri)>>1,ans=0;
if(l<=mid)ans+=judge(i<<1,l,r,k);
if(mid<r)ans+=judge(i<<1|1,l,r,k);
return ans;
}
inline int ask(int x,int y,int k)
{
int l=1,r=num;
while(l<r)
{
int mid=(l+r+1)>>1;
int ls=judge(1,x,y,mid);
if(ls<k)l=mid;
else if(ls>=k)r=mid-1;
}
return bok[l];
}
int main()
{
n=re();m=re();
for(register int i=1;i<=n;i++)
bok[i]=a[i]=re();
sort(bok+1,bok+n+1);
num=unique(bok+1,bok+n+1)-bok-1;
for(register int i=1;i<=n;i++)
a[i]=lower_bound(bok+1,bok+num+1,a[i])-bok;
build(1,1,n);
for(register int i=1;i<=m;i++)
{
int l=re(),r=re(),k=re();
wr(ask(l,r,k)),putchar('\n');
}
return 0;
}
七、主席树
发明者的原话是:“对于原序列的每一个前缀 \([1\sim i]\) 建出一棵线段树维护值域上每个数出现的次数,则其树是可减的。”
其实就是前缀和。
权值线段树储存的是值域上数的个数。对于版本 R 的权值线段树减去版本 L-1 的权值线段树,就得到了维护 [L,R] 的权值线段树。
用时:537ms
#include<cstdio>
#include<algorithm>
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=2e5+7;
int n,m,num,a[inf],bok[inf];
struct Seg_Tree{
int lson,rson;
int sum;
#define lc(i) T[i].lson
#define rc(i) T[i].rson
}T[inf<<5];
int rot[inf],cnt;
void insert(int j,int &i,int l,int r,int k)
{
i=++cnt;T[i]=T[j];
if(l==r){T[i].sum++;return;}
int mid=(l+r)>>1;
if(k<=mid)insert(lc(j),lc(i),l,mid,k);
else insert(rc(j),rc(i),mid+1,r,k);
T[i].sum=T[lc(i)].sum+T[rc(i)].sum;
}
int ask(int i,int j,int l,int r,int k)
{
if(l==r)return l;
int mid=(l+r)>>1,sum=T[lc(j)].sum-T[lc(i)].sum;
if(k<=sum)return ask(lc(i),lc(j),l,mid,k);
return ask(rc(i),rc(j),mid+1,r,k-sum);
}
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
bok[i]=a[i]=re();
sort(bok+1,bok+n+1);
num=unique(bok+1,bok+n+1)-bok-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(bok+1,bok+num+1,a[i])-bok;
for(int i=1;i<=n;i++)
insert(rot[i-1],rot[i],1,num,a[i]);
for(int i=1;i<=m;i++)
{
int l=re(),r=re(),k=re();
wr(bok[ask(rot[l-1],rot[r],1,num,k)]),putchar('\n');
}
return 0;
}
八、线段树套平衡树
九、分块
十、整体二分
十一、莫队
十二、值域分块
十三、划分树
维护一个节点里面有多少数被划分到左边,有多少数被划分到右边,然后在线段树上查询。
由于稳定排序,在父节点的相对位置与在子节点中的相对位置相同。
时间复杂度 \(O(n\log n)\)
用时:419ms
#include<cstdio>
#include<algorithm>
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=2e5+7;
int n,m,bok[inf];
struct Seg_Tree{
int a[inf],sum[inf];
}T[20];
void build(int dep,int l,int r)
{
if(l==r)return;
int mid=(l+r)>>1,k=bok[mid];
int sum=0,cnt=mid-l+1;
for(int i=l;i<=r;i++)
if(T[dep].a[i]<k)cnt--;
int il=l-1,ir=mid;
for(int i=l;i<=r;i++)
{
int now=T[dep].a[i];
if(now<k)sum++,T[dep+1].a[++il]=now;
else if(now==k&&cnt)
{
sum++,cnt--;
T[dep+1].a[++il]=now;
}
else T[dep+1].a[++ir]=now;
T[dep].sum[i]=sum;
}
build(dep+1,l,mid);
build(dep+1,mid+1,r);
}
int ask(int dep,int l,int r,int x,int y,int k)
{
if(l==r)return T[dep].a[l];
int mid=(l+r)>>1;
int s1=0,s2=T[dep].sum[y];
if(x^l)s1=T[dep].sum[x-1];
if(k<=s2-s1)return ask(dep+1,l,mid,l+s1,l+s2-1,k);
return ask(dep+1,mid+1,r,x-s1+mid-l+1,y-s2+mid-l+1,k-s2+s1);
}
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
T[1].a[i]=bok[i]=re();
sort(bok+1,bok+n+1);
build(1,1,n);
for(int i=1;i<=m;i++)
{
int l=re(),r=re(),k=re();
wr(ask(1,1,n,l,r,k)),putchar('\n');
}
return 0;
}