P14362 [CSP-S 2025] 道路修复 / road

考试的时候想到是个最小生成树的来着,先码了 \(O(m \log m+2^km)\) 的解法,预计 64 分。赛后才想明白可以把 \(m\) 换成 \(kn\),这样就能过了。

可惜,当时没想到可以先对原图跑 MST 的来着,痛失 36pts+场切蓝题的机会。


题目传送门

博客传送门

首先,\(k=0\) 的点就是个最小生成树板子,板的不能再板的板子。我们直接跑 Kruskal 即可。预计得分 16pts。

然后说说我考场上的 64 分做法吧。

由于 \(k\) 很小,所以我们合理猜测复杂度带一个 \(O(2^k)\)。那这样的话,我们每次枚举要改造哪些乡村,对于每种情况,我们暴力地跑一次 Kruskal 求最小生成树,最终答案取min。

如果只是这么做的话,时间复杂度是 \(O(2^kM \log M)\) 的,其中 \(M=2m+2kn\)(后来想了想,跑 Kruskal 为什么要建双向边)

好吧实际上 \(M=m+kn\)

但是我们如果把边全部建好了以后,没必要每次枚举改造乡村时都排序的。所以我们一开始就排序,时间复杂度降为 \(O(M \log M + 2^kM)\)

凭记忆复现出来的考场代码:(包括 \(k=0\)\(k \le 5\) 两种情况)

考场代码
#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

const int N=1e4+14;
const int M=1e6+6;
const int K=12;
const int inf=1e17;
int n,m,k,h[N+K],W[N+K],tot,fa[N+K];
struct sw{
	int u,v,w,nxt;
}e[2*M+2*K*N];

inline void add(int u,int v,int w){
	e[++tot]={u,v,w,h[u]};h[u]=tot;
}

inline bool check(int u,int v,int S){
	//检查该边是否合法(或者说,查一查两个端点是否存在) 
	if(u<=n&&v<=n){
		return 1;
	}
	else if(u>n&&v<=n){
		int x=u-n-1;
		if((S&(1<<x))){
			return 1;
		}
		else{
			return 0;
		}
	}
	else{
		int y=v-n-1;
		if((S&(1<<y))){
			return 1;
		}
		else{
			return 0;
		}
	}
}

inline bool cmp(sw x,sw y){
	return x.w<y.w;
} 

inline int FIND(int x){
	return (x==fa[x]?x:fa[x]=FIND(fa[x]));
}

inline int solve(int S){
	//并查集初始化 
	for(int i=1;i<=n+k;i++){
		fa[i]=i;
	}
	int res=0;
	//累加点权 
	for(int i=1;i<=k;i++){
		if((S&(1<<(i-1)))){
			res+=W[i+n];
		}
	}
	//正常Kruskal 
	for(int i=1;i<=tot;i++){
		int u=e[i].u,v=e[i].v;
		if(check(u,v,S)){
			int fu=FIND(u),fv=FIND(v);
			if(fu!=fv){
				fa[fu]=fv;
				res+=e[i].w;
			}	
		}
	}
	return res;
}

signed main(){
	n=read(),m=read(),k=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w);add(v,u,w);
	}
	//其实我没必要写add函数的,跑Kruskal不需要这个 
	for(int i=1;i<=k;i++){
		W[n+i]=read();
		for(int j=1;j<=n;j++){
			int w=read();
			add(i+n,j,w);add(j,i+n,w);
		}
	}
	sort(e+1,e+tot+1,cmp);
	if(k==0){//16pts
		for(int i=1;i<=n;i++){
			fa[i]=i;
		}
		int ans=0;
		for(int i=1;i<=tot;i++){
			int u=e[i].u,v=e[i].v;
			int fu=FIND(u),fv=FIND(v);
			if(fu!=fv){
				ans+=e[i].w;
				fa[fu]=fv;
			}
		}
		printf("%lld",ans);
	}
	else if(k<=10){//64pts
		int ans=inf;
		for(int S=0;S<(1<<k);S++){
			ans=min(ans,solve(S));
		}
		printf("%lld",ans);
	}
	return 0;
} 

接下来讲满分做法。

我们发现,原图里不在最小生成树的边是没有竞争力,也不会被算进答案里的。

因为它竞争不过那些在最小生成树里面的边,而它又不能作为连接改造乡村的边,或者我这么说,它在边少的情况下都竞争不过原本的城市边,现在要加入改造乡村的边,原本的生成树边都可能被淘汰掉,它更竞争不过了。

所以我们对原图跑 Kruskal,只保留最小生成树上的边,这样 \(M=kn\),然后进行 64 分的那个过程,足以通过民间数据了,时间复杂度是 \(O(2^kkn+kn \log kn+m \log m)\) 的,CCF换少爷机的话应该也能跑的过去。

代码:

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

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

const int N=1e4+14;
const int M=1e6+6;
const int K=12;
const long long inf=1e16;
int n,m,k,h[N+K],tot,W[N+K],fa[N+K];
struct Nahida{
	int u,v,w;
}e[K*N],ee[M];

inline bool cmp(Nahida x,Nahida y){
	return x.w<y.w;
}

inline int FIND(int x){
	return (x==fa[x]?x:fa[x]=FIND(fa[x]));
}

inline bool check(int u,int v,int S){
	if(u<=n&&v<=n){
		return 1;
	}
	else if(u>n&&v<=n){
		int x=u-n-1;
		if((S&(1<<x))){
			return 1;
		}
		else{
			return 0;
		}
	}
	else{
		int y=v-n-1;
		if((S&(1<<y))){
			return 1;
		}
		else{
			return 0;
		}
	}
}

inline long long solve(int S){
	for(int i=1;i<=n+k;i++){
		fa[i]=i;
	}
	long long res=0;
	for(int i=1;i<=k;i++){
		if((S&(1<<(i-1)))){
			res+=W[i+n];
		}
	}
	for(int i=1;i<=tot;i++){
		int u=e[i].u,v=e[i].v;
		if(check(u,v,S)){
			int fu=FIND(u),fv=FIND(v);
			if(fu!=fv){
				fa[fu]=fv;
				res+=e[i].w;
			}	
		}
	}
	return res;
}

signed main(){
	n=read(),m=read(),k=read();
	for(int i=1;i<=m;i++){
		ee[i].u=read(),ee[i].v=read(),ee[i].w=read();
	}
	//对原图跑Kruskal 
	sort(ee+1,ee+m+1,cmp);
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		int u=ee[i].u,v=ee[i].v,w=ee[i].w;
		int fu=FIND(u),fv=FIND(v);
		if(fu!=fv){
			e[++tot]={u,v,w};
			fa[fv]=fu;
		}
	}
	//然后仿照64分的过程来就好了 
	for(int i=1;i<=k;i++){
		W[i+n]=read();
		for(int j=1;j<=n;j++){
			int w=read();
			e[++tot]={i+n,j,w};
		}
	}
	sort(e+1,e+tot+1,cmp);
	long long ans=inf;
	for(int S=0;S<(1<<k);S++){
		ans=min(ans,1ll*solve(S));
	}
	printf("%lld",ans);
	return 0;
}

其他事情等 CCF 结果出了再说吧,比如这东西能不能过官方数据什么的。以及 \(O(2^kn)\) 解法,本人暂时没有看明白,等我读明白了也会在这篇题解后面补充。

posted @ 2025-11-03 09:19  qwqSW  阅读(61)  评论(0)    收藏  举报