[2020HDU多校第二场][HDU 6770][H. Dynamic Convex Hull]
赛后3min 1A...自闭_(:з」∠)_
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6770
题目大意:维护一个由函数\(f_i(x)=(x-a_i)^4+b_i\)组成的集合,要求实现插入、删除、以及查询\(x\):求\(f_i(x)\)的最小值
题解:观察题中给出函数的性质,考虑两个函数\(f_i\)与\(f_j\)在何时会出现大小关系变换的情况。根据初中数学知识,\(f_i(x)\)是由函数\(f(x)=x^4\)平移得到的(左加右减,上加下减),因此若令\(g(x)=f_i(x)-f_j(x)\),很明显能将\(g(x)\)转换成\((x-a)^4+b-x^4\)的形式(考虑将\(f_i\)与\(f_j\)在坐标轴上的图像同时平移,使\(f_j\)与\(f(x)=x^4\)的图像重合),将其展开可以得出\(g(x)=-4ax^3+6a^2x^2-4a^3x+a^4+b\),在\(a \neq 0\)(即\(a_i \neq a_j\))时是一个三次函数。对这个三次函数进行求导可以发现\(g'(x)=-12ax^2+12a^2x-4a^3=-4a(3x^2-3ax+a^2)\),而\(3x^2-3ax+a^2\)这个二次函数的判别式算出来的结果是\(-3a^2<0\),因此\(3x^2-3ax+a^2\)恒大于零,故\(g'(x)<0\)恒成立,\(g(x)\)单调下降。于是对于任意\(a_i\)不同的两个函数,他们的大小只会交换一次。另外我们还发现,每次求的是\(f_i(x)\)的最小值,而在\(f_i\)的展开式中,\(x^4\)对函数间的大小毫无影响,所以可以“假装”将题目转换成求\(g_i(x)=-4a_ix^3+6a_i^2x^2-4a_i^3x+a_i^4+b_i\)的最小值(这里指的是用\(g_i\)的性质比大小,求\(f_i\)的值)。而这个函数我们之前验证过了是严格单调下降的,所以题目就变成了求若干个单调下降、且两两只有一个交点的函数的最小值,性质与求\(ax+b\)最小值十分相像。
另一方面,联想到CF 678F的做法,考虑每一个函数存在的时间区间,以时间为下标建立线段树,并在树上的每个结点记录下对应区间内的函数,对问题进行离线求解。这样在建好树之后,就可以在树上的每个结点内进行维护当前区间内存在的所有函数,由于线段树的性质,每个函数只会存在于\(O(log n)\)个结点内,因此我们可以考虑如何在\(O(nlog n)\)的时间复杂度内对一个结点上的若干个函数进行求最小值的操作,在总复杂度为\(O(nlog^2n)\)的情况下解决问题。
由于我们之前发现求\(f_i\)最小值与求\(ax+b\)最小值的性质十分相像,于是考虑也用在每个结点上维护一个类似于下凸壳的东西对此题进行求解。对每个结点内的所有函数按\(a_i\)升序排序(在\(a\)较小时\(g_i\)的下降幅度相对缓慢),并维护一个栈,求出由当前结点内函数组成的“下凸壳”。并记录栈中每个函数与前一个函数“交点”的位置(第一次交换大小关系的整点位置),由于询问的\(x\)是整数,因此只需要在整数域内二分进行“求交”即可。最后求答案时,只需对每个结点求一个“下凸壳”,并对该结点对应时间区间中的所有询问操作进行分别求解。由于我们记录了栈中相邻函数“交点”的位置,只需要二分一下就可以求出在当前的“下凸壳”中哪个函数最小并得出对应答案。
对于每个结点,维护“下凸壳”的操作为\(O(nlog n)\),由于每个函数只会影响到\(O(log n)\)个结点,所以该部分复杂度为\(O(nlog^2n)\)。而对于询问操作,同样的有每次询问只会存在于\(O(nlog n)\)个结点内,因此对于每个询问也只需要花费\(O(log^2n)\)的时间,故询问操作的复杂度也为\(O(nlog^2n)\)。总时间复杂度为\(O(nlog^2n)\)。
#include<bits/stdc++.h> using namespace std; #define N 200005 #define LL long long LL T,n,m,cnt,r[N],a[N],b[N],c[N],o[N],q[N],ans[N],fk[N],id[N],C; struct Point { LL a,b; LL f(LL x){LL tmp=(x-a)*(x-a);return tmp*tmp+b;} bool operator <(const Point&t)const{return a==t.a?b<t.b:a<t.a;} LL isct(Point t){ //a>t.a LL l=1,r=50000,mid; if(f(0)<=t.f(0))return 0; if(f(50000)>t.f(50000))return 50001; while(l<r){ mid=(l+r)>>1; if(f(mid)>t.f(mid))l=mid+1; else r=mid; } return l; } }st[N]; struct Segment_Tree { struct rua{ LL l,r; vector<Point>d; }t[N<<2]; void Build(LL l,LL r,LL x){ t[x].d.clear(); t[x].l=l,t[x].r=r; if(l==r)return; LL mid=(l+r)>>1; Build(l,mid,x*2); Build(mid+1,r,x*2+1); } void change(LL L,LL R,Point p,LL x){ LL l=t[x].l,r=t[x].r; LL mid=(l+r)>>1; if(L<=l && r<=R){t[x].d.push_back(p);return;} if(L<=mid)change(L,R,p,x*2); if(mid<R)change(L,R,p,x*2+1); } void ask(LL i){ LL x=q[i]; LL k=upper_bound(fk+1,fk+cnt+1,x)-fk-1; //cout<<i<<" "<<x<<endl; //cout<<k<<" "<<st[k].a<<" "<<st[k].b<<endl; ans[i]=min(ans[i],st[k].f(x)); } void get(LL x){ if(t[x].l<t[x].r){ get(x*2); get(x*2+1); } cnt=0; sort(t[x].d.begin(),t[x].d.end()); for(auto p:t[x].d){ if(p.a==st[cnt].a)continue; while(cnt>1){ LL tmp=p.isct(st[cnt-1]); if(tmp>fk[cnt])break; if(tmp==fk[cnt] && p.f(fk[cnt])>st[cnt].f(fk[cnt]))break; cnt--; } st[++cnt]=p; if(cnt==1)fk[cnt]=0; else fk[cnt]=st[cnt].isct(st[cnt-1]); } for(LL i=t[x].l;i<=t[x].r;i++) if(o[i]==3 && cnt && c[i])ask(i); } }Tr; void init() { C=cnt=0; scanf("%lld%lld",&n,&m); Tr.Build(1,n+m,1); for(LL i=1;i<=n;i++){ o[i]=1; scanf("%lld%lld",&a[i],&b[i]); r[i]=n+m,cnt++; c[i]=cnt; id[++C]=i; } for(LL i=n+1;i<=n+m;i++){ scanf("%lld",&o[i]); if(o[i]==1){ scanf("%lld%lld",&a[i],&b[i]); r[i]=n+m,cnt++; id[++C]=i; } if(o[i]==2){ scanf("%lld",&q[i]); r[id[q[i]]]=i,cnt--; } if(o[i]==3){ scanf("%lld",&q[i]); ans[i]=9e18; } c[i]=cnt; } n+=m; for(LL i=1;i<=n;i++)if(o[i]==1)Tr.change(i,r[i],{a[i],b[i]},1); Tr.get(1); for(LL i=1;i<=n;i++) if(o[i]==3)printf("%lld\n",c[i]?ans[i]:-1); } int main() { scanf("%lld",&T); while(T--)init(); }
由于做此题时参考了我的另一篇博客[Educational Round 13][Codeforces 678F. Lena and Queries]的代码,因此一开始给出的\(n\)个函数我当成了\(n\)次插入来处理。由于我排序时同时对\(b_i\)进行了升序排序,因此在\(a_i\)与上一个函数相同时,当前函数一定不会成为最小值,可以跳过。另外在求“下凸壳”若遇到当前点与st[cnt-1]的"交点"和栈顶与st[cnt-1]的"交点"相同,需要额外比较一下在该点处两个函数的值以确定留下哪一个函数。