我们从一道例题开始。
CF786B
Description
Solution
朴素解法: 暴力连边+最短路
对于每次连边操作,我们逐一连边,最后在图上跑一遍单源最短路径算法即可。
时间复杂度 O ( n 2 log ( n 2 ) ) O(n^2 \log (n^2)) O(n2log(n2))。
正解: 线段树优化建图
线段树有一个非常优美的性质: 区间 [ l , r ] [l,r] [l,r]可以被映射成线段树上的许多连续的区间,且这些区间的数量不超过 ⌈ log n ⌉ \lceil \log n \rceil ⌈logn⌉。
我们要巧妙运用这个性质——我们是否可以将每一个连边的区间 [ l , r ] [l,r] [l,r]映射到线段树上的 log \log log个节点,然后只向这 log \log log个节点连边呢?
答案是可以的。我们建立两棵线段树,一个线段树往内连边(简称为入树),另一个线段树往外连边(简称为出树)。每棵树的叶节点对应图中一个的真实节点。同时,两棵树中对应的叶节点连一条边权为 0 0 0的有向边(即下图中五彩缤纷的那些边)。同一棵树中的父节点与孩子节点也要连一条边权为 0 0 0的有向边。
对于每次连边操作,我们只向
log
\log
log个节点连一条有向边,边权为
w
w
w。
上图表示一个形如“从
1
1
1号节点向区间
[
3
,
8
]
[3,8]
[3,8]中的点分别连一条边”的第二类操作。第三类操作同理。第一类操作直接将对应的叶节点连边。
最后我们跑一遍单源最短路(Dijkstra)即可。
注意,这里的最短路的“源”是出树中表示区间 [ 1 , 1 ] [1,1] [1,1]的叶节点。
由于边数为 O ( n log n ) O(n \log n) O(nlogn)级别,所以总时间复杂度为 O ( n log n log ( n log n ) ) ≈ O ( n l o g 2 n ) O(n \log n \log (n \log n))≈O(n\ log^2 n) O(nlognlog(nlogn))≈O(n log2n)。
Code
#include <bits/stdc++.h>
#define int long long
#define inf 200000000000007
using namespace std;
const int maxl=100005,maxg=20;
int read(){
int s=0,w=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') w=-w;ch=getchar();}
while (ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^'0');ch=getchar();}
return s*w;
}
int n,m,s,blo,opt,u,l,r,w,cnt=0;
int head[8*maxl],itree[4*maxl],otree[4*maxl],pos[maxl];
int dis[8*maxl],vis[8*maxl];
struct edge{int nxt,to,dis;}e[4*maxg*maxl];//边数可能较多
struct node{
int dis,pos;
bool operator < (const node &x) const{return x.dis<dis;}
};
void add_edge(int u,int v,int w){
cnt++;
e[cnt].to=v,e[cnt].dis=w,e[cnt].nxt=head[u];
head[u]=cnt;
}
void build_itree(int l,int r,int rt){
if (l==r){
pos[l]=rt;
add_edge(rt+blo,rt,0),add_edge(rt,rt+blo,0);
return;
}
int mid=(l+r)>>1;
add_edge(rt,2*rt,0),build_itree(l,mid,2*rt);
add_edge(rt,2*rt+1,0),build_itree(mid+1,r,2*rt+1);
}
void build_otree(int l,int r,int rt){
if (rt!=1) add_edge(blo+rt,blo+(rt/2),0);
if (l==r) return;
int mid=(l+r)>>1;
build_otree(l,mid,2*rt);
build_otree(mid+1,r,2*rt+1);
}
void tree_edge(int nl,int nr,int l,int r,int rt,int link_pos,int ww,int k){
if (nl<=l&&r<=nr){
if (k==0) add_edge(link_pos+blo,rt,ww);
else add_edge(rt+blo,link_pos,ww);
return;
}
int mid=(l+r)>>1;
if (nl<=mid) tree_edge(nl,nr,l,mid,2*rt,link_pos,ww,k);
if (nr>mid) tree_edge(nl,nr,mid+1,r,2*rt+1,link_pos,ww,k);
}
std::priority_queue<node> q;
void dijkstra(){
q.push((node){0,s});
dis[s]=0;
while (!q.empty()){
int now=q.top().pos;
q.pop();
if (vis[now]) continue;
vis[now]=1;
for (int i=head[now];i;i=e[i].nxt){
int y=e[i].to;
if (dis[y]>dis[now]+e[i].dis){
dis[y]=dis[now]+e[i].dis;
if (!vis[y]) q.push((node){dis[y],y});
}
}
}
}
signed main(){
n=read(),m=read(),s=read();blo=4*n;
build_itree(1,n,1);
build_otree(1,n,1);//给树中的每个节点一个统一的编号
for (int i=1;i<=m;i++){
opt=read(),u=read();
if (opt==1){
l=read(),w=read();
tree_edge(l,l,1,n,1,pos[u],w,0);
}
else if (opt==2){
l=read(),r=read(),w=read();
tree_edge(l,r,1,n,1,pos[u],w,0);
}
else if (opt==3){
l=read(),r=read(),w=read();
tree_edge(l,r,1,n,1,pos[u],w,1);
}
}
s=blo+pos[s];
for (int i=1;i<=8*n;i++) dis[i]=inf;
dijkstra();
for (int i=1;i<=n;i++){
if (dis[pos[i]]>=inf) dis[pos[i]]=-1;
}
for (int i=1;i<=n;i++) printf("%lld ",dis[pos[i]]);
return 0;
}
一个小扩展: 第四类操作
第四类操作是区间向区间连边,即 [ l 1 , r 1 ] [l_1,r_1] [l1,r1]中的每个节点向 [ l 2 , r 2 ] [l_2,r_2] [l2,r2]中的每个节点连一条边权为 w w w的边。
对于这一种新的操作我们该怎么办呢?不难想到,我们可以新建一个虚点 p p p,然后就变成了“ [ l 1 , r 1 ] [l_1,r_1] [l1,r1]向 p p p连边”与“ p p p向 [ l 2 , r 2 ] [l_2,r_2] [l2,r2]连边”,分别处理即可。
显然第四类操作并没有对时间复杂度有太大的影响(就是常数变大了好多……),依然是 O ( n log 2 n ) O(n \log^2 n) O(nlog2n)。
例题: P5025
Description
Solution
算法一: 套路,朴素,时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n 2 32 ) O(\frac {n^2} {32}) O(32n2)
对于两个能够互相引爆的节点我们连一条边。
显然,所有第 i i i个炸弹能够引爆的炸弹就是所有从 i i i出发能够到达的炸弹。这是一个可达性统计问题。我们采用 b i t s e t bitset bitset去转移即可。
算法二: 连边的性质与线段树优化建图
不难发现,第 i i i个节点连向的所有节点一定在一个连续的区间 [ L , R ] [L,R] [L,R]内。
于是,我们可以对于每一个 i i i二分出 L L L和 R R R然后线段树优化建图即可。最后再跑一遍可达性统计。
时间复杂度与空间复杂度不变。
算法三: 小性质得到正解
考虑第 i i i个节点可达的节点映射下来一定是一个区间。
所以我们并不需要 b i t s e t bitset bitset这种大空间&大时间复杂度写法,我们只需要求出从第 i i i个节点能够到达的区间的左右端点即可,区间长度即为可达的炸弹数量。这可以通过缩点+ D A G DAG DAG上 D P DP DP求出。
时间复杂度被我们优化成了 O ( n log n ) O(n \log n) O(nlogn)。
Code
#include <bits/stdc++.h>
#define ll long long
#define inf 2000000007
using namespace std;
const int maxl=500005,maxt=1500005,maxg=19,mod=1e9+7;
ll read(){
ll s=0,w=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') w=-w;ch=getchar();}
while (ch>='0'&&ch<='9'){s=(s<<1ll)+(s<<3ll)+(ch^'0');ch=getchar();}
return s*w;
}
int n,tot,blo,len,cnt,nt;ll ans=0;
int head[maxt],tree[maxl*2][2],dfn[maxt],low[maxt],s[maxt];
int fa[maxt],lm[maxt],rm[maxt],inde[maxt];
ll a[maxl],b[maxl];
bitset<maxt> vis,is_fa;
queue<int> q;
map<pair<int,int>,bool> ma;
struct node{int x,y;}edge_lis[2*maxl+maxl*maxg];
struct edge{int nxt,to;}e[2*maxl+maxl*maxg];
void add_edge(int u,int v){
cnt++;
e[cnt].to=v,e[cnt].nxt=head[u],head[u]=cnt;
}
void clear_edges(){
cnt=0;
for (int i=1;i<=3*n;i++) head[i]=0;
}
int build_tree(int l,int r,int rt){
rt=++tot;
if (l==r){
add_edge(rt,2*n+l);
add_edge(2*n+l,rt);
return rt;
}
int mid=(l+r)>>1;
tree[rt][0]=build_tree(l,mid,0);
tree[rt][1]=build_tree(mid+1,r,0);
add_edge(rt,tree[rt][0]);
add_edge(rt,tree[rt][1]);
return rt;
}
void tree_edge(int nl,int nr,int l,int r,int rt,int k){
if (nl<=l&&r<=nr){
add_edge(k,rt);
return;
}
int mid=(l+r)>>1;
if (nl<=mid) tree_edge(nl,nr,l,mid,tree[rt][0],k);
if (nr>mid) tree_edge(nl,nr,mid+1,r,tree[rt][1],k);
}
void tarjan(int now){
dfn[now]=low[now]=++nt;
s[++len]=now,vis[now]=1;
for (int i=head[now];i;i=e[i].nxt){
int y=e[i].to;
if (!dfn[y]){
tarjan(y);
low[now]=min(low[now],low[y]);
}
else if (vis[y]) low[now]=min(low[now],dfn[y]);
}
if (dfn[now]==low[now]){
int y;
while (y=s[len]){
fa[y]=now,vis[y]=0;
len--;
if (y>2*n){
lm[now]=min(lm[now],y-2*n);
rm[now]=max(rm[now],y-2*n);
}
if (y==now) break;
}
}
}
signed main(){
n=read();
build_tree(1,n,0);
for (int i=1;i<=n;i++) a[i]=read(),b[i]=read();
for (int i=1;i<=n;i++){
int p=lower_bound(a+1,a+n+1,a[i]-b[i])-a;
int p2=upper_bound(a+1,a+n+1,a[i]+b[i])-a-1;
if (p<p2&&p>=1&&p2<=n) tree_edge(p,p2,1,n,1,i+2*n);
}
for (int i=1;i<=3*n;i++) lm[i]=inf,rm[i]=0;
tarjan(1);
nt=0;
for (int i=1;i<=3*n;i++) is_fa[fa[i]]=1;
for (int now=1;now<=3*n;now++){
for (int i=head[now];i;i=e[i].nxt){
int y=e[i].to;
if (fa[now]!=fa[y]){
edge_lis[++nt].x=fa[y];
edge_lis[nt].y=fa[now];
}
}
}
clear_edges();
for (int i=1;i<=nt;i++){
int fx=edge_lis[i].x,fy=edge_lis[i].y;
if (!ma[make_pair(fx,fy)]){
add_edge(fx,fy);inde[fy]++;
ma[make_pair(fx,fy)]=ma[make_pair(fy,fx)]=1;
}
}
for (int i=1;i<=3*n;i++){
if (is_fa[i]&&(!inde[i])) q.push(i);
}
while (!q.empty()){
int now=q.front();
q.pop();
for (int i=head[now];i;i=e[i].nxt){
int y=e[i].to;
inde[y]--;
lm[y]=min(lm[y],lm[now]);
rm[y]=max(rm[y],rm[now]);
if (!inde[y]) q.push(y);
}
}
for (int i=1;i<=n;i++)
ans=(ans+1ll*i*(rm[fa[i+2*n]]-lm[fa[i+2*n]]+1ll)%mod)%mod;
cout<<ans<<endl;
return 0;
}