LGP11833 [PUTS 2025] 推箱子 学习笔记

LGP11833 [PUTS 2025] 推箱子 学习笔记

\(\texttt{Luogu Link}\)

前言

对我造成心理阴影最大的一题,也是我相当不愿意回忆的一题。

那次省选的第一天,所有人都因为一眼送的T1和诗人握持的T2,把当天的比赛当作无事发生;
我想着,不就比(低水平段的)“大众分”低了8分(T3暴力)吗,也没差到哪去,所以回去的路上还能和家人谈笑风生。

噩梦于Day2降临。

我知道Day2T2是什么——几乎完全强于重塑时光的问题;
我也知道Day2T3是什么——CF3000+风格的DP。
我更知道,在敲出Day2T1正解之前,思考它们对我来说大概率没有什么价值。

但很遗憾,我不知道Day2T1是什么。赛后证明这题有很多做法,然而我考场上一个也不会。
(其实这题的多解从数据范围上也能看出端倪,\(n\le 2\times 10^5\) 说明 \(0\sim 2\) 只牢葛都能接受。但是现在说这有什么用呢。)

最幽默的事莫过于这题最终还挂了20分,从36挂到了16。

就这样,这题没抓到的八十多分让我差点坠落出省队的三倍正式选手队线。
就这样,高一的选手,初三的选手,甚至初二的选手就这样赤裸裸的从我的脸上碾轧过去。
。。。。。。

欲哭无泪。什么时候我才能真正自信起来呢?

题意简述

在一条无穷长的数轴上摆放着 \(n\) 个箱子。第 \(i\) 个箱子在时刻 \(0\) 位于数轴 \(a_i\) 处,而你希望在时刻 \(t_i\) 以及之后所有的时刻,这个箱子处在数轴的 \(b_i\) 处。保证序列 \([a_1\dots a_n]\)\([b_1\dots b_n]\) 单调递增。

为此,从时刻 \(0\) 开始的每个单位时间内,你可以将某个箱子在数轴上移动一个单位长度,或者什么也不做。你需要保证任意时刻每个点上最多只有一个箱子。

你想知道,是否存在一种操作方法同时满足所有箱子的要求。

多测。\(T<6,n\le 2\times 10^5,1\le a_i,b_i\le 10^9,0\le t_i\le 10^{16}\)

题目配给了三个特殊性质部分分档:

  • 特殊性质 \(\text{A}\)\(\forall 1\le i<j\le n\)\(t_i=t_j\)
  • 特殊性质 \(\text{C}\)\(\forall 1\le i\le n,a_i\le b_i\)
  • 特殊性质 \(\text{B}\):在特殊性质 \(\text{C}\) 的基础上,\(\forall 1\le i<n\)\(b_i\le a_{i+1}\)

做法解析(一)

特性 \(\text{A}\) 是好做的。在所有 \(t_i\) 相等的情况下先移谁后移谁没有任何区别,所以只用算算总路程够不够就行。

特性 \(\text{B}\) 也是好做的。在所有箱子的移动不可能互相干扰的情况下,我如果要动一个箱子,一次把他推到终点就是优秀的。把 \(t_i\) 从小到大排序,看能不能完成。

注:按 \(e_i=t_i-|a_i-b_i|\)(即最早需要开始动的时刻)从小到大排序是不对的。以下给出一个反例。

\(|a_1-b_1|=3,t_1=9\to e_1=6\)
\(|a_2-b_2|=9,t_2=12\to e_2=3\)

按照 \(e_i\) 贪心的话我们得先动 \(2\) 号箱子,但这样反而会让 \(1\) 号箱子超时;按照 \(t_i\) 贪心的话我们得先动 \(1\) 号箱子,这么做是对的。

可以理解为,对于有些最晚开动时间更靠后的箱子 \(x\),它的截止时间靠前但移动时间也短,你如果先动一些需要时间很长的箱子 \(y\),反而会直接拖过 \(x\) 的截止时间。

再换个角度解释,比较显然的一点是如果有解,那么每个移动任务的步长不会改变,并且任务之间彼此独立先后完成,不应该存在什么东西动一半转而动另一个东西的。我们应该从更紧急的任务开始做起,你按 \(t\) 排序去做任务的话不会发生优先级忽然改变的事情。

考场上,我正是因此连特性 \(\text{B}\) 都没拿到。哎哎,太小丑了。砂壁。

