“蔚来杯“2022牛客暑期多校训练营(加赛) B题: Bustling City

B题: Bustling City

原题链接:https://ac.nowcoder.com/acm/contest/38727/B

题目大意

n ( 1 ≤ n ≤ 1 0 6 ) n(1\le n\le 10^6) n(1n106) 个城市,第 i i i 个城市有一个商人,和一条城市 i i i 通往城市 a i a_i ai 的双向道路。

每年年初,城市 i i i 的所有商人会前往城市 a i a_i ai

求每个城市最早在哪一年有至少 k k k 个商人。

题解

显然,输入为一张内向基环森林。
考虑将每棵基环树分为树和环两部分进行求解。
树上部分可以通过维护每个点的若干级儿子来求解,这个信息是可以从儿子传递到父亲的,通过长链剖分,每次将其他儿子的信息暴力转移到长链上对应深度的点可以做到 O ( ∑ l e n ) = O ( n ) O(\sum len)=O(n) O(len)=O(n) 的复杂度。
对于环上部分,考虑其子树上的点何时会转移到它,可以根据子树中的点到根的距离等价映射到环上的一个前置点。
参考下图:

图中黑色为原本的边,绿框内部分为环,我们将环外的子树内的点,根据相对距离标记到环上的对应的点。
显然,子树内深度相同的点的标记位置是相同的,因此我们可以利用前面长链剖分中已经统计的每个深度对应的节点数。
当环上某个点的一个前置点的人数能够达到 k k k 个时,我们对其计算所需步数。
然后枚举环上每一个点,考虑从其他已经计算过的点走到他还需要几步,选取最小值,这个可以用一个 m u l t i s e t multiset multiset 维护。
总复杂度 O ( n l o g n ) O(nlogn) O(nlogn) ,可以通过。

注意点
  1. 处理子树问题时可以反向建边( a i → i a_i\rightarrow i aii ),将基环内向树转化为外向树。
  2. 因为是森林,记得多次初始化清空。

参考代码

#include<bits/stdc++.h>
using namespace std;

template<class T>inline void read(T&x){
	char c,last=' ';
	while(!isdigit(c=getchar()))last=c;
	x=c^48;
	while(isdigit(c=getchar()))x=(x<<3)+(x<<1)+(c^48);
	if(last=='-')x=-x;
}

const int MAXN=1e6+5,INF=0x3f3f3f3f;
int n,k;
int a[MAXN];
int vis[MAXN];//是否访问过
int dep[MAXN];//深度
int max_dep[MAXN];//子树内最大深度(求长儿子)
int _son[MAXN];//长儿子
int cnt[MAXN];//维护长链上等深度的节点个数
int dp[MAXN];//当前点往下最小有k个同层节点的深度
int num[MAXN];//环上某个前置点的等价点个数
int when[MAXN];//预计算的个数
int ans[MAXN];//答案
vector<int>loop;//环上的点
vector<int>e[MAXN];//边(反向)
vector<int>chain[MAXN];//环上每个点的子树内的长链信息

struct node{
	int p,length;//p表示子树的根是环上第p个点,length表示长链的长度
}infor[MAXN];

bool cmp(const node&a,const node&b){
	return a.length<b.length;
}

int find_loop(int x){//寻找环
	if(vis[x])return x;//找到了一个已访问过的点(即找到了环)
	vis[x]=1;
	loop.push_back(x);
	return find_loop(a[x]);
}

void _dfs(int x,int depth){//预搜索,求长儿子
	max_dep[x]=dep[x]=depth;
	for(int i=0,son;i<(int)e[x].size();++i){
		son=e[x][i];
		if(vis[son])continue;
		_dfs(son,depth+1);
		if(max_dep[son]>max_dep[_son[x]])_son[x]=son;//有更长的链,更新长儿子
		max_dep[x]=max(max_dep[x],max_dep[son]+1);//更新最大深度
	}
}

void dfs(int x){
	cnt[x]=1;
	vis[x]=1;
	if(_son[x]){//先搜长链
		dfs(_son[x]);
		dp[x]=min(dp[x],dp[_son[x]]);
	}
	for(int i=0;i<(int)e[x].size();++i){
		int son=e[x][i];
		if(vis[son])continue;
		dfs(son);
		dp[x]=min(dp[x],dp[son]);
		int main=_son[x];//长链上的对应点
		while(son){
			cnt[main]+=cnt[son];//将信息转移到长链上
			if(cnt[main]>=k)dp[x]=min(dp[x],dep[main]);//更新一下达到k人所需的最小深度
			son=_son[son],main=_son[main];
		}
	}
	if(dp[x]<INF)ans[x]=dp[x]-dep[x];//可以达到k人,计算答案
}

void solve(int I){
	vector<int>().swap(loop);
	int head=find_loop(I);
	vector<int>pool;
	for(int i=0;i<(int)loop.size();++i){
		if(loop[i]!=head)vis[loop[i]]=0;//除去搜索时那些不属于环上的点
		else{
			for(int j=i;j<(int)loop.size();++j){
				pool.push_back(loop[j]);
			}
			break;
		}
	}
	loop=pool;
	int m=(int)loop.size();
	for(int i=0;i<m;++i){
		_dfs(loop[i],0),dfs(loop[i]);
		int x=loop[i];
		vector<int>().swap(chain[i]);
		while(x){
			chain[i].push_back(cnt[x]);//保存长链的信息
			x=_son[x];
		}
		infor[i].p=i,infor[i].length=(int)chain[i].size();
	}
	sort(infor,infor+m,cmp);//按照链长排序
	int l=0;
	for(int i=0;i<m;++i)num[i]=0,when[i]=INF;
	for(int i=0;i<infor[m-1].length;++i){//从小到大枚举子树内点到环上的距离
		while(infor[l].length<i+1)++l;//除去那些最大深度不到i的子树
		for(int j=l;j<m;++j){
			int p=((infor[j].p-i)%m+m)%m;//前置点
			num[p]+=chain[infor[j].p][i];
			if(num[p]>=k){//可达k人
				when[infor[j].p]=min(when[infor[j].p],i);//从前置点到此处需k步
			}
		}
	}
	multiset<int>st;
	for(int i=0;i<m;++i)if(when[i]<INF)st.insert(when[i]+m-i);//这些点转移的方向是S->m->1->T(绕一圈)
	for(int i=0;i<m;++i){
		if(when[i]<INF){//对于<=i的点,转移方向是S->T(不需要绕圈)
			st.erase(st.find(when[i]+m-i));
			st.insert(when[i]-i);
		}
		if(!st.empty())ans[loop[i]]=(*st.begin())+i;
	}
}

int main()
{
	read(n),read(k);
	for(int i=1;i<=n;++i){
		read(a[i]);
		e[a[i]].push_back(i);
		ans[i]=-1;
		dp[i]=INF;
	}
	for(int i=1;i<=n;++i)if(!vis[i])solve(i);
	for(int i=1;i<=n;++i)cout<<ans[i]<<" \n"[i==n];
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值