ICPC 2019 World Final H. Hobsons' trains


题目链接
对于有向图,首先考虑\(bfs\)拓扑,然后就剩下一些环。由于题目中每个点只有一条出边,故剩下的是一些简单环,所以整个图就可以化为一些有向基环树(森林)。

然后考虑计算答案,对于环上的点,考虑该基环树内每个点都可以贡献环上的一个区间,于是直接差分后做前缀和即可。对于树上的点,答案即为子树内与自身距离不超过\(k\)的点的个数。比较直观的做法是线段树合并(本来以为可以长剖,然而要算前缀和),但感觉空间开不下,VP时糊了一个按照\(dfs\)序和深度两个偏序用主席树的做法,写得乱七八糟的之后才调过。。。(其实时空复杂度都要\(O(nlogn)\),依然很不优美)

对于树上的点,我们不考虑直接计算其子树内的答案(限制相对比较复杂);而是仍然像计算环上的点那样,考虑每个点的贡献。那么会发现每个点贡献到往上的长度为\(k\)的链上,这就比直接计算子树答案要简单得多。

这时候可以直接树剖,但利用树上差分会更简洁。对链顶的父亲\(-1\),链底\(+1\),然后查询每个点的子树和即为答案,用dfs序离线查询区间和即可,时空复杂度都是\(O(n)\)的。

所以其实按照环上的点的思路计算树上的点就很简单了,原本还想了各种数据结构,还是VP的时候降智了吧。

一些错误点
1.有向图区分树边和环边只需要\(bfs\)拓扑,不需要\(tarjan\)(即使写了\(tarjan\)也要记得访问完一个点要退栈!)
2.注意是有向图,环也是有向的!

带log做法

#include
using namespace std;
const int N=5e5+5;
int n,k,fa[N],du[N];
bool is[N];
int hd[N],to[N<<1],nx[N<<1],tt_edge;
void add(int u,int v){
	nx[++tt_edge]=hd[u];
	to[hd[u]=tt_edge]=v;
	du[u]++;
}
queueq;
void bfs(){
	for(int i=1;i<=n;i++) if(!du[i]) is[i]=1,q.push(i);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		int v=fa[u];
		du[v]--;
		if(!du[v]) is[v]=1,q.push(v);
	}
}
int dn[N],sz[N],dp[N],rt[N],lc[N*20],rc[N*20],su[N*20],tt,ct,Rt;
//consider carefully about the apporopriate size__(RE,MLE)
#define mid ((l+r)>>1)
int cnt(int u,int l,int r,int x){
	if(x>=r) return su[u];
	if(xmid) sm+=cnt(rc[u],mid+1,r,x);
	return sm;
}
int bld(int v,int l,int r,int x){
	int u=++ct;
	lc[u]=lc[v];
	rc[u]=rc[v];
	su[u]=su[v]+1;
	if(l==r) return u;
	if(x<=mid) lc[u]=bld(lc[v],l,mid,x);
	else rc[u]=bld(rc[v],mid+1,r,x);
	return u;
}
int ans[N];
void dfs(int u){
	dn[u]=++tt; sz[u]=1;
	rt[tt]=bld(rt[tt-1],0,n,dp[u]);
	for(int e=hd[u];e;e=nx[e]){
		int v=to[e];
		if(!is[v]) continue;
		dp[v]=dp[u]+1;
		dfs(v);
		sz[u]+=sz[v];
	}
	if(u!=Rt) ans[u]+=cnt(rt[dn[u]+sz[u]-1],0,n,min(dp[u]+k,n))-cnt(rt[dn[u]-1],0,n,min(dp[u]+k,n));
}
int p[N],cu[N];
int main()
{
	cin>>n>>k;
	for(int i=1;i<=n;i++) scanf("%d",&fa[i]),add(fa[i],i);
	bfs();
	/*
	!!!You can find circles in a directed graph by just bfs!!!
	!!!Even if you use tarjan,the stack should just store the nodes from the root to u,
		whilch means you should pop u when you finish visiting it!!!
	*/
	for(int i=1;i<=n;i++) if(!is[i]){
		int m=0;
		p[++m]=i;
		for(int j=fa[i];j!=i;j=fa[j]) p[++m]=j;
		for(int j=1;j<=m;j++){
			int u=p[j];
			tt=0; Rt=u;
			dfs(u);
			for(int i2=0;i2<=min(k,sz[u]);i2++){
				int x;
				x=cnt(rt[sz[u]],0,n,i2)-cnt(rt[sz[u]],0,n,i2-1);
				int t=k-i2;
				//remember that the circle is directed!!!
				if(t>=m-1){
					cu[1]+=x;
					continue;
				}
				if(j+t<=m) cu[j]+=x,cu[j+t+1]-=x;
				else cu[j]+=x,cu[1]+=x,cu[j+t-m+1]-=x;
			}
		}
		for(int j=1;j<=m;j++) cu[j]+=cu[j-1],ans[p[j]]+=cu[j],is[p[j]]=1;
		for(int j=0;j<=m+1;j++) cu[j]=0;
	}
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
	return 0;
}

线性做法

#include
using namespace std;
const int N=5e5+5;
int n,k,fa[N],du[N];
bool is[N];
int hd[N],to[N<<1],nx[N<<1],edge;
void add(int u,int v){
	nx[++edge]=hd[u];
	to[hd[u]=edge]=v;
	du[u]++;
}
void bfs(){
	queueq;
	for(int i=1;i<=n;i++) if(!du[i]) is[i]=1,q.push(i);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		int v=fa[u];
		du[v]--;
		if(!du[v]) is[v]=1,q.push(v);
	}
}
int dn[N],id[N],sz[N],dp[N],su[N],dfn,rt;
int qu[N],tp;
void dfs(int u){
	qu[++tp]=u;
	dn[u]=++dfn;
	id[dfn]=u;
	sz[u]=1;
	su[dn[u]]++;
	if(tp>k+1) su[dn[qu[tp-k-1]]]--;
	for(int e=hd[u];e;e=nx[e]){
		int v=to[e];
		if(!is[v]) continue;
		dp[v]=dp[u]+1;
		dfs(v);
		sz[u]+=sz[v];
	}
	tp--;
}
int ans[N],p[N],c[N];
int main()
{
	cin>>n>>k;
	for(int i=1;i<=n;i++) scanf("%d",&fa[i]),add(fa[i],i);
	bfs();
	for(int i=1;i<=n;i++) if(!is[i]){
		int m=0;
		p[++m]=i;
		for(int j=fa[i];j!=i;j=fa[j]) p[++m]=j;
		for(int j=1;j<=m;j++){
			int u=p[j];
			dfn=0; rt=u;
			dfs(u);
			for(int l=1;l<=sz[u];l++) su[l]+=su[l-1];
			for(int l=2;l<=sz[u];l++){
				int v=id[l];
				ans[v]+=su[l+sz[v]-1]-su[l-1];
			}
			for(int l=1;l<=sz[u];l++) su[l]=0;
			for(int l=1;l<=sz[u];l++){
				int v=id[l],t=k-dp[v];
				if(t<0) continue;
				if(t>=m-1){
					c[1]++;
					continue;
				}
				if(j+t<=m) c[j]++,c[j+t+1]--;
				else c[j]++,c[1]++,c[j+t-m+1]--;
			}
		}
		for(int j=1;j<=m;j++) c[j]+=c[j-1],ans[p[j]]+=c[j],is[p[j]]=1;
		for(int j=0;j<=m+1;j++) c[j]=0;
	}
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
	return 0;
}