至于特性 \(\text{C}\),有一点很显然:解决了性质 \(\text{C}\) 就相当于会正解了。因为你可以很简单地观察到整个数轴一定可以分成若干个极长段,其中每个段内箱子推的方向都一样,那么这些极长段在很大程度上都是互相独立的,把特性 \(\text{C}\) 的做法套上去改下就行。

另一方面来讲,顺着特性 \(\text{B}\) 的贪心,我们倒是有一个 \(O(n^2)\) 的做法,就是每次推那个需要最早推的箱子时候,直接暴力更新那些受到影响的箱子。

在考场上如果你把特性 \(\text{A}\)、特性 \(\text{B}\)\(O(n^2)\) 做法拼起来,你可以得到 \(\text{60pts}\)。令人忍俊不禁。

考虑我们在做暴力的时候到底发生了什么。接下来我们就以特殊性质 \(C\) 来考虑,也就是说所有箱子都往右推。设我们现在推的是箱子 \(i\),它的当前位置是 \(pos_i\)。那么它在推到 \(b_i\) 的过程中,就顺带挤走了所有满足 \(pos_j\in(pos_i,b_i]\) 中的 \(m\) 个箱子 \(x_1,x_2,\cdots ,x_m\)。接下来,我们大致需要更新 \(a_{x_1},a_{x_2},\cdots ,a_{x_m}\)\(b_i+1,b_i+2,\cdots ,b_i+m\)

那么这个 \(m\) 究竟是多少呢?从某个下标找某种区间延伸的范围,看到这东西,直接往线段树二分之类的地方想。更何况后面还有一段长得这么标致的赋值,这铁定可以拿线段树干掉的啊!

事实也的确可以如此,不过我们首先需要推导转化一下——

我们考虑对于一个箱子 \(j\) 来说它会不会被 \(i\) 推动。此时 \(j\) 会被 \(i\) 推动当且仅当 \(pos_j\le b_i+(j-i-1)\)。最关键的一步来了:移项!!!上式化为 \(pos_j-j\le b_i-i-1\)。你又发现 \(pos_j-j\) 任何时候都单调不降(\(j\) 每增加 \(1\)\(pos_j\) 至少增加 \(1\))且恒不小于 \(0\)(这意味着代码里面可以用 \(-1\) 来代表空 tag),所以你可以线段树二分找 \(i\) 后面第一个满足 \(pos_j-j>b_i-i-1\)(也即 \(pos_j-j\ge b_i-i\))的 \(j\) 喽。二分完之后你要对一堆 \(pos_j-j\) 重新赋值,更令人喜出望外的是,由于这些 \(j\) 被推过来之后是连在一起的,所以它们的 \(pos_j-j\) 全部相等。也就是说你的线段树支持区间赋值和 \(1-\text{side}\) 区间二分就可以了!

这推广到正解是不言而喻的。箱子往左推的时候,我们类似地可以推导出,箱子被推动的条件是 \(pos_j\ge b_i-(i-j-1)\),所以需要线段树二分找 \(i\) 前面的第一个满足 \(pos_j\le b_i-i\)\(j\)。再有什么疑问看代码就行。

代码实现(一)

不敢写线段树的小朋友们有难了。

