1 条题解

  • 0
    @ 2026-9-3 22:38:38

    *IX. P5896 [IOI2016]aliens

    DP 优化方法大杂烩,详解 wqs 二分及其注意事项,斜率优化等其它 DP 优化方法。

    更好的阅读体验

    **** 团队赛 T6,没想到是 IOI 原题。当时看出来要用 wqs 二分,但是我没学过(经常发生知道要用某种算法但是不会写的情况),因此最近学习了一下 wqs 二分。找题目练手的时候看到了这题,世 界 线 收 束。做题的过程中发现还要用到斜率优化,于是也学了一下。

    这算是一道比较套路的题目,但套路并不意味这不是好题,毕竟我从中学到了 wqs 二分和斜率优化两种 DP 优化方法。说它套路其实也不太合适,因为 2016 年 wqs 二分还没有在 OI 界普及,甚至国外将其称之为 “Aliens’ Trick” 就是根据本题的名字而命名的,所以这个 idea 在当时应该比较新颖。


    不妨对网格图的主对角线所经过的格子从左上到右下依次编号为 0,1,2,,m10,1,2,\cdots,m-1。显然,编号为 ii 的格子在原网格图中的坐标为 (i,i)(i,i)。因为正方形的主对角线完全包含于网格图的主对角线,所以为了拍到一个点 (ri,ci)(r_i,c_i) 必然要拍到编号为 li,li+1,li+2,,ril_i,l_i+1,l_i+2,\cdots,r_i 的格子,其中 li=min(ri,ci),ri=max(ri,ci)l_i=\min(r_i,c_i),r_i=\max(r_i,c_i)。因此,我们可以将题目转化为线段覆盖问题:现在有 nn 条线段 [li,ri][l_i,r_i],我们要用最多 kk 个区间 [xi,yi][x_i,y_i] 去覆盖它们,使得每条线段至少被一个区间完全包含。即 i,j,s.t.[li,ri][xj,yj]\forall i,\exist j,s.t. [l_i,r_i]\in [x_j,y_j]。求以每个区间为对角线形成的所有正方形的面积并的最小值。接下来我们可以挖掘一些性质:

    • Observation 1.\textbf {Observation 1.} 如果两条线段 i,ji,j 满足 [li,ri][lj,rj][l_i,r_i]\in [l_j,r_j],那么线段 ii 对最终的答案没有影响。

      正确性显然。根据该性质,我们可以将所有线段按照左端点 lil_i 从小到大为第一关键字,右端点 rir_i 从大到小为第二关键字排序。然后依次遍历所有线段并记录右端点最大值 rr。若当前线段 ii 的右端点 rirr_i\leq r,则忽略线段 ii;否则用 rir_i 去更新 rr,即 rrir\gets r_i。最终所有没有被忽略的线段,一定满足左端点和右端点都单调递增。读者自证不难。

    • Observation 2.\mathbf{Observation\ 2.} 最终所选取的所有区间一定满足 xi<xi+1,yi<yi+1 (1i<k)x_i<x_{i+1},y_i<y_{i+1}\ (1\leq i<k)

      读者自证不难。

    • Observation 3.\mathbf{Observation\ 3.}FiF_i 为恰好选取 ii 个区间时答案的最小值,那么 FiF_i 是一个下凸函数。

      读者自证很难。感性理解一下即可,严格证明的话应该比较复杂。

    有了上述性质,我们可以愉快地进行 DP 了:wqs 二分斜率 KK,然后设 fif_i 表示覆盖第 ii 条线段时的最小代价(线段按照左端点从小到大排序。根据性质 11 的结论,右端点也行,因为不影响线段之间的顺序)。显然有转移方程:

    $$f_i=\min_{j=0}^{i-1}-K+\begin{cases}f_j+(y_i-x_{j+1}+1)^2-(y_j-x_{j+1}+1)^2&y_j\geq x_{j+1}\land j>0\\f_j+(y_i-x_{j+1}+1)^2&\mathrm{otherwise}\end{cases}$$

    这个分情况讨论很讨厌,尝试把它消灭掉:设 gi=(yixi+1+1)2[yixi+1i>0]g_i=(y_i-x_{i+1}+1)^2[y_i\geq x_{i+1}\land i>0],那么转移方程变为 fi=minj=0i1fj+(yixj+1+1)2gjKf_i=\min_{j=0}^{i-1}f_j+(y_i-x_{j+1}+1)^2-g_j-K。显然的斜率优化,但是我不会,所以多讲一点:拆开变成 $f_i=\min_{j=1}^{i-1} f_j+(y_i+1)^2-2(y_i+1)x_{j+1}+x^2_{j+1}-g_j-K$。先将所有 yiy_i 加上 11(不影响单调性),忽略掉 min\min 再移移项,写成 Y=kX+bY=kX+b 的形式,fj+xj+12gjK=2yixj+1+(fiyi2)f_j+x^2_{j+1}-g_j-K=2y_ix_{j+1}+(f_i-y_i^2)。那么 Y=fj+xj+12gjKY=f_j+x^2_{j+1}-g_j-Kk=2yik=2y_iX=xj+1X=x_{j+1}b=fiyi2b=f_i-y^2_i。将 (Xj,Yj)(X_j,Y_j) 看做一个点,因为 kk 确定了,现在就要找使得 bb 最小的 (Xj,Yj)(X_j,Y_j)(这样能使得 fif_i 最小)。由于 k,Xk,X 都是单调递增的,所以单调队列维护下凸包即可。

    具体地,记 ki,jk_{i,j} 为点 (Xi,Yi)(X_i,Y_i)(Xj,Yj)(X_j,Y_j) 之间的斜率。首先将点 00 加入单调队列,接下来遍历从小到大每个位置 ii

    • 若单调队列大小不小于 22,设 a,ba,b 分别为队首的第一个和第二个点,若 ka,bk=2yik_{a,b}\leq k=2y_ibb 一定不劣于 aa,弹出队首。重复该操作直到单调队列大小为 11ka,b>2yik_{a,b}>2y_i
    • 此时队首 aa 即为最优决策 jjfjfif_j\to f_i 转移并记录选择的区间个数。
    • 若单调队列大小不小于 22,设 c,dc,d 分别为队尾的倒数第二个和倒数第一个点;若 kc,dkd,ik_{c,d}\geq k_{d,i}dd 一定不在下凸包上,弹出队尾。重复该操作直到单调队列大小为 11kc,d<kd,ik_{c,d}<k_{d,i}
    • 将点 ii 加入单调队列。

    我们需要求出在代价最小的前提下最少的区间个数。不妨设 numinum_i 为覆盖前 ii 条线段且代价最小时最少的区间个数 ,那么因为 numinum_i 显然有单调性,所以我们每次转移时只需要选择最前面的一个决策点转移即可,即第一步操作弹出队首时,将条件 ka,b2yik_{a,b}\leq 2y_i 改为 ka,b<2yik_{a,b}<2y_i,这样就保证了相同优劣的决策点,前者不会被后者所弹出(第三步的条件不需要改,因为是前面的决策点弹出后面的决策点,不影响结果)

    时间复杂度 O(nlogV)\mathcal{O}(n\log V),其中 VV 是二分范围,本题中为 m2m^2。浮点数比较建议使用 epseps。代码倒是不难写。

    #include <bits/stdc++.h>
    using namespace std;
    
    #define ll long long
    #define ld long double
    
    const int N=1e5+5;
    const ld eps=1e-10;
    
    ll n,m,k,c,ans;
    struct seg{
    	int l,r;
    	bool operator < (const seg &v) const{
    		return l!=v.l?l<v.l:r>v.r;
    	}
    }p[N];
    
    ll K,f[N],g[N],num[N];
    ll sq(ll x){return x*x;}
    ll Y(int i){return f[i]+sq(p[i+1].l)-g[i]-K;}
    ll X(int i){return p[i+1].l;}
    ld slope(int i,int j){return (ld)(Y(j)-Y(i))/(X(j)-X(i));}
    
    ll hd,tl,d[N];
    bool check(){
    	d[hd=tl=1]=0;
    	for(int i=1;i<=n;i++){
    		while(hd<tl&&slope(d[hd],d[hd+1])+eps<=2*p[i].r)hd++;
    		int j=d[hd]; num[i]=num[j]+1,f[i]=f[j]+sq(p[i].r-p[j+1].l)-g[j]-K;
    		if(i<n)while(hd<tl&&slope(d[tl-1],d[tl])-eps>=slope(d[tl],i))tl--;
    		d[++tl]=i;
    	} return num[n]<=k;
    }
    
    int main(){
    	cin>>n>>m>>k;
    	for(int i=1,c,r;i<=n;i++){
    		cin>>p[i].l>>p[i].r;
    		if(p[i].l>p[i].r)swap(p[i].l,p[i].r);
    	} sort(p+1,p+n+1);
    	for(int i=1,r=-1;i<=n;i++)if(p[i].r>r)r=p[i].r,p[++c]=p[i]; n=c;
    	for(int i=1;i<n;i++)if(p[i].r>=p[i+1].l)g[i]=sq(p[i].r-p[i+1].l+1);
    	for(int i=1;i<=n;i++)p[i].r++;
    	ll l=-1e12,r=0;
    	while(l<r)K=(l+r>>1)+1,check()?(l=K,ans=f[n]+K*k):r=K-1;
    	cout<<ans<<endl;
    	return 0;
    }
    
    • 1

    信息

    ID
    10415
    时间
    2000ms
    内存
    2048MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者