【洛谷4364】[九省联考2018] IIIDX(线段树)
- 给定一个长度为\(n\)的序列以及一个实数参数\(k\)。
- 要求将序列重新排序,满足\(\forall i\ge k,a_i\ge a_{\lfloor\frac ik\rfloor}\),且字典序最大。
- \(n\le5\times10^5\)
一个简单但错误的贪心
先把题意转化,我们可以建出一棵树形结构,那么就是要让每个点的权值小于等于子树内所有点的权值。
然后就很容易想到一个贪心,对于每个点\(x\),假设其子树大小为\(Sz_x\),就为它子树内的点预留\(Sz_x-1\)个较大值,将第\(Sz_x\)大的数作为当前点的权值。
的确,在没有相同值的时候,这个做法的正确性是显然的,而良心出题人也给了这种贪心\(55\)分。
但是啊,有了相同值就不一定了。
随便给个例子,当\(n=k=3\),四个数分别为\(2,1,1\)的时候,显然\(1\)号点只能填\(1\),而按照前面的贪心更大的\(2\)会填给\(3\)号点,但实际上填给\(2\)号点更优且依然能满足条件。
因此这个做法需要修正。
线段树上二分
依旧考虑前面的例子,发现贪心能够保证我们选择当前点的正确性,但可能影响到和当前点同一深度的其他点的选择。
为此,首先我们依旧求出第\(Sz_x\)大的数,\(x\)的答案的确是这个值,但我们并不一定要选择第\(Sz_x\)个位置上的这个值,而应该选择尽可能靠后的这个值来使答案更优(具体实现中可以直接记录每种值最右边的位置\(R_v\))。
这样一来尽管还是贪心,就不像先前那样简单了,而是要利用线段树上二分来求解。
具体地,线段树上维护每个位置的排名,一次询问就是要找出线段树上排名大于等于\(Sz_x\)的最靠左的数,也就是排名小于\(Sz_x\)的最靠右的数所在位置加\(1\)。
找到一个位置后需要在前面预留出\(Sz_x-1\)个位置,这直接在线段树上先区间减法修改,等到处理到子树内之后再区间加法还原回去即可。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 500000
using namespace std;
int n,a[N+5],dc,dv[N+5],R[N+5],f[N+5],sz[N+5],ans[N+5];double k;
I bool cmp(CI x,CI y) {return x>y;}
class SegmentTree
{
private:
#define PT CI l=0,CI r=n,CI rt=1
#define LT l,mid,rt<<1
#define RT mid+1,r,rt<<1|1
#define PU(x) (V[x]=min(V[x<<1],V[x<<1|1]))
#define PD(x) (F[x]&&(T(x<<1,F[x]),T(x<<1|1,F[x]),F[x]=0))
#define T(x,v) (V[x]+=v,F[x]+=v)
int V[N<<2],F[N<<2];
public:
I void Build(PT)//建树
{
if(l==r) return (void)(V[rt]=l);RI mid=l+r>>1;Build(LT),Build(RT),PU(rt);
}
I int Q(CI x,PT)//线段树上二分大于等于x的最小位置,先找到小于等于x的最大位置再加1
{
if(l==r) return l+1;RI mid=l+r>>1;PD(rt);return V[rt<<1|1]<x?Q(x,RT):Q(x,LT);
}
I void U(CI L,CI R,CI v,PT)//区间修改
{
if(L<=l&&r<=R) return (void)T(rt,v);RI mid=l+r>>1;PD(rt);
L<=mid&&(U(L,R,v,LT),0),R>mid&&(U(L,R,v,RT),0),PU(rt);
}
}S;
int main()
{
RI i,x;for(scanf("%d%lf",&n,&k),i=1;i<=n;++i) scanf("%d",a+i);
for(i=n;i;--i) sz[f[i]=i/k]+=++sz[i];sort(a+1,a+n+1,cmp),S.Build();//求出每个点子树大小;权值从大到小排序后建线段树
for(i=1;i<=n;++i) (!dc||dv[dc]^a[i])&&(dv[++dc]=a[i]),R[a[i]=dc]=i;//离散化,记录每个值最右边的位置
for(i=1;i<=n;++i) f[i]^f[i-1]&&(S.U(ans[f[i]],n,sz[f[i]]-1),0),//把预留位置还原回去
printf("%d ",dv[x=a[S.Q(sz[i])]]),S.U(ans[i]=R[x]--,n,-sz[i]);return 0;//相同值尽可能靠后选择,給子树预留
}
待到再迷茫时回头望,所有脚印会发出光芒