【洛谷4364】[九省联考2018] IIIDX(线段树)

点此看题面

  • 给定一个长度为(n)的序列以及一个实数参数(k)
  • 要求将序列重新排序,满足(forall ige k,a_ige a_{lfloorfrac ik floor}),且字典序最大。
  • (nle5 imes10^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;//相同值尽可能靠后选择,給子树预留
}
败得义无反顾,弱得一无是处
原文地址:https://www.cnblogs.com/chenxiaoran666/p/Luogu4364.html