题解 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;
}

继续思考,我们发现还有一个地方没能处理,也就是我们一直在暴力枚举转移的左端点 $j$,而我们的最大连续跑步天数 $k$ 和 $m$ 同级,导致我们的复杂度一直在一个 $O(m^2)$ 的瓶颈上。考虑每次转移的左端点实际上时一段区间,我们要做的就是从这段区间中挑出一个最大值转移过来,而中途加入的任务又会导致一段区间的 $j$ 的贡献发生变化,也就是区间修改。要想同时满足这两个操作的需求,则考虑线段树。 我们建一棵线段树,每次扫到一个任务的右端点 $r_p$,就给 $[1,l_p]$ 的部分都加上一个 $v_p$,代表从这一段区间转移时都能获得任务 $p$ 给出的贡献。对于每个点 $i$,二分查找它的合法转移区间的最左侧的点 $j$,然后按照先前的逻辑进行转移。每次转移完以后,把第 $i$ 位置从之前的天数中转移来的部分给 $i$ 加上,用于后面的转移。最后输出 $dp[cnt]$,其中 $cnt$ 是离散化之后的编号最大值。其余实现细节和前面提到的相同。 代码如下:
点击查看代码
#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;
}

posted @ 2025-07-01 17:18  Yun_Mo_s5_013  阅读(70)  评论(0)    收藏  举报