题解: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\),原式可以写成
考虑用 \(l\) 代替 \(l-1\),则 \(l, r\) 的限制变为了 \(L-1 \le l < r \le R\)。则上式可以写成
这个很像斜率的形式!你发现其实现在已经不需要二分答案了;记 \(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\) 数组。
\(mid < i < j\) 的情况也是类似的,只需要对于每个 \(k\),建出 \(i \isin (mid, k)\) 的 \(Q_i\) 构成的上凸壳,然后三分出过 \(P_k\) 的切线斜率即可。我们同样处理出一个 \(rans\) 数组。
接下来考虑一个询问 \([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)\)。
我的做法不需要任何任何火箭,喝彩!
#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;
}

浙公网安备 33010602011771号