题解 NOIP2023 天天爱打卡
题解 天天爱打卡
天天爱打卡,但我不爱天天打卡。
分析题意,每天可以选择是否跑步,跑步需要消耗体力,不跑步消耗为 0。存在 \(m\) 个目标,形如 \((x,y,v)\),表示截止到第 \(x\) 天已经连续跑步超过 \(y\) 天就能恢复 \(v\) 点体力,求 \(n\) 天之后能保留的最大体力。
首先贪心地想,没有任务的那些天我们一定不会跑步,因为没有任务就没有收益,白白消耗体力。所以我们应当只关注有任务的区间的情况。
考虑 dp,设 \(dp_i\) 表示截止到第 \(i\) 天能够保存的体力。则首先想到一组转移为 \(dp_i=dp_{i-1}\),也就是第 \(i\) 天不跑步,则没有收益,继承第 \(i-1\) 天的状态。接着考虑从之前的某一天 \(j\) 一直到 \(i\) 这一段时间内都跑步的情况。规定跑步的这个人最多连着跑 \(k\) 天,再多就寄了,所以我们从 \(i\) 向前枚举 \(k\) 天,有转移方程:
\(dp[i]=\max\limits_{j=i-k+1}^{i}(dp[j-2]+\sum\limits_{[l_p,r_p]\subset [j,i]}v_p-d\times (i-j+1))\)
也就是,\(j-2\) 天的情况加上 \([j,i]\) 这段跑步的时间内完成的所有任务的贡献和再减去跑步的消耗。
为什么是 \(j-2\) 呢?我们考虑我们枚举的 \(j\) 和 \(i\) 的距离最大为 \(k\),然而不能连续跑超过 \(k\) 天,所以对于当前枚举的跑步区间 \([j,i]\),必然不能和前面的某个跑步区间相连,否则会打破 \(k\) 天的限制。因此,如果第 \(j\) 天跑步, \(j-1\) 天必然不能跑步,则从 \(j-2\) 转移。
此处注意一个实现细节:如果 \(j-2\le 0\),\(dp[j-2]\) 没意义,但我们仍然能够进行转移,此时直接从 0 转移即可。
这个方法的复杂度为 \(O(nmk)\),可以拿到少量分数。
考虑继续优化这个方法。首先我们观察到,我们求和的这一部分是可以优化的。考虑把所有任务按照右端点排序,从左向右 dp 时依次把所有的任务加进一个树状数组(扫到一个右端点加一次),每次加入都把当前任务的贡献加到其左端点的位置。这样当我们转移 \(i\) 时,一定保证了当前树状数组中的所有的结束时间都比 \(i\) 早。则枚举 \(j\) 后转移时直接对 \([j,i]\) 区间求和就能得到这段时间内所有能够完成的任务的贡献和。
继续考虑,我们发现 \(n\) 特别大,能够达到 \(10^9\) 量级,然而只有 \(m\) 个区间的左右端点会产生影响。所以干脆把这 \(2m\) 个点离散化一下,根据离散化后的映射进行 dp,避免 \(n\) 过大的情况。
此时又出现了一些实现细节:
第一,离散化时要特别加入 0,因为我们转移时如果 \([j-2]\) 越界会从 0 进行转移,也就是说 0 也应该单独对应一个映射。
第二,离散化之后,我们枚举的 \(j\) 也变成了映射,所以原本直接找 \(j-2\) 天的方式就不适用了。我们需要判断 \(j\) 位置的天数 \(b[j]\) 和前一个位置 \(b[j-i]\) 相差多少。如果差距大于 1,转移时将从 \(j-1\) 转移过来;否则仍从 \(j-2\) 转移。
复杂度 \(O(mk \log(m))\),可以通过一半数据。
树状数组+扫描线dp部分分代码:(部分错误,不知道哪里错)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 100010
#define lowbit(x) x&(-x)
int c,T;
int n,m,K,d,x,y,v[N];
int X[N],Y[N];//此处X,Y分别记录每个任务区间的左右端点
int b[N],cnt;//离散化数组
struct que{
int y,v;
};
vector<que> q[N];
int dp[N];
/*树状数组*/
int Bit[N];
void add(int x,int k){
while(x<=cnt){Bit[x]+=k; x+=lowbit(x);}
}
int get(int x){
int res=0;
while(x>0){res+=Bit[x]; x-=lowbit(x);}
return res;
}
int ask(int l,int r){return get(r)-get(l-1);}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>c>>T;
while(T--){
for(int i=1;i<=cnt;i++) {
q[i].clear();Bit[i]=0;dp[i]=0;b[i]=0;v[i]=0;X[i]=Y[i]=0;}
cnt=0;
cin>>n>>m>>K>>d;
for(int i=1;i<=m;i++){
cin>>x>>y>>v[i];
X[i]=x-y+1; Y[i]=x;
b[++cnt]=x;
b[++cnt]=x-y+1;
}
//离散化
sort(b+1,b+cnt+1);
cnt=unique(b+1,b+cnt+1)-b-1;
for(int i=1;i<=m;i++){
X[i]=lower_bound(b+1,b+cnt+1,X[i])-b;
Y[i]=lower_bound(b+1,b+cnt+1,Y[i])-b;
q[Y[i]].push_back((que){X[i],v[i]});
}
/*暴力dp*/
dp[0]=0;
for(int i=1;i<=cnt;i++){//枚举天数
//把枚举到的挑战加入树状数组,便于查询
for(auto it:q[i]){
add(it.y,it.v);
}
for(int j=i;j>=1&&(b[i]-b[j]+1<=K);j--){
//枚举j代表从j到i这段时间跑步。
/*转移从j-2来,因为连续跑步天数不超过K,所以我们枚举到j则第
j-1天一定不跑(否则将存在一个长度大于K的大区间)则从j-2转移
注意离散化之后需要注意相邻两个值是否相差1,差1则选取j-2,否则
直接选取j-1*/
int pos=(b[j-1]!=b[j]-1)?j-1:j-2;
int tmp=(pos<=0)?0:dp[pos];
//j-2可能越界,但越界了也能转移,所以需要特殊处理
dp[i]=max(dp[i],tmp+ask(j,i)-d*(i-j+1));
}
dp[i]=max(dp[i],dp[i-1]);//今天不跑,直接继承
}
cout<<dp[cnt]<<'\n';
}
return 0;
}
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 2000010
#define lowbit(x) x&(-x)
#define ls(p) p<<1
#define rs(p) p<<1|1
#define val(p) t[p].val
#define tag(p) t[p].tag
/*思路见博客*/
int c,T;
int n,m,K,d,x,y,v[N];
int X[N],Y[N];//此处X,Y分别记录每个任务区间的左右端点
int b[N],cnt;//离散化数组
struct que{
int y,v;
};
vector<que> q[N];
int dp[N];
/*线段树*/
struct segTree{
int val,tag;
}t[4*N];
void push_up(int p){
val(p)=max(val(ls(p)),val(rs(p)));
}
void push_down(int p){
tag(ls(p))+=tag(p); tag(rs(p))+=tag(p);
val(ls(p))+=tag(p); val(rs(p))+=tag(p);
tag(p)=0;
}
//本题中build兼具清空线段树功能,所有值均重置为0
void build(int p,int l,int r){
tag(p)=0;val(p)=0;
if(l==r) return;
int mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
}
//区间修改
void modify(int p,int l,int r,int L,int R,int k){
if(L<=l&&r<=R){
val(p)+=k;tag(p)+=k;return;
}
push_down(p);
int mid=(l+r)>>1;
if(L<=mid) modify(ls(p),l,mid,L,R,k);
if(R>mid) modify(rs(p),mid+1,r,L,R,k);
push_up(p);
}
//查询区间最大值
int query(int p,int l,int r,int L,int R){
if(L>R) return 0;
if(L<=l&&r<=R) return val(p);
push_down(p);
int mid=(l+r)>>1;
if(R<=mid) return query(ls(p),l,mid,L,R);
if(L>mid) return query(rs(p),mid+1,r,L,R);
return max(query(ls(p),l,mid,L,R),query(rs(p),mid+1,r,L,R));
}
//二分查找对于当前i,最靠左的合法端点
int findl(int x){
int l=1,r=cnt,ans=r;
while(l<=r){
int mid=(l+r)>>1;
if(b[mid]>=x){ans=mid;r=mid-1;}
else l=mid+1;
}
return ans;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>c>>T;
while(T--){
if(cnt)
{build(1,1,cnt);//清空线段树
for(int i=1;i<=cnt;i++) {
q[i].clear();dp[i]=0;b[i]=0;v[i]=0;X[i]=Y[i]=0;}
cnt=0;}
cin>>n>>m>>K>>d;
b[++cnt]=0;
for(int i=1;i<=m;i++){
cin>>x>>y>>v[i];
X[i]=x-y+1; Y[i]=x;
b[++cnt]=x;
b[++cnt]=x-y+1;
}
//离散化
sort(b+1,b+cnt+1);
cnt=unique(b+1,b+cnt+1)-b-1;
for(int i=1;i<=m;i++){
X[i]=lower_bound(b+1,b+cnt+1,X[i])-b;
Y[i]=lower_bound(b+1,b+cnt+1,Y[i])-b;
q[Y[i]].push_back((que){X[i],v[i]});
}
/*暴力dp*/
dp[0]=0;
for(int i=1;i<=cnt;i++){//枚举天数
//区间加,记录当前的挑战产生的影响
for(auto it:q[i]){
modify(1,1,cnt,1,it.y,it.v);
}
//最左端可以转移的位置,用于区间求最值转移
int j=findl(b[i]-K+1);
//从j转移
dp[i]=max(dp[i],query(1,1,cnt,j,i)-b[i]*d-d);
//特判i的情况,即i自己是一个任务
int pos=(b[i-1]!=b[i]-1)?i-1:i-2;
int tmp=(pos<=0)?0:dp[pos];
//j-2可能越界,但越界了也能转移,所以需要特殊处理
dp[i]=max(dp[i],tmp+query(1,1,cnt,i,i)-d);
dp[i]=max(dp[i],dp[i-1]);//今天不跑,直接继承
//加入贡献
modify(1,1,cnt,i,i,tmp+b[i]*d);
}
cout<<dp[cnt]<<'\n';
}
return 0;
}

浙公网安备 33010602011771号