P9871 [NOIP2023] 天天爱打卡 题解

· · 题解

前言

本人在赛时可以说是按照出题人设计的部分分一步一步想到正解,思维过程应该算非常标准,且很多部分分的代码也在赛时打出来了。

所以这篇题解肯定能做到流畅,详细,具体。

题目大意

一共有 n 天,大 Y 可以花费 d 的能量在任何一天跑步,但大 Y 不能在连续的 k 天跑步。

此外,将给出 m 段任务区间 [l_i,r_i]。如果在这段区间的每一天都跑了步,则会获得 v_i 的能量。

Y 的初始能量为 0,请你最大化跑完步后大 Y 最终的能量。

题目分析

以下复杂度均省略了 T,即数据组数。

直接枚举每一天是否跑了步,然后计算贡献即可。

复杂度 O(2^n(n+m)),期望得分 8 分。

这一部分没什么太大用处,代码也很好写,故不给出代码。

如果我们知道了在一段区间内都跑了步,我们则可以直接枚举 m 个区间并计算出对答案的贡献。所以我们设计状态 dp_{i},表示只考虑前 i 天的最大答案。首先有转移 dp_i=dp_{i-1}。其次,我们考虑右端点为 i 的区间。暴力枚举左端点 j(注意区间长度不能超过 k),则我们有转移:

其中 $w_{l,r}$ 表示任务区间对 $[l,r]$ 的贡献,可以 $O(m)$ 单次计算。加上 $O(n)$ 枚举左端点,可以做到 $O(nmk)$。 期望得分 $16$ 分。 这一部分代码如下: ```cpp ll dp[N]; inline ll get(int l,int r){ ll ans=0; rep(i,1,m)if(a[i].l>=l&&a[i].r<=r)ans+=a[i].k; return ans; }//计算贡献 inline ll got(int x){ //由于 j-2 可能越界,但并非无法转移,故我们额外写一个函数表示 dp 的值 if(x>=0)return dp[x]; return 0; } inline void subtask1(){ rep(i,0,n)dp[i]=0; //初始化 rep(i,1,n){ rep(j,max(i-k+1,1),i)dp[i]=max(dp[i],get(j,i)+got(j-2)-1LL*d*(i-j+1));//状态转移 dp[i]=max(dp[i],dp[i-1]); } cout <<dp[n]<<'\n'; } ``` - 我会扫描线和树状数组! 我们考虑优化计算贡献的方式。 可以想到,对于枚举的区间 $[j,i]$,能够贡献答案的任务区间 $[l_k,r_k]$ 一定满足 $j\le l_k\le r_k\le i$。 那么我们按照右端点顺序把任务区间加入,则 $r_k\le i$ 的条件就能天然满足。计算贡献时,我们就只需要查询满足 $l_k\ge j$ 的任务区间的权值和即可,这个可以用树状数组维护,单次计算贡献的复杂度降至 $O(\log m)$。 复杂度 $O(nk \log m)$,期望得分 $36$ 分。 这一部分代码如下: ```cpp vector<Pi >p[N];//这个存的任务区间 ll t[N]; inline void add(int x,int k){ x=n-x+1;//这里直接倒着存,写的时候就不用差分了,下面一样的道理 while(x<=n)t[x]+=k,x+=x&-x; } inline ll query(int x){ ll ans=0; x=n-x+1; while(x)ans+=t[x],x-=x&-x; return ans; }//树状数组 #define E(x) for(auto y:p[x]) inline void subtask2(){ rep(i,1,m)p[a[i].r].pb({a[i].l,a[i].k}); rep(i,0,n)dp[i]=0;//初始化 rep(i,1,n){ E(i)add(y.first,y.second); rep(j,max(i-k+1,1),i)dp[i]=max(dp[i],query(j)+got(j-2)-1LL*d*(i-j+1)); dp[i]=max(dp[i],dp[i-1]); } rep(i,1,n)p[i].clear(),t[i]=0;//多测要清空哦! cout <<dp[n]<<'\n'; } ``` - 我会离散化! 当 $n$ 变得特别大,有用的端点也就只有 $O(m)$ 个,所以直接把 $n$ 按照任务区间的端点离散化,计算贡献依旧正常,就是要注意离散化后相邻两个值是否相差 $1$。复杂度 $O(mk\log m)$。 期望得分 $52$ 分。 这一部分代码如下: ```cpp //b 数组是离散化数组,ln 是离散化数组的长度,其它部分都一样 inline void subtask3(){ n=ln; rep(i,1,m){ a[i].l=lower_bound(b+1,b+ln+1,a[i].l)-b; a[i].r=lower_bound(b+1,b+ln+1,a[i].r)-b; p[a[i].r].pb({a[i].l,a[i].k}); } rep(i,0,ln)dp[i]=0; rep(i,1,ln){ E(i)add(y.first,y.second); for(int j=i;j>=1&&b[i]-b[j]+1<=k;j--){ int pos=j-2; if(b[j-1]!=b[j]-1)pos=j-1; dp[i]=max(dp[i],query(j)+got(pos)-1LL*(b[i]-b[j]+1)*d); } dp[i]=max(dp[i],dp[i-1]); } cout <<dp[ln]<<'\n'; rep(i,1,ln)t[i]=0,p[i].clear(); } ``` - 我会线段树! 考虑我们的复杂度始终无法摆脱一个类似 $O(n^2)$ 的瓶颈。显然在于我们每次转移都是暴力枚举左端点。而不难发现,每次枚举的左端点是一个区间,只要能够满足一个查询区间最大值的数据结构就能把复杂度降下去。但中途加入任务区间又会带来区间修改。等等,区间修改和区间最大值?这直接线段树不就行了!使用线段树来处理,每次二分最靠左的满足条件的 $j$(当然也可以双指针),复杂度 $O(m\log m)$,足以通过此题! 赛时满分代码如下(去掉文件输入输出): ```cpp #include<bits/stdc++.h> #define ll long long #define mid (l+r>>1) #define rep(x,y,z) for(int x=(y);x<=(z);x++) #define per(x,y,z) for(int x=(y);x>=(z);x--) #define e(x) for(int i=h[x],y=to[i];i;i=nxt[i],y=to[i]) #define Pi pair<int,int> #define pb push_back #define ui unsigned ll inline int read(){int s=0,w=1;char c=getchar();while(c<48||c>57){if(c=='-')w=-1;c=getchar();}while(c>=48&&c<=57)s=(s<<1)+(s<<3)+c-'0',c=getchar();return s*w;} const int N=2e5+5,M=1e6+5,inf=(1LL<<31)-1; const ll llf=1e18; using namespace std; int n,m,k,d,lsh[N],b[N],ln,testid,T;//days,tasks,limits,costs struct node{ int l,r,k; }a[N]; inline void init(){ ln=0; n=read(),m=read(),k=read(),d=read(); lsh[m*2+1]=0; rep(i,1,m){ a[i].r=read(),a[i].l=a[i].r-read()+1,a[i].k=read(); lsh[(i<<1)-1]=a[i].l,lsh[i<<1]=a[i].r; } sort(lsh+1,lsh+m*2+1); rep(i,2,m*2+1)if(lsh[i]^lsh[i-1])b[++ln]=lsh[i-1];//离散化 } ll dp[N]; inline ll got(int x){ if(x>=0)return dp[x]; return 0; } vector<Pi >p[N]; #define E(x) for(auto y:p[x]) #define L x<<1 #define R L|1 #define lc L,l,mid #define rc R,mid+1,r #define OK Ll<=l&&r<=Rr struct seg{ ll w,laz; }xd[N<<2]; inline void insert(int x,ll k){ xd[x].w+=k,xd[x].laz+=k; } inline void pushdown(int x){ insert(L,xd[x].laz),insert(R,xd[x].laz),xd[x].laz=0; } inline void getup(int x){ xd[x].w=max(xd[L].w,xd[R].w); } inline void build(int x,int l,int r){ xd[x]={0,0}; if(l==r)return; build(lc),build(rc); } inline void modify(int x,int l,int r,int Ll,int Rr,ll k){ if(OK)return insert(x,k),void(); pushdown(x); if(Ll<=mid&&Rr>=l)modify(lc,Ll,Rr,k); if(Ll<=r&&Rr>mid)modify(rc,Ll,Rr,k); getup(x); } inline ll query(int x,int l,int r,int Ll,int Rr){ if(Ll>Rr)return 0; if(OK)return xd[x].w; pushdown(x); if(Rr<=mid)return query(lc,Ll,Rr); if(Ll>mid)return query(rc,Ll,Rr); return max(query(lc,Ll,Rr),query(rc,Ll,Rr)); } //线段树 #define Root 1,1,ln inline int find(int x){ int l=1,r=ln,ans=r; while(l<=r)if(b[mid]>=x)ans=mid,r=mid-1; else l=mid+1; return ans; }//手写二分 inline void subtaskall(){ build(Root); rep(i,0,ln)dp[i]=0; rep(i,1,m){ a[i].l=lower_bound(b+1,b+ln+1,a[i].l)-b; a[i].r=lower_bound(b+1,b+ln+1,a[i].r)-b; p[a[i].r].pb({a[i].l,a[i].k}); } rep(i,1,ln){ E(i)modify(Root,1,y.first,y.second); int j=find(b[i]-k+1); dp[i]=max(dp[i],query(Root,j,i-1))-1LL*b[i]*d-1LL*d; int pos=i-2; if(b[i-1]!=b[i]-1)pos=i-1; dp[i]=max(dp[i],query(Root,i,i)+got(pos)-d); dp[i]=max(dp[i],dp[i-1]); modify(Root,i,i,got(pos)+1LL*b[i]*d); } cout <<dp[ln]<<'\n'; rep(i,1,ln)p[i].clear(); }//dp inline void solve(){ subtaskall(); } int main(){ testid=read(),T=read(); while(T--){ init(); solve(); } return 0; } ``` 写的那么用心,点个赞再走吧 !awa #### update 2024/11/5: - 修改了一处评论区指出的笔误。 - 一晃一年过去,又一年的 NOIP 又要开始了,如果在备战 NOIP 时感到迷茫,可以从[这篇专栏](https://www.luogu.com.cn/article/r244u201)获取一些建议,祝大家的 OI 生涯一帆风顺!