arc061e Snuke's Subway Trip

时间:2021-08-04
本文章向大家介绍 arc061e Snuke's Subway Trip,主要包括 arc061e Snuke's Subway Trip使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

\(n\)​ 个点,\(m\)​ 条边的无向图,每条边有一个颜色 \(c_i\)​ ,每条边的代价为 \(1\)​ .

如果连续经过多条颜色相同的边,代价会减少,总代价为 \(1\)​ .

问从 \(1\) 号点到 \(n\) 号点,最少代价是多少?

\(1\leq n\leq 10^5,0\leq m\leq 2\cdot 10^5,1\leq c_i\leq 10^6\)

大概 2019 年 12 月份第一次见到这个题目. 知道了建虚拟节点.

首先,因为经过多条颜色相同的边代价为 \(1\)​​ ,那么,可以把颜色相同的边连成的点看成一个连通块. 但这和普通的联通块也有些区别,因为两个点中可能有多条颜色相同的边,所以,一个点会包含在多个联通块中.

那么,同一个联通块中的点两两之间到达的代价是 \(1\)​​ ,如果直接连边肯定不优,考虑新建一个点 \(x\)​​ ,让联通块中的所有点连向 \(x\)​​ ,代价为 \(1\)​,接着,让 \(x\)​ 连向联通块中的所有点,代价为 \(0\)​. 这样,把 \(O(n^2)\)​ 的点变成了 \(O(n)\)​ 的.

建出新图中的边权是 \(0\)\(1\) ,求最短路,可以用 01bfs 实现 .

在处理联通块的时候也要小心,要避免写成 \(O(nm)\) 的,对于同一个颜色统一处理,然后用 dsu 判断联通块.

还需要注意的就是这题数组要开到的范围,如果是上面的思路,图中的点就有可能达到 \(3\cdot 10^5\) ,而且颜色是 \(10^6\) 个的.

统计被同种颜色连接的点时需要去重之后再用dsu判断,否则,显出新图的时候会重复连边,导致空间爆炸. 这个问题非常隐蔽,但是我感觉挺重要的.

时间复杂度 : \(O(n+m)\)

空间复杂度 : \(O(n+m)\)

第一次提交 : AC

code

#include<bits/stdc++.h>
using namespace std;
const int inf=1e9+10;
int n,m;
int fa[100010];
vector<int>v[1000010],cmp[100010];
vector<pair<int,int> >g[300010],e[1000010];
int dist[300010];
deque<int>que;
inline int get_fa(int x){
	return fa[x]==x?x:fa[x]=get_fa(fa[x]);
}
inline void merge(int a,int b){
	a=get_fa(a);b=get_fa(b);
	if(a==b)return;
	fa[a]=b;
}
inline void ae(int u,int v,int cost){
	g[u].push_back(make_pair(v,cost));
}
int main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=0;i<m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		a--;b--;c--;
		e[c].push_back(make_pair(a,b));
		v[c].push_back(a);
		v[c].push_back(b);
	}
	int cnt=n;
	for(int i=0;i<n;i++)fa[i]=i;
	for(int i=0;i<1000000;i++){
		if((int)v[i].size()==0)continue;
		sort(v[i].begin(),v[i].end());
		v[i].erase(unique(v[i].begin(),v[i].end()),v[i].end());
		for(int j=0;j<(int)e[i].size();j++){
			int a=e[i][j].first,b=e[i][j].second;
			merge(a,b);
		}
		for(int j=0;j<(int)v[i].size();j++){
			int x=v[i][j];
			cmp[get_fa(x)].push_back(x);
		}
		for(int j=0;j<(int)v[i].size();j++){
			int x=v[i][j];
			if((int)cmp[x].size()==0)continue;
			for(int k=0;k<(int)cmp[x].size();k++){
				ae(cmp[x][k],cnt,1);
				ae(cnt,cmp[x][k],0);
			}
			cnt++;
		}
		for(int j=0;j<(int)v[i].size();j++){
			int x=v[i][j];
			fa[x]=x;
			cmp[x].clear();
		}
	}
	for(int i=0;i<cnt;i++)dist[i]=inf;
	dist[0]=0;
	que.push_front(0);
	while(!que.empty()){
		int x=que.front();que.pop_front();
		for(int i=0;i<(int)g[x].size();i++){
			int to=g[x][i].first,cost=g[x][i].second;
			if(dist[to]==inf){
				dist[to]=dist[x]+cost;
				if(cost==0)que.push_front(to);
				else que.push_back(to);
			}
		}
	}
	if(dist[n-1]==inf)dist[n-1]=-1;
	cout<<dist[n-1]<<endl;
	return 0;
}
/*inline? ll or int? size? min max?*/

w4yneb0t

还是考虑联通块中的点能花费代价为 \(1\) 互相到达,是否可以不建出新图的情况下,bfs 得到.

\(dis(i)\)​ 表示第 \(0\) 个点到第 \(i\) 个点的最短距离.

bfs 队列的第一个元素 \(x\)​ ,它可以花费为 \(1\)​ 的代价到达的点有哪些. 考虑连接 \(x\) 的一个边为 \(i\) ,那么,包含 \(x\) ,以颜色为 \(c_i\) 的边连接的连通块中的点都可以花费 \(1\) 的代价到达 . 那么,可以 dfs 跟新这些点.

但是,这就存在一个问题,转移的时候时间复杂度是 \(O(n)\)​ 的 .

但是,其实有些转移是不需要的,一个连通块中只要有一个点从 bfs 序列的队首弹出跟新,整个连通块解会被跟新一遍 . 对于在 bfs 序列后的此联通块中的点,就不会是最优转移的来源了 . 此时,需要一个 set 数组记录每个点被跟新过的颜色集合. 并且,还需要一个用 set 或 map 存储的连接表,能单独取出与 \(x\) 相连,颜色为 \(c\) 的点.

这个思路的有点在于出去了上面求连通块的,非常容易犯错的部分. 但是,在时间复杂度上面就没有上面的优秀.

时间复杂度 : \(O(n+m+m\log m+n\log m)\)

空间复杂度 : \(O(n+m)\)

tourist

还是考虑对于每个联通块新建一个点,建出上面的图,在处理联通块的时候,还可以考虑 bfs 一个联通块中的点. 问题在于,连接表中无法表示颜色,所以有可能遍历是 \(O(n(n+m))\) 的 .

因为颜色分类,求出联通块,所以,如果将邻接表按照颜色 sort 一遍之后,能通过的点,就是一段区间,所以,可以考虑对每个节点的邻接表维护一个指针,用双指针的方法做到 \(O(n+m)\) 的遍历. 因此,就可以 bfs 求出一个联通块的点.

那么,其实可以不用 01 bfs,因为上面新建出的图是个有向图,其中原图中的点的出边都是 \(1\) ,入边都是 \(0\) .

图其实非常有规律,路径必定都是 图中的点 \(\rightarrow\) 新建的点 \(\rightarrow\) 图中的点 \(\rightarrow\) 新建的点 \(\rightarrow\) 图中的点 \(\cdots\)​ 这样下去 .

其实可以令所有的边为代价 \(1\)​ ,相当于每个联通块的代价是 \(2\) . 在 bfs 之后,只要将总代价 / 2 就可以了.

时间复杂度 : \(O(n+m+m\log m)\)

空间复杂度 : \(O(n+m)\)

原文地址:https://www.cnblogs.com/suadwm/p/15099512.html