[IOI2021]distribute candies

https://loj.ac/s/1365219

总算是A掉了这道IOI2021day1签到。但不仅受了题解提示,而且花的时间太长了,修正了好多思路上的补丁,很无奈。好在积累了一个数据结构常见套路。
这才知道IOI的题目不用输入输出;甚至不用主函数,这跟Topcoder似乎有点类似。

vector<int>distribute_candies(vector<int>c,vector<int>l,vector<int>r,vector<int>v){
    vector<int>ans;
    return ans;
}//这是本题的输出格式,数组用vector,其他数据都会给到()中。真神奇,这是怎么运行的呢。。

Intuition

套路 区间操作转扫描线,并平行于时间轴扫描、处理。

单独考察某个元素,我们拟定它的变化曲线如下,则发现一旦有一段的极差 \(>c\),就一定最小值对应了盒子为0,最大值对应了盒子为满(\(c\))。这告诉我们,极差很有用。

考察极差公式=最大值-最小值,因此我们需要分别对每个元素维护其在时间增长过程中的最大值与最小值。发现一般数据结构无法解决关于时间的问题。考虑升维。

将一维序号轴加一维时间轴做纵轴;对一个 \(t(1\le t\le q)\) 时刻的操作 \((l,r,v)\),在横坐标 \([l,r]\)、纵坐标 \([t,q]\) 的矩形加 \(v\)。
这样,用一条竖的扫描线横着扫便可以分别地处理各个元素的答案。

框架有了,继续考虑,我们扫描完 \(\le i\) 的所有差分后,需要线段树查询的是一个极差。怎样的极差呢?
首先,如果整体的极差小于等于 \(c\),则不会“顶天”(但可能“立地”),所以答案为最后一个点的值-整体最小值。
考虑一个后缀,如果它的极差大于 \(c\),那么就一定“顶天立地”,考虑找到最靠后的后缀使得后缀的极差大于 \(c\),并且容易判断分界点是“顶天”还是“立地”(显然必为其一),于是稍微分类讨论一下:

  1. 若该点“顶天”,则往后会“立”一次“地”,计算式为:最后一个点的值-后缀最小值
  2. 若该点“立地”,则往后会“顶”一次天,计算式为:max{\(c\)-(后缀最大值-最后一个点的值),0}(至于为什么这里要取max上面不用取min,感觉也不是太好用语言说,大概结合样例意会一下吧(万一hack掉也说不定,总之这样是能AC的)(或许跟代码实现也有一定关系))

那么现在只需要知道怎样找最靠后的满足条件的后缀了!这不是线段树二分的模板吗?

线段树二分是什么?

这里记录了树状数组上二分的基本步骤。可以发现,树状数组二分是在树状数组上从边角自下而上跳;线段树二分不然,是和普通的线段树一样从上层往下层选左右儿子中的一个前往,其单次时间复杂度为 \(O(\log n)\)。利用【左/右儿子是否满足条件?】这一问题的单调性,来进行抉择。

具体实现较为繁琐,且看下面代码。

#include <bits/stdc++.h>
#include "candies.h" //根据题目要求,添加所需头文件
using namespace std;
const int N=2e5+5;
typedef long long ll;
int n,q,tot,c[N],l[N],r[N],v[N];
ll tn[N<<2],tx[N<<2],tag[N<<2];
struct O{int x,l,r,v;}a[N<<1];//主函数中可以看到共有2n个差分,所以N要*2
void pushup(int k){
	tn[k]=min(tn[k<<1],tn[k<<1|1]),tx[k]=max(tx[k<<1],tx[k<<1|1]);//tn是t_min的简写,tx——t_max
}
void pushdown(int k){
	if(!tag[k])return;
	tn[k<<1]+=tag[k],tn[k<<1|1]+=tag[k];
	tx[k<<1]+=tag[k],tx[k<<1|1]+=tag[k];
	tag[k<<1]+=tag[k],tag[k<<1|1]+=tag[k];
	tag[k]=0;
}
void chg(int L,int R,int v,int l,int r,int k){
	if(L<=l&&r<=R){tn[k]+=v,tx[k]+=v,tag[k]+=v;return;}
	pushdown(k);
	int mid=l+r>>1;
	if(L<=mid)chg(L,R,v,l,mid,k<<1);
	if(R>mid)chg(L,R,v,mid+1,r,k<<1|1);
	pushup(k);
}
ll get(int p,int l,int r,int k){ //一个显得有些累赘的查询单点值的函数
	if(l==r)return tn[k];
	pushdown(k);
	int mid=l+r>>1;
	if(p<=mid)return get(p,l,mid,k<<1);
	return get(p,mid+1,r,k<<1|1);
}
bool ident; //ident=1表示分界点是“立地”,反之“顶天”
ll askmn(int l,int r,int k,int c,ll mx,ll mn){
	if(l==r)return ident=mn>tn[k],min(mn,tn[k]);
	pushdown(k);
	int mid=l+r>>1;
	if(max(tx[k<<1|1],mx)-min(tn[k<<1|1],mn)>c)return askmn(mid+1,r,k<<1|1,c,mx,mn);//这个mn/mx的传和更新是查询**后缀**最大值的桥梁
	return askmn(l,mid,k<<1,c,max(mx,tx[k<<1|1]),min(mn,tn[k<<1|1]));
}
ll askmx(int l,int r,int k,int c,ll mx,ll mn){ //只是把askmn反了一下,并且没必要再有一个函数来返回ident了(本来应该新建一个返回ident的函数才比较对称,但……)
	if(l==r)return max(mx,tx[k]);
	pushdown(k);
	int mid=l+r>>1;
	if(max(tx[k<<1|1],mx)-min(tn[k<<1|1],mn)>c)return askmx(mid+1,r,k<<1|1,c,mx,mn);
	return askmx(l,mid,k<<1,c,max(mx,tx[k<<1|1]),min(mn,tn[k<<1|1]));
}
vector<int>distribute_candies(vector<int>c,vector<int>l,vector<int>r,vector<int>v){
	n=c.size(),q=l.size();
	for(int i=0;i<q;i++)a[++tot]={l[i],i+1,q,v[i]},a[++tot]={r[i]+1,i+1,q,-v[i]};
	sort(a+1,a+tot+1,[](O a,O b){return a.x<b.x;});//lambda语法
	vector<int>ans;
	for(int i=0,j=1;i<n;i++){ 
		while(j<=tot&&a[j].x<=i)chg(a[j].l,a[j].r,a[j].v,0,q,1),j++;
		ll k=get(q,0,q,1);//最后一个点的值
		if(tx[1]-tn[1]<=c[i])ans.push_back(k-tn[1]);//整体极差不超过c[i],不会顶天
		else {
			askmn(0,q,1,c[i],-1e18,1e18);
			if(ident)ans.push_back(max(0ll,c[i]-askmx(0,q,1,c[i],-1e18,1e18)+k));//分类一下
			else ans.push_back(k-askmn(0,q,1,c[i],-1e18,1e18));
		}
	}
	return ans;
}

p.s.似乎intuition写完就已经讲完了。。

上一篇:通过pytube模块和FFmpeg实现自动爬取youtube上的视频和音频并自动拼接


下一篇:Link-Cut-Tree(1)