「SCOI2012」滑雪与时间胶囊 题解 && 卡题记录

发布时间 2023-08-21 21:47:39作者: LYXOfficial

前言

传送门

调了一个下午终于弄出来了!!!

卡题的事情

上图:

DCZ 貌似更惨(

题解

首先不难想到这是一个最小生成树题目:a180285 从点 1 出发,他希望遍历尽可能多的点且他可以回退,需要求此时代价最小路径的长度。

考虑使用 Kruskal 或 Prim 算法,因为 Kruskal 编码比较简单,遂采用该算法。

因为多个滑雪站之间只有高度较高的才能前往高度较低的站点,即对于任意 \(G(V,E)\),当 \(i,j\in V\) 时且 \(h_i\ge h_j\) 时才存在 \((i,j)\in E\),所以我们需要根据 \(h_u\)\(h_v\) 的关系来确定需要添加的边。

然后我们从起点 1 开始执行 BFS (DFS也可),算出从起点出发最多可以到达的顶点个数 \(tot\)

为了方便,我开了一个前向星和一个邻接表分别用于 Kruskal 和 BFS(都是打的 vector qwq)。

接下来排序并执行 Kruskal。排序需要注意一点:直接根据每一条边的权值 \(w\) 排序的贪心策略是错误的,可以考虑这么一种情况:

这是一张图,其中每个节点上面的 \(vx\) 表示编号,\(h: x\) 表示高度,边上的数字表示权值。如果我们按照边权排序后选择的话,就会选到 \((4,3)=1\), \((1,3)=4\)\((1,2)=4\),即上面描红了的边。然而这是无向图,我们就会发现 \(v1\) 并不能到达 \(v4\),然而我们要是选择了 \((1,4)=11\),虽然代价稍大,但是可以保证选到最多的点。

因为高度呈降序排列,前面的点一定可以到达后面的点,所以我们在高度上贪心取最小值即可保证尽可能选到多的点,当然高度一样时肯定也需要取权值较小的。

有几个坑:

坑 1:注意 BFS 时的 vis 数组,处理 Kruskal 时如果 !vis[u]||!vis[v] 时需要跳过(因为无法走到)。

坑 2:建边处理高度时需要同时考虑开的一个前向星和一个邻接表,不能少一个,否则必爆 0 ~ 18

坑 3:处理 Kruskal 时,如果 cnt==tot-1 即已遍历边数已经形成树形结构时就得 break 了。

那么代码就很简单了(但是注意有很多坑+没看注释导致的CE)

代码

偷懒,所以用了新特性,注意开 C++20以上。

注释版

#include <bits/stdc++.h>
#define int long long // 见祖宗+偷懒
using namespace std;
int n,m,fa[100005],tot=1,ans,vis[100005],h[100005];
struct node {int x,y,w,h;}; // 前向星,存起点终点和高度
vector<node> star; // 前向星
vector<int> lst[100005]; // 邻接表
queue<int> q; // BFS用
inline void add(int u,int v,int w){
    if(h[u]>=h[v]) // 根据高度情况考虑,注意两点高度相等时也可滑过
        star.push_back({u,v,w,h[v]}), // 前向星建图
        lst[u].push_back(v); // 邻接表建图
    if(h[u]<=h[v]) // 同上
        star.push_back({v,u,w,h[u]}),
        lst[v].push_back(u);
}
inline int find(int x){ // 三目表达式版简化find函数
	return x==fa[x]?x:fa[x]=find(fa[x]);
}
signed main(){
    ios::sync_with_stdio(0); // 关流同步,卡常(实际上题目给的5秒够用)
	cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>h[i],fa[i]=i; // 初始化
	for(int i=1,u,v,c;i<=m;i++)
        cin>>u>>v>>c,add(u,v,c); // 加边
    q.push(1),vis[1]=1; // BFS部分,起点入队
    while(!q.empty()){
        int u=q.front();q.pop();
        for(auto &it:lst[u])
            if(!vis[it]) // 计算可到达次数
                vis[it]=1,tot++,q.push(it);
    }
	sort(star.begin(),star.end(),[](node a,node b){
        return a.h==b.h?a.w<b.w:a.h>b.h;
    }); // 注意需要按高度排序,具体见TJ.
    // 这里用了C++11新特性lambda表达式,可以内联进参数里面
	for(int cnt=0;auto it:star){ // 遍历前向星跑 Kruskal
        // 这里的初始化cnt偷懒用了C++20新特性在范围循环初始化
		if(vis[it.x]&&vis[it.y]){ 
            int x=find(it.x),y=find(it.y); // 并查集板子
            if(x==y) continue;
            fa[x]=y,cnt++,ans+=it.w; // 算答案
            if(cnt==tot-1) break; // 边界条件,见TJ
        }
	}
	cout<<tot<<" "<<ans;
	return 0;
}

无注释版

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,m,fa[100005],tot=1,ans,vis[100005],h[100005];
struct node {int x,y,w,h;};
vector<node> star;
vector<int> lst[100005];
queue<int> q;
inline void add(int u,int v,int w){
    if(h[u]>=h[v])
        star.push_back({u,v,w,h[v]}),
        lst[u].push_back(v);
    if(h[u]<=h[v])
        star.push_back({v,u,w,h[u]}),
        lst[v].push_back(u);
}
inline int find(int x){
	return x==fa[x]?x:fa[x]=find(fa[x]);
}
signed main(){
    ios::sync_with_stdio(0);
	cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>h[i],fa[i]=i;
	for(int i=1,u,v,c;i<=m;i++)
        cin>>u>>v>>c,add(u,v,c);
    q.push(1),vis[1]=1;
    while(!q.empty()){
        int u=q.front();q.pop();
        for(auto &it:lst[u])
            if(!vis[it])
                vis[it]=1,tot++,q.push(it);
    }
	sort(star.begin(),star.end(),[](node a,node b){
        return a.h==b.h?a.w<b.w:a.h>b.h;
    });
	for(int cnt=0;auto it:star){
		if(vis[it.x]&&vis[it.y]){
            int x=find(it.x),y=find(it.y);
            if(x==y) continue;
            fa[x]=y,cnt++,ans+=it.w;
            if(cnt==tot-1) break;
        }
	}
	cout<<tot<<" "<<ans;
	return 0;
}

(为DCZ卡题感到十分同情(

各位大佬点个赞嘛qwq