#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=2e5+5,Inf=0x3f3f3f3f;
int N,flag;
struct anob{int a,b,id;lolo t;}C[MaxN];
bool cmpt(anob a,anob b){return a.t<b.t;}
struct SegTree{
    int cl[MaxN<<2],cr[MaxN<<2],cmid[MaxN<<2];
    int tag[MaxN<<2],mx[MaxN<<2],mn[MaxN<<2];lolo sum[MaxN<<2];
    int ls(int u){return u<<1;}
    int rs(int u){return (u<<1)|1;}
    void pushup(int u){
        mx[u]=max(mx[ls(u)],mx[rs(u)]);
        mn[u]=min(mn[ls(u)],mn[rs(u)]);
        sum[u]=sum[ls(u)]+sum[rs(u)];
    }
    void build(int u,int tl,int tr){
        tag[u]=-1,cl[u]=tl,cr[u]=tr;
        if(tl==tr){
            int val=C[tl].a-tl;
            if(tl==0)val=-Inf;
            if(tl==N+1)val=Inf;
            sum[u]=mx[u]=mn[u]=val;
            return;
        }
        int tmid=(tl+tr)>>1;cmid[u]=tmid;
        build(ls(u),tl,tmid),build(rs(u),tmid+1,tr),pushup(u);
    }
    void maketag(int u,lolo x){
        sum[u]=(cr[u]-cl[u]+1)*x,mx[u]=mn[u]=tag[u]=x;
    }
    void pushdown(int u){
        if(tag[u]==-1)return;
        maketag(ls(u),tag[u]),maketag(rs(u),tag[u]),tag[u]=-1;
    }
    void update(int u,int dl,int dr,int x){
        if(dl<=cl[u]&&cr[u]<=dr){maketag(u,x);return;}
        pushdown(u);
        if(dl<=cmid[u])update(ls(u),dl,dr,x);
        if(dr>cmid[u])update(rs(u),dl,dr,x);
        pushup(u);
    }
    lolo getsum(int u,int dl,int dr){
        if(dl<=cl[u]&&cr[u]<=dr)return sum[u];
        pushdown(u);lolo res=0;
        if(dl<=cmid[u])res+=getsum(ls(u),dl,dr);
        if(dr>cmid[u])res+=getsum(rs(u),dl,dr);
        pushup(u);return res;
    }
    int lstser(int u,int dd,int x){
        if(cl[u]>dd||mn[u]>x)return -1;
        if(cl[u]==cr[u])return cl[u];
        pushdown(u);int cres=lstser(rs(u),dd,x);
        if(cres==-1)cres=lstser(ls(u),dd,x);
        return cres;
    }
    int nxtser(int u,int dd,int x){
        if(cr[u]<dd||mx[u]<x)return -1;
        if(cl[u]==cr[u])return cl[u];
        pushdown(u);int cres=nxtser(ls(u),dd,x);
        if(cres==-1)cres=nxtser(rs(u),dd,x);
        return cres;
    }
}SgT;
void mian(){
    readi(N);flag=1;
    for(int i=1;i<=N;i++)readis(C[i].a,C[i].b,C[i].t),C[i].id=i;
    SgT.build(1,0,N+1);sort(C+1,C+N+1,cmpt);
    lolo ctim=0,calc,cpos,epd;
    for(int i=1,cdif;i<=N;i++){
        auto [ca,cb,cid,ct]=C[i];
        cpos=SgT.getsum(1,cid,cid)+cid,cdif=cb-cid;
        if(cpos<cb){
            epd=SgT.nxtser(1,cid,cdif);
            calc=SgT.getsum(1,cid,epd-1)+(cid+epd-1)*(epd-cid)/2;
            ctim+=(2*cb+(epd-cid-1))*(epd-cid)/2-calc;
            SgT.update(1,cid,epd-1,cdif);
        }
        if(cpos>cb){
            epd=SgT.lstser(1,cid,cdif);
            calc=SgT.getsum(1,epd+1,cid)+(cid+epd+1)*(cid-epd)/2;
            ctim+=calc-(2*cb-(cid-epd-1))*(cid-epd)/2;
            SgT.update(1,epd+1,cid,cdif);
        }
        if(ctim>ct){flag=0;break;}
    }
    puts(flag?"Yes":"No");
}
int Tpn,Tcn;
int main(){
    readis(Tpn,Tcn);
    while(Tcn--)mian();
    return 0;
}

做法解析(二)

实际上,这道题还有线性做法。小编也觉得很惊讶,但它真的有线性做法。

但是现在鸽了。找个时间补。

代码实现(二)

//鸽了。别急。

反思总结

  1. 从某个下标找某种区间延伸的范围,看到这东西,不妨直接往线段树二分之类的地方想。
  2. 要多试着用不等式刻画条件,然后尝试移项把和不同下标关联的量分开。

后记

这题的单老葛做法还有用可并堆或者颜色段均摊的。但是同样是单老葛,线段树的应用是确为最广泛的,所以要说的话还是先好好精进一下线段树吧。

至于线性做法,别急。

总之,

一季后,我从跌倒的地方站了起来。

我成功了,我不再是以前的那个我了。

好好好,希望赛场有稳定切掉这种题的能力。

后记的后记

喜欢我们NOI2025的D2T1吗?推箱子也是后继有题了。

posted @ 2025-07-09 10:46  矞龙OrinLoong  阅读(6)  评论(0)    收藏  举报