好题,神题。
题目大意:
一个国家有 $n$ 个城市,$m$ 条有向道路组成。在这个国家一个星期有 $d$ 天,每个城市有一个博物馆。
有个旅行团在城市 $1$ 出发,当天是星期一。每天早上,如果这个城市的博物馆开了,那么可以去这个博物馆参观。每天晚上,旅行团可以选择沿一条出边前往下一个城市,或者结束旅行。一个城市可以经过多次。
请问旅行团最多能参观多少个博物馆。一个博物馆参观了多次,只计算一次。
$1le n,mle 10^5,1le dle 50$。
根据题解,先说做法:
首先拆成 $n imes d$ 个点,点 $(i,j)$ 表示当前在城市 $i$,是星期 $j$。对于原图的边 $u ightarrow v$,连边 $(u,i) ightarrow (v,imod d+1)$。
对这个图求强连通分量,统计每个强连通分量里面有多少个不同的博物馆。
在缩点后的图中,找一条从包含 $1$ 的点开始的最长链(点权是不同博物馆个数),即为答案。
为什么呢?
首先当我们进入一个强连通分量时,肯定可以把里面所有博物馆游览完。然后也一定可以走到下一个强连通分量。
然后为什么答案不会被重复计算呢?不会出现博物馆 $u$ 在这条链里出现多次的情况吗?
我们发现,如果从 $(u,i)$ 能走到 $(u,j)$,那么从 $(u,j)$ 也能走到 $(u,i)$。
因为这代表在原图上可以走一条长 $(j-i)mod d$ 的链从 $u$ 走回到 $u$。现在是星期 $j$ 时,只需要走这条链 $d-1$ 次就能到星期 $i$。
那么如果从 $(u,i)$ 能走到 $(u,j)$,两点一定属于一个强连通分量。在统计强连通分量内的答案时可以判掉。于是便不可能有重复统计的情况。
那么这题就做完了……吗?
如果用DFS式的tarjan,那么递归深度可以到 $n imes d$ 即 $5 imes 10^6$,爆栈无疑。
虽然可以用pragma指令或者各种其他方式开栈内存,但……假如这是OI考场上呢?
那么就需要用手写栈模拟tarjan过程。具体写着会比较难受,我在代码里写好了注释。
当然也可能是我写复杂了,我的代码真的巨慢无比
时间复杂度 $O((n+m) imes d)$。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=5000500; #define FOR(i,a,b) for(int i=(a);i<=(b);i++) #define ROF(i,a,b) for(int i=(a);i>=(b);i--) #define MEM(x,v) memset(x,v,sizeof(x)) inline int read(){ char ch=getchar();int x=0,f=0; while(ch<'0' || ch>'9') f|=ch=='-',ch=getchar(); while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar(); return f?-x:x; } inline ll getc(){ char ch=getchar(); while(ch<'0' || ch>'1') ch=getchar(); return ch-'0'; } int n,m,d,head[maxn],el,to[maxn],nxt[maxn],head2[maxn],el2,to2[maxn],nxt2[maxn]; int dfn[maxn],low[maxn],dfs_clock,stk[maxn],tp,id[maxn],scc,cnt[maxn],tmp[maxn],tl,dp[maxn]; int recstk[maxn],rectp,cur[maxn]; ll vld[100010]; bool vis[maxn],ins[maxn],fst[maxn],was[maxn]; inline void add(int u,int v){ to[++el]=v;nxt[el]=head[u];head[u]=el; } inline void add2(int u,int v){ to2[++el2]=v;nxt2[el2]=head2[u];head2[u]=el2; } void dfs(int s){ recstk[rectp=1]=s; //recstk模拟系统函数栈 cur[s]=head[s]; //cur[u]表示当前u的边遍历到哪一条 while(rectp){ int u=recstk[rectp]; if(fst[u]){ //fst[u]表示不是第一次入栈(那么就遍历过边) if(was[u]) low[u]=min(low[u],low[to[cur[u]]]),was[u]=false; //was[u]表示u上一条遍历到的边是否满足!dfn[v] //此时就要low[u]=min(low[u],low[v]) //不过由于模拟栈中不能方便回溯,就这么写了 cur[u]=nxt[cur[u]]; //向后推一条边 } else{ //u第一次进栈 fst[u]=true; dfn[u]=low[u]=++dfs_clock; ins[u]=true; stk[++tp]=u; } if(!cur[u]){ //边遍历完了 if(low[u]==dfn[u]){ scc++;tl=0; //强连通编号++ do{ ins[stk[tp]]=false; id[stk[tp]]=scc; int x=(stk[tp]+d-1)/d,y=stk[tp]%d?stk[tp]%d:d; //这个点是(x,y) if(!((vld[x]>>y)&1)) continue; //(x,y)博物馆不开,不管 tmp[++tl]=x; if(!vis[x]) vis[x]=true,cnt[scc]++; //如果博物馆x之前没访问过,那么新计入答案 }while(stk[tp--]!=u); FOR(i,1,tl) vis[tmp[i]]=0; //vis清空 } rectp--; //u退栈(相当于回溯) } else{ int i=cur[u],v=to[i]; if(!dfn[v]){ recstk[++rectp]=v; //v进栈(相当于递归) cur[v]=head[v]; //v当前遍历的边是head[v] was[u]=true; //u这条边满足!dfn[v] } else if(ins[v]) low[u]=min(low[u],dfn[v]); } } } int main(){ n=read();m=read();d=read(); FOR(i,1,m){ int u=read(),v=read(); FOR(j,1,d) add((u-1)*d+j,(v-1)*d+j%d+1); } FOR(i,1,n) FOR(j,1,d) vld[i]|=getc()<<j; //干脆压下空间……(一开始以为会MLE) FOR(i,1,n*d) if(!dfn[i]) dfs(i); FOR(u,1,n*d) for(int i=head[u];i;i=nxt[i]){ int v=to[i]; if(id[u]!=id[v]) add2(id[u],id[v]); //新图连边 } //根据tarjan的性质, 强连通编号为反向的拓扑序,所以可以从1到n直接遍历一遍 FOR(u,1,scc){ for(int i=head2[u];i;i=nxt2[i]) dp[u]=max(dp[u],dp[to2[i]]); dp[u]+=cnt[u]; } printf("%d ",dp[id[1]]); }
upd:貌似可以把tarjan变成inline就没有栈的问题了……
(代码好写N倍……)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=5000500; #define FOR(i,a,b) for(int i=(a);i<=(b);i++) #define ROF(i,a,b) for(int i=(a);i>=(b);i--) #define MEM(x,v) memset(x,v,sizeof(x)) inline int read(){ char ch=getchar();int x=0,f=0; while(ch<'0' || ch>'9') f|=ch=='-',ch=getchar(); while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar(); return f?-x:x; } inline ll getc(){ char ch=getchar(); while(ch<'0' || ch>'1') ch=getchar(); return ch-'0'; } int n,m,d,head[maxn],el,to[maxn],nxt[maxn],head2[maxn],el2,to2[maxn],nxt2[maxn]; int dfn[maxn],low[maxn],dfs_clock,stk[maxn],tp,id[maxn],scc,cnt[maxn],tmp[maxn],tl,dp[maxn]; ll vld[100010]; bool vis[maxn],ins[maxn]; inline void add(int u,int v){ to[++el]=v;nxt[el]=head[u];head[u]=el; } inline void add2(int u,int v){ to2[++el2]=v;nxt2[el2]=head2[u];head2[u]=el2; } inline void dfs(int u){ dfn[u]=low[u]=++dfs_clock; ins[stk[++tp]=u]=true; for(int i=head[u];i;i=nxt[i]){ int v=to[i]; if(!dfn[v]) dfs(v),low[u]=min(low[u],low[v]); else if(ins[v]) low[u]=min(low[u],dfn[v]); } if(dfn[u]==low[u]){ scc++;tl=0; //强连通编号++ do{ ins[stk[tp]]=false; id[stk[tp]]=scc; int x=(stk[tp]+d-1)/d,y=stk[tp]%d?stk[tp]%d:d; //这个点是(x,y) if(!((vld[x]>>y)&1)) continue; //(x,y)博物馆不开,不管 tmp[++tl]=x; if(!vis[x]) vis[x]=true,cnt[scc]++; //如果博物馆x之前没访问过,那么新计入答案 }while(stk[tp--]!=u); FOR(i,1,tl) vis[tmp[i]]=0; //vis清空 } } int main(){ n=read();m=read();d=read(); FOR(i,1,m){ int u=read(),v=read(); FOR(j,1,d) add((u-1)*d+j,(v-1)*d+j%d+1); } FOR(i,1,n) FOR(j,1,d) vld[i]|=getc()<<j; //干脆压下空间……(一开始以为会MLE) FOR(i,1,n*d) if(!dfn[i]) dfs(i); FOR(u,1,n*d) for(int i=head[u];i;i=nxt[i]){ int v=to[i]; if(id[u]!=id[v]) add2(id[u],id[v]); //新图连边 } //根据tarjan的性质, 强连通编号为反向的拓扑序,所以可以从1到n直接遍历一遍 FOR(u,1,scc){ for(int i=head2[u];i;i=nxt2[i]) dp[u]=max(dp[u],dp[to2[i]]); dp[u]+=cnt[u]; } printf("%d ",dp[id[1]]); }