Meteors 题解
蒟蒻初学整体二分,写一篇题解记录一下思考与看法。
题目大意
在一个环形的轨道上分别着若干国家的空间站,在接下来的一段时间内会出现若干次陨石,每次出现在环形的某一段轨道,每个国家都想收集一定量的陨石,需要判断每个国家最少在什么时候可以集齐所需的陨石。
思路分析
首先可以想到一个简单而暴力的做法:
枚举每一个国家,逐一将每一次陨石落下,判断其在哪一个时间收集满了所需的陨石。
但这样的时间复杂度是 \(O(n^2)\)(令 \(n,m,k\) 同阶)的,没有办法解决这个问题。
需要更优的解法!
容易发现,对于每一个国家,答案都是单调不降的,这启示我们可以使用二分!
那么我们首先会想到,对于每一个国家,二分其收集满陨石的时刻。
但这样的时间复杂度是 \(O(n^2\log n)\) 的,甚至还没有暴力优秀。
因此,我们需要转化思考角度。
我们发现,在对于每个国家进行二分时,我们都降落了很多次陨石(平均 \(n\log n\) 次),这肯定是巨亏的,那么,我们是否可以不枚举国家进行降落陨石,而是改为降落陨石,枚举每个国家的时间呢?
这时,整体二分就闪亮登场了!
整体二分的思想是将很多个需要二分的对象放在一起,通过改变枚举对象来达到优化时间复杂度的目标。
比如在这题中,我们可以将所有的国家放在一起进行二分,在二分时只降落区间中点前半部分的陨石,在查询每个国家是否收集满,并以此将其划分到子区间中。
具体的说,我们设计函数 solve(l,r,x,y) 表示 \([l,r]\) 的陨石对 \([x,y]\) 的国家的贡献。(比较抽象?感性理解)
首先考虑边界,当 \(l=r\) 时,我们可以确定答案。
否则,我们另 \(\text{mid}=\frac{l+r}{2}\),将从 \(l\) 到 \(\text{mid}\) 之间的陨石全部降落,并将这个区间的所有的国家进行判断,如果已经收集满了,我们就将它划分到左区间中,否则划分到右区间中。
这样递归下去我们就可以得到所有国家的答案了!
时间复杂度?我们需要递归 \(\log n\) 层,每层需要把一半的陨石降落,陨石降落是一个区间操作,我们可以通过一个数据结构将降落过程优化到 \(\log n\) 级别,因此整体二分的时间复杂度是 \(O(n\log^2n)\) 的。(是不是比之前的做法优秀许多?)
整体二分的思想比较有意思,它通过转化统计和二分的对象来解决问题。
还有很多类似的思想:更换定义域和值域的权值线段树,通过离线来随意操纵时间线的 \(\text{cdq}\) 分治,通过倍增对值域进行划分的倍增分块等等,它们无疑是人类智慧的结晶,化不可做为可做,化不可能为可能。
代码
注释超详细
#include <bits/stdc++.h>
using namespace std;
const int N=600100;//因为是环,要断环成链,所以开双倍
typedef long long ll;
int n,m,k,in1,idx;
int to[N],nxt[N],ans[N];//我们需要用邻接表来存储一个国家的某个空间站的下一个空间站,不能每次遍历轨道,不然会 T(时间复杂度不变,但常数会变大)
struct country{int head,id;ll need;}con[N],con2[N];//国家的结构体,另一个是用来暂时存储的,head 是该国家邻接表的表头,id是国家编号,need是所需的陨石数量
struct stone{int l,r;ll a;}sto[N];//陨石结构体,左右端点和陨石数量
void add(int u,int v){idx++;to[idx]=v;nxt[idx]=con[u].head;con[u].head=idx;}//邻接表加边
struct Tree_array{//选择树状数组来辅助陨石下落
ll a[N];
#define lowbit(x) ((x)&(-(x)))
void change(int x,ll k){for(;x<=(m<<1);x+=lowbit(x)) a[x]+=k;}
ll ask(int x){ll ans=0;for(;x;x-=lowbit(x)) ans+=a[x];return ans;}//树状数组常规操作,单点加,区间查询
}tree;
void solve(int l,int r,int x,int y){
if(l==r){//到达边界了!
for(int i=x;i<=y;i++)
ans[con[i].id]=l;//得到该区间的国家的答案
return ;//速润
}
int mid=(l+r)>>1,ll=0,rr=n;//mid是区间中点,ll,rr是辅助暂时存储国家的两个计数变量
for(int i=l;i<=mid;i++){
tree.change(sto[i].l,sto[i].a);
tree.change(sto[i].r+1,-sto[i].a);
}//通过差分操作让树状数组实现区间修改
for(int s=x;s<=y;s++){
long long sum=0;
for(int i=con[s].head;i&&sum<=con[s].need;i=nxt[i])
sum+=tree.ask(to[i]+m)+tree.ask(to[i]);//对于这个国家,统计信息(树状数组的区间查询变成了单点查询,又因为断环成链所以要查询两个部分)
if(sum>=con[s].need) con2[++ll]=con[s];//划分到左区间
else con2[++rr]=con[s],con2[rr].need-=sum;//划分到右区间,同时需要的陨石减去这一部分(这样才可以保证以后的递归过程中只需要降落子区间前半部分的陨石就可以统计答案)
}
for(int i=l;i<=mid;i++){
tree.change(sto[i].l,-sto[i].a);
tree.change(sto[i].r+1,sto[i].a);
}//把陨石送回去
for(int i=1;i<=ll;i++)
con[x+i-1]=con2[i];//copy一份
for(int i=n+1;i<=rr;i++)
con[x+ll+i-n-1]=con2[i];
solve(l,mid,x,x+ll-1);//递归左右子区间
solve(mid+1,r,y-rr+n+1,y);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d",&in1);
add(in1,i);//加边
}
for(int i=1;i<=n;i++){
scanf("%lld",&con[i].need);//读入国家信息
con[i].id=i;
}
scanf("%d",&k);
for(int i=1;i<=k;i++){
scanf("%d%d%d",&sto[i].l,&sto[i].r,&sto[i].a);
if(sto[i].r<sto[i].l) sto[i].r+=m;//更新左端点
}
solve(1,k+1,1,n);//右端点划分到k+1,方便判断无解
for(int i=1;i<=n;i++){
if(ans[i]==k+1) cout<<"NIE\n";
else cout<<ans[i]<<'\n';
}
return 0;
}

浙公网安备 33010602011771号