各种优化建图
I. (线段树)CF786B Legacy
太经典了。
看到区间想到线段树,在线段树上连边跑 Dijkstra 即可。
具体来说,建两棵树,一棵只连入边,另一棵只连出边。入边的树父节点向子节点连边(如果子节点向父节点连边,会导致本来只连向该区间的边通过子节点向父节点连的边连向了更大的区间),同理出边的子节点向父节点连边。父子节点之间的边,边权为 \(0\)。
时间复杂度 \(\mathcal{O}(n\log n\log(n\log n))\)。
CF786B
/*
Author : Alex_Wei
Problem : CF786B Legacy
Powered by C++11
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define pb push_back
#define pii pair <int,int>
#define pll pair <ll,ll>
#define fi first
#define se second
const int N=1e5+5;
int n,q,s,a[N],in[N],out[N];
int tp,p,ql,qr,w;
vector <pii> e[N<<3];
void build(int l,int r,int fa,int x){
if(x>1)e[fa].pb({x,0}),e[x+n*4].pb({fa+n*4,0});
if(l==r)return in[l]=x,out[l]=x+n*4,void();
build(l,l+r>>1,x,x<<1),build((l+r>>1)+1,r,x,x<<1|1);
} void con(int l,int r,int x){
if(ql<=l&&r<=qr){
if(tp==2)e[out[p]].pb({x,w});
else e[x+n*4].pb({in[p],w});
return;
} int m=l+r>>1;
if(ql<=m)con(l,m,x<<1);
if(m<qr)con(m+1,r,x<<1|1);
}
ll dis[N<<3],vis[N<<3];
priority_queue <pll,vector<pll>,greater<pll> > d;
void dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
dis[in[s]]=0,d.push({0,in[s]});
while(!d.empty()){
pll t=d.top(); d.pop();
ll id=t.se,ds=t.fi;
if(vis[id])continue;
vis[id]=1;
for(pii it:e[id])if(dis[it.fi]>ds+it.se)
dis[it.fi]=ds+it.se,d.push({dis[it.fi],it.fi});
}
}
int main(){
cin>>n>>q>>s,build(1,n,0,1);
for(int i=1;i<=q;i++){
cin>>tp;
if(tp==1)cin>>p>>ql>>w,e[out[p]].pb({in[ql],w});
else cin>>p>>ql>>qr>>w,con(1,n,1);
} for(int i=1;i<=n;i++)e[in[i]].pb({out[i],0}),e[out[i]].pb({in[i],0});
dijkstra(s);
for(int i=1;i<=n;i++)cout<<(dis[in[i]]<1e18?dis[in[i]]:-1)<<" ";
return 0;
}
*II. (后缀树)P5284 [十二省联考2019]字符串问题
这个是用后缀树优化建图,然后跑 DAG 上 DP。
SAM 例题 XI.(后缀树)
P5284
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using vint = vector <int>;
#define pb emplace_back
#define all(x) x.begin(),x.end()
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
const int N=4e5+5;
const int S=26;
const int inf=1e9+7;
// Segtree_Min
int deg[N],val[N<<2],laz[N<<2];
void up(int x){
val[x]=min(val[x<<1],val[x<<1|1]);
} void down(int x){
if(laz[x]){
val[x<<1]-=laz[x],val[x<<1|1]-=laz[x];
laz[x<<1]+=laz[x],laz[x<<1|1]+=laz[x];
} laz[x]=0;
} void build(int l,int r,int x){
laz[x]=val[x]=0;
if(l==r)return val[x]=deg[l],void();
int m=l+r>>1;
build(l,m,x<<1),build(m+1,r,x<<1|1),up(x);
} void modify(int l,int r,int ql,int qr,int x){
if(ql<=l&&r<=qr)return val[x]--,laz[x]++,void();
int m=l+r>>1; down(x);
if(ql<=m)modify(l,m,ql,qr,x<<1);
if(m<qr)modify(m+1,r,ql,qr,x<<1|1); up(x);
} int query(int l,int r,int x){
if(l==r)return val[x]=inf,l;
int m=l+r>>1,ans; down(x);
if(!val[x<<1])ans=query(l,m,x<<1);
else ans=query(m+1,r,x<<1|1);
return up(x),ans;
}
// SegTree_Max
ll ini[N<<2],val2[N<<2],laz2[N<<2];
void cmax(ll &x,ll y){
x=max(x,y);
} void up2(int x){
val2[x]=max(val2[x<<1],val2[x<<1|1]);
} void down2(int x){
if(laz[x]!=-1){
cmax(laz2[x<<1],laz2[x]),cmax(laz2[x<<1|1],laz2[x]);
cmax(val2[x<<1],laz2[x]),cmax(val2[x<<1|1],laz2[x]);
} laz2[x]=-1;
} void build2(int l,int r,int x){
val2[x]=laz2[x]=-1;
if(l==r)return val2[x]=ini[l],void();
int m=l+r>>1;
build2(l,m,x<<1),build2(m+1,r,x<<1|1),up2(x);
} void modify2(int l,int r,int ql,int qr,int x,ll v){
if(ql<=l&&r<=qr)return cmax(val2[x],v),cmax(laz2[x],v),void();
int m=l+r>>1; down2(x);
if(ql<=m)modify2(l,m,ql,qr,x<<1,v);
if(m<qr)modify2(m+1,r,ql,qr,x<<1|1,v); up2(x);
} ll query2(int l,int r,int p,int x){
if(l==r)return val2[x];
int m=l+r>>1; down2(x);
if(p<=m)return query2(l,m,p,x<<1);
return query2(m+1,r,p,x<<1|1);
}
// Suffix_Automaton
int n,K,cnt,las;
int fa[N],len[N],ed[N],son[N][S],ff[N][S];
vint FAIL[N];
void ins(char s){
int p=las,cur=++cnt,it=s-'a';
len[cur]=len[las]+1,ed[len[cur]]=cur,las=cur;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
mcpy(son[cl],son[q]);
while(son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(char *s){
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=2;i<=cnt;i++)FAIL[fa[i]].pb(i),ff[i][0]=fa[i];
K=log2(cnt);
for(int i=1;i<=K;i++)for(int j=1;j<=cnt;j++)ff[j][i]=ff[ff[j][i-1]][i-1];
} int getpos(int l,int r){
int p=ed[r];
for(int i=K;~i;i--)if(r-len[ff[p][i]]+1<=l)p=ff[p][i];
return p;
}
char s[N];
int na,nb,tot,m;
int dnum,lens[N],tmp[N],rev[N],id[N],sz[N];
vint DAG[N],tag[N];
bool cmp(int a,int b){
return lens[a]!=lens[b]?lens[a]<lens[b]:a>b;
} int dfs(int d){
int z=tag[d].size(),l=dnum+1,r=dnum+z;
sort(all(tag[d]),cmp);
for(int it:tag[d])id[it]=++dnum;
for(int it:FAIL[d])z+=dfs(it);
for(int i=l;i<=r;i++)sz[i]=z-(i-l);
return z;
}
void clear(){
// clear SAM
for(int i=1;i<=cnt;i++)mem(son[i],0),mem(ff[i],0),ed[i]=len[i]=fa[i]=0;
// clear Fail tree
for(int i=1;i<=cnt;i++)FAIL[i].clear(),tag[i].clear();
for(int i=1;i<=tot+1;i++)lens[i]=id[i]=sz[i]=deg[i]=0;
// clear DAG
for(int i=1;i<=na;i++)DAG[i].clear();
// clear variables
las=cnt=1,dnum=na=nb=tot=0;
} void init(){
scanf("%s%d",s+1,&na),n=strlen(s+1);
reverse(s+1,s+n+1),build(s);
for(int i=1;i<=na;i++){
int l,r; scanf("%d%d",&l,&r);
l=n-l+1,r=n-r+1,swap(l,r),lens[i]=r-l+1;
tag[getpos(l,r)].pb(i);
} scanf("%d",&nb),tot=na+nb;
for(int i=1;i<=nb;i++){
int l,r; scanf("%d%d",&l,&r);
l=n-l+1,r=n-r+1,swap(l,r),lens[i+na]=r-l+1;
tag[getpos(l,r)].pb(i+na);
} scanf("%d",&m);
for(int i=1;i<=m;i++){
int x,y; scanf("%d%d",&x,&y);
DAG[x].pb(y+na);
} dfs(1);
for(int i=1;i<=tot;i++)tmp[id[i]]=lens[i];
for(int i=1;i<=tot;i++)lens[i]=tmp[i],rev[id[i]]=i;
}
queue <int> q;
bool update(){
if(val[1])return 0;
int p=query(1,tot,1);
return ini[p]=0,q.push(p),1;
} bool calc_deg(){
for(int i=1;i<=na;i++)
for(int it:DAG[i]){
int l=id[it],r=l+sz[l]-1;
if(l<=id[i]&&id[i]<=r)return 1;
deg[l]++,deg[r+1]--;
}
for(int i=1;i<=tot;i++)deg[i]+=deg[i-1];
for(int i=na+1;i<=tot;i++)deg[id[i]]=inf;
return build(1,tot,1),0;
} ll topo(){
for(int i=1;i<=tot;i++)ini[i]=-1;
while(update()); build2(1,tot,1);
ll ans=0;
while(!q.empty()){
ll t=q.front(),v=query2(1,tot,t,1)+lens[t]; q.pop();
cmax(ans,v);
for(int it:DAG[rev[t]]){
int l=id[it],r=l+sz[l]-1;
modify(1,tot,l,r,1),modify2(1,tot,l,r,1,v);
while(update());
}
} return val[1]<1e6?-1:ans;
}
void solve(){
clear(),init();
if(calc_deg())return puts("-1"),void();
cout<<topo()<<endl;
} int main(){
int t; cin>>t;
while(t--)solve();
return 0;
}
III.(倍增 or ST 表 + 虚点)P5344 【XR-1】逛森林
区间向区间连边,可以转化为区间向虚点连边,再由虚点向区间连边。
倍增优化建图,与线段树优化建图非常相似,只不过放到了树上。
同线段树优化建图一样,需要建出两个 “倍增树”,一条管理出边,一条管理入边。
总时间复杂度 \(\mathcal{O}(m\log n\log(m\log n))\)。
可以将树通过 dfs 序拍平到序列上,并使用 ST 表维护连边。具体地,可以将 \([l,r]\) 拆为 \([l,l+2^k)\) 与 \((r-2^k,r]\),其中 \(k\) 为 \(\lfloor \log_2(r-l+1)\rfloor\),因为重复连边不影响最终的答案。 这样一来时间复杂度优化成了 \(\mathcal{O}(m\log m)\),非常优秀。
P5344
/*
Author : Alex_Wei
Problem : P5344 【XR-1】逛森林
Powered by C++11
*/
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair <int,int>
#define fi first
#define se second
#define gc getchar()
const int N=5e4+5;
const int K=16;
const int M=1e6+5;
const int T=N*K*2+M;
inline int read(){
int x=0; char s=gc;
while(!isdigit(s))s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+s-'0',s=gc;
return x;
}
int f[N];
int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
bool same(int u,int v){return find(u)==find(v);}
bool merge(int u,int v){
u=find(u),v=find(v);
return f[u]=v,u!=v;
}
struct operation{
int u1,v1,u2,v2,w;
void rd(){u1=read(),v1=read(),u2=read(),v2=read(),w=read();}
}p[M];
int n,m,s,k,cnt,node;
vector <pii> e[T];
int vis[N],dep[N],fa[N][K],in[N][K],out[N][K];
void dfs(int x,int f,int d){
vis[x]=1,fa[x][0]=f,in[x][0]=x,out[x][0]=x+n,dep[x]=d;
for(pii it:e[x])if(it.fi!=f)dfs(it.fi,x,d+1);
} void con(int u,int v,int p,int w,int tp){
if(dep[u]<dep[v])swap(u,v);
for(int i=k;~i;i--)if(dep[fa[u][i]]>=dep[v]){
if(tp)e[out[u][i]].pb({p,w});
else e[p].pb({in[u][i],w});
u=fa[u][i];
} for(int i=k;~i;i--)if(fa[u][i]!=fa[v][i]){
if(tp)e[out[u][i]].pb({p,w}),e[out[v][i]].pb({p,w});
else e[p].pb({in[u][i],w}),e[p].pb({in[v][i],w});
u=fa[u][i],v=fa[v][i];
} if(u!=v){
if(tp)e[out[u][0]].pb({p,w}),e[out[v][0]].pb({p,w});
else e[p].pb({in[u][0],w}),e[p].pb({in[v][0],w});
u=fa[u][0],v=fa[v][0];
} if(tp)e[out[u][0]].pb({p,w});
else e[p].pb({in[u][0],w});
}
int dis[T];
priority_queue <pii,vector<pii>,greater<pii> >q;
void dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
dis[s]=0,q.push({0,s});
while(!q.empty()){
pii t=q.top(); q.pop();
int id=t.se,ds=t.fi;
if(dis[id]<ds)continue;
for(pii it:e[id])if(dis[it.fi]>ds+it.se)
q.push({dis[it.fi]=ds+it.se,it.fi});
}
}
int main(){
cin>>n>>m>>s,node=n<<1,k=log2(n);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++){
int tp=read();
if(tp==1){
operation t; t.rd();
if(same(t.u1,t.v1)&&same(t.u2,t.v2))p[++cnt]=t;
} else{
int u=read(),v=read(),w=read();
if(merge(u,v))e[u].pb({v,w}),e[v].pb({u,w});
}
} for(int i=1;i<=n;i++)if(!vis[i])dfs(i,0,1);
for(int i=1;i<=n;i++)e[i].pb({i+n,0}),e[i+n].pb({i,0});
for(int i=1;i<=k;i++)for(int j=1;j<=n;j++)
in[j][i]=++node,
e[node].pb({in[j][i-1],0}),e[node].pb({in[fa[j][i-1]][i-1],0}),
out[j][i]=++node,
e[out[j][i-1]].pb({node,0}),e[out[fa[j][i-1]][i-1]].pb({node,0}),
fa[j][i]=fa[fa[j][i-1]][i-1];
for(int i=1;i<=cnt;i++)
con(p[i].u1,p[i].v1,++node,p[i].w,1),
con(p[i].u2,p[i].v2,node,0,0);
dijkstra(s);
for(int i=1;i<=n;i++)cout<<(dis[i]<1e8?dis[i]:-1)<<" ";
return 0;
}
IV.(树套树)P5471 [NOI2019] 弹跳
NOI 怎么出裸题?
好吧看起来并不裸。
二维上的区间用树套树来解决,但是树套树被卡空间了(悲),而且时间复杂度好像是 \(\mathcal{O}(m\log^3 n)\) 的,难搞。
看了题解发现可以 线段树 + set 解决:对于每个线段树的节点 \([l,r]\),用 set 维护所有横坐标落在 \([l,r]\) 的城市(即 \(S_{l,r}=\{i|i\in[1,n]\land x_i\in[l,r]\}\))。
为了节省空间,不建图,而是把每个能扩展的矩形放到优先队列里面,每次取出距离最小的,并到线段树 + set 上面找到相应的点并删除(因为取出来的距离就是最小的,所以这个点以后一定不会再被扩展到了),删除前将该点能扩展的矩形的 \(t\) 更新成 \(dis+t\) 丢到优先队列里面就行。
这样一来就是个裸的 dijkstra,代码也很好写。
时间复杂度分析:每个点在线段树上最多出现 \(\log n\) 次,所以扩展的矩形最多进入优先队列 \(m\log n\) 次,\(\mathcal{O}(m\log n\log(m\log n))\)。
线段树 + set 也可以算是树套树了吧。/doge
P5471
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair <int,int>
#define fi first
#define se second
const int N=7e4+5;
int n,m,w,h,xx[N],yy[N],d;
set <pii> s[N<<2];
void modify(int l,int r,int x){
s[x].insert({yy[d],d});
if(l==r)return;
int m=l+r>>1;
if(xx[d]<=m)modify(l,m,x<<1);
else modify(m+1,r,x<<1|1);
}
struct jump{
int t,l,r,d,u;
bool operator < (const jump &v) const {
return t>v.t;
}
}; vector <jump> e[N];
int T,ql,qr,qd,qu,dis[N];
priority_queue <jump> q;
void erase(int l,int r,int x){
if(ql<=l&&r<=qr){
auto pr=s[x].lower_bound({qd,0}),sf=s[x].lower_bound({qu,N});
for(auto it=pr;it!=sf;){
int id=(*it++).se;
if(dis[id]>T){
dis[id]=T;
for(auto i:e[id])i.t+=T,q.push(i);
}
} return s[x].erase(pr,sf),void();
} int m=l+r>>1;
if(ql<=m)erase(l,m,x<<1);
if(m<qr)erase(m+1,r,x<<1|1);
} void dijkstra(int s){
memset(dis,0x3f,sizeof(dis)),dis[s]=0;
for(auto it:e[s])q.push(it);
while(!q.empty()){
jump t=q.top(); q.pop();
T=t.t,ql=t.l,qr=t.r,qd=t.d,qu=t.u,erase(1,w,1);
}
}
int main(){
cin>>n>>m>>w>>h;
for(int i=1;i<=n;i++)cin>>xx[i]>>yy[i],d=i,modify(1,w,1);
for(int i=1;i<=m;i++){
int p,t,l,r,d,u; cin>>p>>t>>l>>r>>d>>u;
e[p].pb({t,l,r,d,u});
} dijkstra(1);
for(int i=2;i<=n;i++)cout<<dis[i]<<endl;
return 0;
}
*V.(线段树 + 虚点)P1983 [NOIP2013 普及组] 车站分级
以小见大。
解法 1(暴力):对于一条线路 \(t_1\to t_2\to \cdots \to t_s\),将所有编号在该线路之间却没有经过的站点 \(p\)(\(p\in[t_1,t_s]\) 且 \(p\notin \{t_i\}\) 向所有经过的站点 \(t_i\) 连一条边 \(p\to t_i\) 表示 \(p\) 的编号一定小于 \(t_i\),然后跑拓扑排序即可。一次连边 \(\mathcal{O}(n^2)\),总时间复杂度 \(\mathcal{O}(n^2m)\)。可以通过(常数很小)。
解法 2(虚点):根据例题 III. 可知区间向区间连边可以用虚点优化。需要注意最终答案要除以 \(2\),因为点到虚点再到点的长度为 \(2\),而实际上应为 \(1\)。时间复杂度 \(\mathcal{O}(nm)\)。
P1983
#include <bits/stdc++.h>
using namespace std;
const int N=1e3+5;
int n,m,node,t[N],buc[N<<1],deg[N<<1],f[N<<1];
vector <int> e[N<<1];
int main(){
cin>>n>>m,node=n;
for(int i=1,s;i<=m;i++){
memset(buc,0,sizeof(buc)),cin>>s;
for(int i=1;i<=s;i++)cin>>t[i],buc[t[i]]=1;
if(t[s]-t[1]+1==s)continue;
node++;
for(int i=t[1];i<=t[s];i++)
if(buc[i])e[node].push_back(i),deg[i]++;
else e[i].push_back(node),deg[node]++;
} queue <int> q;
for(int i=1;i<=node;i++)if(!deg[i])q.push(i);
while(!q.empty()){
int t=q.front(); q.pop();
for(int it:e[t]){
f[it]=max(f[it],f[t]+1);
if(!--deg[it])q.push(it);
}
} int ans=0;
for(int i=1;i<=node;i++)ans=max(ans,f[i]);
cout<<ans/2+1<<endl;
return 0;
}
解法 3(线段树 + 虚点):点与区间的连边用线段树优化即可。最后跑拓扑也在线段树上跑。时间复杂度 \(\mathcal{O}(m\log n)\)。