洛谷 P5025 - 炸弹
洛谷又关发题解入口了…………………………
洛谷题目页面传送门
题意见洛谷。
不难想到建图,将每个炸弹连向所有它能炸到的炸弹,然后在这个有向图上处理。
先来考虑怎么处理。不难发现,每个SCC内的节点的最终能炸到的炸弹集合是相等的。于是我们跑一遍Tarjan,然后缩点。这时候变成了一个DAG,我们只需要求出每个SCC对应的炸弹集合大小,即它能够到达的非虚拟节点节点的数量即可。定义每个SCC的权值为它内部的非虚拟节点节点的数量,那么要求的就是缩点之后每个点能够到达的所有点的点权之和。考虑DP,\(dp_i=\sum\limits_{(i,j)\in E}dp_j\)。然后交到洛谷里,WA了前两个点;交到LOJ,AC。wtf??
看了神鱼的题解才醒悟过来,这样DP会算重(原数据太水了,洛谷加了hack数据)。根据她的题解,上面说的那个问题是个世纪难题。那怎么办呢?不难发现这里有特殊性质:每个炸弹最终能炸到的炸弹集合是个区间!这个证明实在是太简单了。于是我们可以维护每个SCC能到达的区间(的左端点和右端点),然后DP求这两个端点即可。这样不是\(\sum\)了,而是\(\min/\max\)了,就不存在重不重的问题了。
于是时间复杂度与边数成正比。
接下来考虑如何建图使得边数比较小。你可能会说,这也太套路了……线段树优化即可。由于这里是单点连向区间,只需要维护一棵虚拟节点线段树。
时空复杂度都是\(\mathrm O\!\left(n\log n\right)\)。常数比较大,把vector
邻接表改成链式前向星即可不开O2 AC。有一个奇怪的现象,如果用vector
,我们不是要开\(\mathrm O(n\log n)\)个vector
嘛,这样会导致机子很卡,输出答案之后还要停一会儿才结束程序。可能是因为vector
空间实在太大了。于是就有了一个经验:下次遇到点数/边数比较大的图论问题,尽量用链式前向星。说起来,今年省选Day2T2也是这个问题,如果改成链式前向星大概率就AC了,可惜现场我想当然了,以为linux虚拟机里跑过就可以了,就懒得改了。当时几天后发现这个,后悔死………………
代码:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
typedef long long ll;
const int inf=0x3f3f3f3f,mod=1000000007;
void read(ll &x){
x=0;char c=getchar();bool ne=false;
while(!isdigit(c))ne|=c=='-',c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
if(ne)x=-x;
}
const int N=500000,NOW=N<<2,M=N*20;
int n;
struct bomb{ll x,r;}a[N+1];
bool operator<(bomb x,bomb y){return x.x<y.x;}
int now;
struct addedge{
int sz,head[NOW+1],nxt[M+1],val[M+1];
void init(){sz=0;memset(head,0,sizeof(head));}
void ae(int x,int v){
nxt[++sz]=head[x];val[sz]=v;head[x]=sz;
}
}nei;
struct segtree{
struct node{int l,r,nd;}nd[N<<2];
#define l(p) nd[p].l
#define r(p) nd[p].r
#define nd(p) nd[p].nd
void bld(int l=1,int r=n,int p=1){
l(p)=l;r(p)=r;
if(l==r)return nd(p)=l,void();
nd(p)=++now;
int mid=l+r>>1;
bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
nei.ae(nd(p),nd(p<<1));nei.ae(nd(p),nd(p<<1|1));
}
void init(){bld();}
void ae(int l,int r,int v,int p=1){
if(l<=l(p)&&r>=r(p))return nei.ae(v,nd(p)),void();
int mid=l(p)+r(p)>>1;
if(l<=mid)ae(l,r,v,p<<1);
if(r>mid)ae(l,r,v,p<<1|1);
}
}segt;
int dfn[NOW+1],low[NOW+1],nowdfn;
int stk[NOW],top;
bool ins[NOW+1];
vector<vector<int> > scc;
int cid[NOW+1];
void dfs(int x){
dfn[x]=low[x]=++nowdfn;
ins[stk[top++]=x]=true;
for(int i=nei.head[x];i;i=nei.nxt[i]){
int y=nei.val[i];
if(!dfn[y])dfs(y),low[x]=min(low[x],low[y]);
else if(ins[y])low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]){
scc.pb(vector<int>());
while(true){
int y=stk[--top];
ins[y]=false;
scc.back().pb(y);cid[y]=scc.size()-1;
if(y==x)break;
}
}
}
addedge cnei;
int dp_l[NOW],dp_r[NOW];
int main(){
cin>>n;
for(int i=1;i<=n;i++)read(a[i].x),read(a[i].r);
now=n;
nei.init();
segt.init();
for(int i=1;i<=n;i++){//建图
int l=lower_bound(a+1,a+n+1,bomb({a[i].x-a[i].r,0}))-a,r=upper_bound(a+1,a+n+1,bomb({a[i].x+a[i].r,0}))-1-a;
segt.ae(l,r,i);
}
for(int i=1;i<=now;i++)if(!dfn[i])dfs(i);//Tarjan
cnei.init();
for(int i=1;i<=now;i++)for(int j=nei.head[i];j;j=nei.nxt[j]){//缩点
int x=nei.val[j];
if(cid[i]!=cid[x])cnei.ae(cid[i],cid[x]);
}
int ans=0;
for(int i=0;i<scc.size();i++){//DP
dp_l[i]=inf;dp_r[i]=-inf;
vector<int> &v=scc[i];
// printf("scc#%d=",i);for(int j=0;j<v.size();j++)cout<<v[j]<<" ";puts("");
int sum=0;
for(int j=0;j<v.size();j++)if(v[j]<=n)
dp_l[i]=min(dp_l[i],v[j]),dp_r[i]=max(dp_r[i],v[j]),(sum+=v[j])%=mod;
for(int j=cnei.head[i];j;j=cnei.nxt[j]){
int x=cnei.val[j];
dp_l[i]=min(dp_l[i],dp_l[x]);dp_r[i]=max(dp_r[i],dp_r[x]);
}
// printf("dp=%d\n",dp[i]);
(ans+=1ll*(dp_r[i]-dp_l[i]+1)*sum%mod)%=mod;
}
cout<<ans;
return 0;
}
然而这题真的真的一脸有线性复杂度做法的样子。因为在DP出错然后发现性质的那个时候,就已经暗示了这题有特殊性质,不是一般的区间连边,有可能能做到线性。
想连出边没什么前途。不妨反过来想,盯着一个点的连向它的入边(其实在算贡献的题目中,这个思想就是换一个贡献体)。注意到,能向它连入边的点的爆炸范围都能覆盖它。而对于它左边,显然那些能连入边的点都覆盖最右边那个点;右边类似。于是我们只需要对于每个点,让左右两侧最靠近它的能连向它的点连向它即可,这样正确性可以用“能到达”的传递性证。
现在边数复杂度\(\mathrm O(n)\)了,我们想努力把连边也做到\(\mathrm O(n)\),这样总时空复杂度就是\(\mathrm O(n)\)了。难点在于如何快速找到最靠近的能连向它的点。以左边为例,可以从左往右扫描,任意时刻显然选越后被扫描到的越好。而每个点的爆炸范围又是一个区间,一旦不能被它爆炸到,以后的点都不能了。很自然地想到单调栈。
代码(在之前的代码上魔改的):
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
typedef long long ll;
const int inf=0x3f3f3f3f,mod=1000000007;
void read(ll &x){
x=0;char c=getchar();bool ne=false;
while(!isdigit(c))ne|=c=='-',c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
if(ne)x=-x;
}
const int N=500000,M=N<<1;
int n;
struct bomb{ll x,r;}a[N+1];
bool operator<(bomb x,bomb y){return x.x<y.x;}
struct addedge{
int sz,head[N+1],nxt[M+1],val[M+1];
void init(){sz=0;memset(head,0,sizeof(head));}
void ae(int x,int v){
nxt[++sz]=head[x];val[sz]=v;head[x]=sz;
}
}nei;
int dfn[N+1],low[N+1],nowdfn;
int stk[N],top;
bool ins[N+1];
vector<vector<int> > scc;
int cid[N+1];
void dfs(int x){
dfn[x]=low[x]=++nowdfn;
ins[stk[top++]=x]=true;
for(int i=nei.head[x];i;i=nei.nxt[i]){
int y=nei.val[i];
if(!dfn[y])dfs(y),low[x]=min(low[x],low[y]);
else if(ins[y])low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]){
scc.pb(vector<int>());
while(true){
int y=stk[--top];
ins[y]=false;
scc.back().pb(y);cid[y]=scc.size()-1;
if(y==x)break;
}
}
}
addedge cnei;
int dp_l[N],dp_r[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++)read(a[i].x),read(a[i].r);
nei.init();
for(int i=1;i<=n;i++){//连边
while(top&&a[stk[top-1]].x+a[stk[top-1]].r<a[i].x)top--;
if(top)nei.ae(stk[top-1],i);
stk[top++]=i;
}
top=0;
for(int i=n;i;i--){//连边
while(top&&a[stk[top-1]].x-a[stk[top-1]].r>a[i].x)top--;
if(top)nei.ae(stk[top-1],i);
stk[top++]=i;
}
top=0;
for(int i=1;i<=n;i++)if(!dfn[i])dfs(i);//Tarjan
cnei.init();
for(int i=1;i<=n;i++)for(int j=nei.head[i];j;j=nei.nxt[j]){//缩点
int x=nei.val[j];
if(cid[i]!=cid[x])cnei.ae(cid[i],cid[x]);
}
int ans=0;
for(int i=0;i<scc.size();i++){//DP
dp_l[i]=inf;dp_r[i]=-inf;
vector<int> &v=scc[i];
// printf("scc#%d=",i);for(int j=0;j<v.size();j++)cout<<v[j]<<" ";puts("");
int sum=0;
for(int j=0;j<v.size();j++)
dp_l[i]=min(dp_l[i],v[j]),dp_r[i]=max(dp_r[i],v[j]),(sum+=v[j])%=mod;
for(int j=cnei.head[i];j;j=cnei.nxt[j]){
int x=cnei.val[j];
dp_l[i]=min(dp_l[i],dp_l[x]);dp_r[i]=max(dp_r[i],dp_r[x]);
}
// printf("dp=%d\n",dp[i]);
(ans+=1ll*(dp_r[i]-dp_l[i]+1)*sum%mod)%=mod;
}
cout<<ans;
return 0;
}