题解:P11587 [KTSC 2022 R2] 编程测试

· · 题解

更好的阅读体验

凸凸的壳?!?!

首先答案是有单调性的。那么假设我现在想要二分答案,思考如何判断一个答案 ans 是否合法。

考虑把原问题转化为一个二分图匹配问题。左部点就是总共 (r - l + 1) \cdot ans 个我们希望凑出的问题,右部点就是我们可以拿出的问题。具体地,在位置 i,有 a_i 个问题可以贡献到难度为 i 的预期问题上;同时有 b_i 个问题可以分别贡献到难度为 i 和 i+1 的预期问题上。因此 a_i 个点每个点向总共 ans 个难度为 i 的预期问题连一条边,b_i 个点每个店向总共 2\cdot ans 个难度为 i, i+1 的预期问题连一条边。如图是一种连边方式的示例。

那么有解的条件就是对于左部点存在完美匹配。由 Hall 定理,有解的充要条件就是对于左部点的每个子集,其邻域大小都 \ge 这个子集的大小。进一步注意到,假设这个子集可以划分成若干个下标上的连续段,那么每个连续段的邻域是不交的。也就是说,我们只需要考虑这个子集表现为下标上连续的一段的情况。至此我们可以设计出一个多项式时间的判定算法。

假设现在询问的区间是 [L, R]。那么可以刻画出 ans 合法的条件,也就是对于 \forall [l, r] \subseteq [L, R],有 \sum \limits_{i = l} ^ r a_i + \sum \limits_{i = l-1}^r b_i \ge (r-l+1) \cdot ans。记 a, b 的前缀和数组分别是 sa, sb,原式可以写成

ans \le \frac{sa_r - sa_{l-1} + sb_r - sb_{l-2}}{r-l+1}

考虑用 l 代替 l-1,则 l, r 的限制变为了 L-1 \le l < r \le R。则上式可以写成

ans \le \frac{(sa_r + sb_r) - (sa_l + sb_{l-1})}{r-l}

这个很像斜率的形式!你发现其实现在已经不需要二分答案了;记 P_i(i, sa_i + sb_i), Q_i(i, sa_i + sb_{i - 1}),则答案就是取 L-1 \le i < j \le R,Q_i 和 P_j 连线斜率的最小值,再向下取整。

下文中所有 L 为原问题中的 L-1。则所求解的就是选取 L \le i < j \le R,Q_i 和 P_j 连线的最小斜率。

对于这个转化过后的问题,考虑进行猫树分治。现在假设分治区间为 [l, r],区间中点为 mid。则有三种可能产生贡献的 i, j:i, j 都位于 mid 左侧,都位于 mid 右侧,或者 i \le mid < j。

先考虑 i, j 都位于 mid 左侧的情况。则我们可以预处理出一个数字 lans_k,表示 [k, mid] 区间内 P_i 和 Q_j 连线斜率的最小值。那么我们只需要对于每个 k,求出对于所有 j \isin (k, mid],Q_k 和 P_j 的连线最小斜率。

那么显然这个 P_j 一定会在 j \isin (k, mid] 的点构成的下凸壳上,如图。从左往右维护这个下凸壳,每次加入一个 P_k,然后再凸壳上三分出过 Q_k 的切线即可。

对每个 Q_k 的结果取一个后缀 \min 就可以得到 lans 数组。

