题解:P11587 [KTSC 2022 R2] 编程测试
dyc2022
·
2026-09-14 16:56:47
·
题解
更好的阅读体验
凸凸的壳?!?!
首先答案是有单调性的。那么假设 我现在想要二分答案,思考如何判断一个答案 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$ 构成的上凸壳。那么斜率最小的连线,必然是这两个凸壳的公切线,如图。

现在思考假设我手里已经有这两个凸壳中的点的话要怎么做。我们可以二分斜率 $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$。这个过程可以用下图较清晰地呈现。

但是现在的问题在于,我们每次询问 $[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;
}
```