接下来考虑一个询问 $[L, R]$,我们还需要找到所有满足 $L \le i \le mid < j \le R$ 的 $Q_i$ 和 $P_j$ 连线斜率最小值。同理,建出 $j \isin (mid, R]$ 的 $P_j$ 构成的下凸壳,以及 $i \isin [L, mid]$ 的 $Q_i$ 构成的上凸壳。那么斜率最小的连线,必然是这两个凸壳的公切线,如图。 ![](https://cdn.luogu.com.cn/upload/image_hosting/6vniqegl.webp) 现在思考假设我手里已经有这两个凸壳中的点的话要怎么做。我们可以二分斜率 $k$,然后判断这个斜率是偏小还是偏大。 我们分别作两个凸壳斜率为 $k$ 的切线,并取它们在 $y$ 轴上的投影 $b_P, b_Q$。则 $b_P$ 的值就是 $P$ 构成的凸壳上,$y - k \cdot x$ 的最小值,$b_Q$ 的值就是 $Q$ 构成的凸壳上,$y - k \cdot x$ 的最大值。这两个值都能三分或者二分导数求出。 那么如果切线斜率过小,表现为 $b_P > b_Q$;过大表现为 $b_P < b_Q$。这个过程可以用下图较清晰地呈现。 ![](https://cdn.luogu.com.cn/upload/image_hosting/cuat59wq.webp) 但是现在的问题在于,我们每次询问 $[L, R]$ 要能快速且在线地得到 $[L, mid]$ 的凸壳以及 $(mid, R]$ 的凸壳中的点。这启发我们把建立凸包时的单调栈可持久化。 对栈的可持久化其实是最简单但是最容易被忽视的可持久化技术,因为一个栈的历史可以描述成一棵有根树。我们对空栈单独开一个节点作为树的根节点,同时维护目前栈顶所在的节点 $now$。当 push 一个元素的时候,我们给 $now$ 新加入一个儿子,然后将 $now$ 移动到新加入的儿子上;当 pop 一个元素的时候,只需要将 $now$ 移动至 $now$ 的父亲即可。 这样我们就可以访问任意一个时刻的单调栈的任意一个元素了。容易发现在任意时刻单调栈内元素的状态都可以用 $now$ 的到根链来表示。因此我们对左侧和右侧求凸壳的过程使用的单调栈分别可持久化,那么二分导数的过程可以用树上倍增来实现。 那么这道题就做完了!时间复杂度为 $O(n \log n + q \log n \log V)$。 我的做法不需要任何任何火箭,喝彩! ```cpp #include<bits/stdc++.h> #define endl '\n' #define N 100006 using namespace std; template <class T1,class T2> inline void chkmin(T1 &x,T2 y) {x=x<y?x:y;} template <class T1,class T2> inline void chkmax(T1 &x,T2 y) {x=x<y?y:x;} using i64=long long; using i128=__int128; int n,q,tp,pos_l[N],pos_r[N]; i64 sa[N],sb[N],ans[N],lans[N],rans[N]; struct Point {i64 x,y;} P[N],Q[N],stk[N]; struct Ask {int l,r,id;}; inline i64 K(Point x,Point y) {return (y.y-x.y)/(y.x-x.x);} struct Stack { int tot,now,fa[N][20]; Point pt[N]; void clear() { for(int i=0;i<=tot;i++) for(int j=0;j<20;j++)fa[i][j]=0; tot=now=1; } int push(Point x) { fa[++tot][0]=now,pt[tot]=x; for(int j=1;j<20;j++)fa[tot][j]=fa[fa[tot][j-1]][j-1]; return now=tot; } void pop() {now=fa[now][0];} i64 query_mx(int u,i64 k) { auto val=[](Point x,i64 k) -> i64 {return x.y-k*x.x;}; for(int i=19;~i;i--) { int up=fa[u][i],fa_up=fa[up][0]; if(up<=1||fa_up<=1)continue; if(val(pt[fa_up],k)>=val(pt[up],k))u=up; } i64 ret=val(pt[u],k); if(fa[u][0]>1)chkmax(ret,val(pt[fa[u][0]],k)); return ret; } i64 query_mn(int u,i64 k) { auto val=[](Point x,i64 k) -> i64 {return x.y-k*x.x;}; for(int i=19;~i;i--) { int up=fa[u][i],fa_up=fa[up][0]; if(up<=1||fa_up<=1)continue; if(val(pt[fa_up],k)<=val(pt[up],k))u=up; } i64 ret=val(pt[u],k); if(fa[u][0]>1)chkmin(ret,val(pt[fa[u][0]],k)); return ret; } } stk_l,stk_r; void solve(int l,int r,vector<Ask> ask) { if(!ask.size()||l>=r)return; int mid=l+r>>1; auto check1=[](Point x) -> bool { i64 nx=x.x,ny=x.y,sx1=stk[tp].x,sy1=stk[tp].y,sx2=stk[tp-1].x,sy2=stk[tp-1].y; i128 cross=((i128)sx1-sx2)*(ny-sy1)-((i128)nx-sx1)*(sy1-sy2); return cross>=0; }; auto check2=[](Point x) -> bool { i64 nx=x.x,ny=x.y,sx1=stk[tp].x,sy1=stk[tp].y,sx2=stk[tp-1].x,sy2=stk[tp-1].y; i128 cross=((i128)sx1-sx2)*(ny-sy1)-((i128)nx-sx1)*(sy1-sy2); return cross<=0; }; for(int i=l;i<=r;i++)lans[i]=rans[i]=2e18; tp=0; for(int i=mid+1;i<=r;i++) { rans[i]=rans[i-1]; int l=1,r=tp; while(r-l>=6) { int mid1=l+(r-l)/3,mid2=r-(r-l)/3; i128 t1=((i128)P[i].y-stk[mid1].y)*(P[i].x-stk[mid2].x); i128 t2=((i128)P[i].y-stk[mid2].y)*(P[i].x-stk[mid1].x); if(t1<t2)r=mid2; else l=mid1; } for(int j=l;j<=r;j++)chkmin(rans[i],K(P[i],stk[j])); while(tp>1&&check1(Q[i]))tp--; stk[++tp]=Q[i]; } tp=0; for(int i=mid;i>=l;i--) { lans[i]=lans[i+1]; int l=1,r=tp; while(r-l>=6) { int mid1=l+(r-l)/3,mid2=r-(r-l)/3; i128 t1=((i128)Q[i].y-stk[mid1].y)*(Q[i].x-stk[mid2].x); i128 t2=((i128)Q[i].y-stk[mid2].y)*(Q[i].x-stk[mid1].x); if(t1<t2)r=mid2; else l=mid1; } for(int j=l;j<=r;j++)chkmin(lans[i],K(Q[i],stk[j])); while(tp>1&&check1(P[i]))tp--; stk[++tp]=P[i]; } stk_r.clear(),tp=0; for(int i=mid+1;i<=r;i++) { while(tp>1&&check2(P[i]))tp--,stk_r.pop(); stk[++tp]=P[i],pos_r[i]=stk_r.push(P[i]); } stk_l.clear(),tp=0; for(int i=mid;i>=l;i--) { while(tp>1&&check2(Q[i]))tp--,stk_l.pop(); stk[++tp]=Q[i],pos_l[i]=stk_l.push(Q[i]); } vector<Ask> ask_l,ask_r; for(auto vv:ask) { int ql=vv.l,qr=vv.r,id=vv.id; if(qr<=mid)ask_l.push_back({ql,qr,id}); else if(ql>mid)ask_r.push_back({ql,qr,id}); else { i64 lb=0,rb=1e10,res=2e9; while(lb<=rb) { i64 mid=lb+rb>>1; i64 l_mx=stk_l.query_mx(pos_l[ql],mid),r_mn=stk_r.query_mn(pos_r[qr],mid); if(l_mx>=r_mn)res=mid,rb=mid-1; else lb=mid+1; } i64 l_mx=stk_l.query_mx(pos_l[ql],res),r_mn=stk_r.query_mn(pos_r[qr],res); if(l_mx>r_mn)res--; ans[id]=min({res,lans[ql],rans[qr]}); } } solve(l,mid,ask_l); solve(mid+1,r,ask_r); } vector<int> testset(vector<int> A,vector<int> B,vector<int> L,vector<int> U) { n=A.size(),q=L.size(); for(int i=1;i<=n;i++)sa[i]=sa[i-1]+A[i-1],sb[i]=sb[i-1]+B[i-1]; for(int i=1;i<=n;i++) Q[i]={i,sa[i]+sb[i-1]},P[i]={i,sa[i]+sb[i]}; vector<Ask> ask; for(int i=0;i<q;i++) ask.push_back({L[i],U[i]+1,i}),ans[i]=2e18; solve(0,n,ask); vector<int> ret; for(int i=0;i<q;i++)ret.push_back((int)ans[i]); return ret; } ```