货车运输的五种解法

news/2024/10/4 17:23:10/文章来源:https://www.cnblogs.com/Iictiw/p/18274446
(这里的“五种解法”之间有实现方式之外的不同)

方法1:最大生成树 + 树上倍增

本题的标准解法,先用 kruskal 建出最大生成树,再在最大生成树跑树上倍增求路径 \(min\) ,时间复杂度为 \(\Theta(n \log n + q \log n)\)

树上倍增也可以用树剖替换,但是需要两个 \(\log\)

具体实现上,可以先找到 LCA 再让两个端点一起向上跳,也可以在求 LCA 的过程中一并求出路径 \(min\),第一种方法常数小一半。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+5,logN=17,inf=1e9+5;
int n,m,q,idx=1,head[N];
struct eg{int x,y,v;}g[N<<1];
inline bool cmp(eg x,eg y){return x.v>y.v;}
int FA[N];
inline int find(int f){while(FA[f]!=f)f=FA[f]=FA[FA[f]];return FA[f];
}
inline void merge(int x,int y){x=find(x),y=find(y);if(x!=y)FA[y]=x;
}
struct edge{int to,next,val;}e[N<<1];
inline void add(int from,int to,int val){e[++idx].to=to;e[idx].val=val;e[idx].next=head[from];head[from]=idx;
}
void kruscal(){//求最大生成树sort(g+1,g+1+m,cmp);for(int i=1;i<=n;i++)FA[i]=i;for(int i=1;i<=m;i++){int x=g[i].x,y=g[i].y;if(find(x)==find(y))continue;merge(x,y);add(x,y,g[i].v),add(y,x,g[i].v);}
}
int fa[N][logN],mn[N][logN];
int dep[N];
void dfs(int p){for(int i=head[p];i;i=e[i].next){int to=e[i].to;if(to==fa[p][0])continue;fa[to][0]=p,dep[to]=dep[p]+1;mn[to][0]=e[i].val;dfs(to);}
}
int calc(int x,int y){//倍增求路径minint ret=inf;if(dep[x]>dep[y])swap(x,y);int del=dep[y]-dep[x];for(int i=0;(1<<i)<=del;i++)if((del>>i)&1)ret=min(ret,mn[y][i]),y=fa[y][i];if(x==y)return ret;for(int i=logN-1;~i;i--)if(fa[x][i]!=fa[y][i])ret=min(ret,min(mn[x][i],mn[y][i])),x=fa[x][i],y=fa[y][i];return min(ret,min(mn[x][0],mn[y][0]));
}
int main(){ios::sync_with_stdio(0);cin.tie(0);cin>>n>>m;for(int i=1;i<=m;i++)cin>>g[i].x>>g[i].y>>g[i].v;kruscal();for(int i=1;i<=n;i++)if(!dep[i])dep[i]=1,dfs(i);for(int j=1;j<logN;j++)for(int i=1;i<=n;i++){//预处理倍增数组fa[i][j]=fa[fa[i][j-1]][j-1];mn[i][j]=min(mn[i][j-1],mn[fa[i][j-1]][j-1]);}cin>>q;while(q--){int x,y;cin>>x>>y;if(find(x)!=find(y))cout<<-1<<'\n';else cout<<calc(x,y)<<'\n';}return 0;
}
//marisa
without O2:

with O2:

出人意料的有着不错的效率,不知道用树剖效果怎么样。

方法2:kruskal 重构树

比较裸的做法,先建出 kruskal 重构树,然后对于每次询问,两个端点在重构树上的 LCA 的点权即为答案。

该方法本质上与方法 1 接近,感觉有点大材小用。

时间复杂度也是 \(\Theta(n \log n + q \log n)\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int N=2e5+5,logN=18;
int n,m,q;
struct eg{int x,y,v;}g[N];
inline bool cmp(eg x,eg y){return x.v>y.v;}
int FA[N];
inline int find(int f){while(FA[f]!=f)f=FA[f]=FA[FA[f]];return FA[f];
}
int tot,a[N];
vector<int>v[N];
void ex_kruskal(){sort(g+1,g+1+m,cmp);tot=n;for(int i=1;i<2*n;i++)FA[i]=i;for(int i=1;i<=m;i++){int x=find(g[i].x),y=find(g[i].y);if(x==y)continue;FA[x]=FA[y]=++tot;a[tot]=g[i].v;v[x].push_back(tot),v[tot].push_back(x);v[y].push_back(tot),v[tot].push_back(y);}
}
int dep[N],fa[N][logN];
void dfs(int p){for(auto to:v[p]){if(to==fa[p][0])continue;dep[to]=dep[p]+1;fa[to][0]=p;dfs(to);}
}
inline int lca(int x,int y){if(dep[x]>dep[y])swap(x,y);int del=dep[y]-dep[x];for(int i=0;(1<<i)<=del;i++)if((del>>i)&1)y=fa[y][i];if(x==y)return x;for(int i=logN-1;~i;i--)if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];return fa[x][0];
}
int main(){ios::sync_with_stdio(0);cin.tie(0);cin>>n>>m;for(int i=1;i<=m;i++)cin>>g[i].x>>g[i].y>>g[i].v;ex_kruskal();for(int i=tot;i;i--)if(!dep[i])dep[i]=1,dfs(i);for(int j=1;j<logN;j++)for(int i=1;i<=tot;i++)//kruskal重构树有2n-1个节点!!!fa[i][j]=fa[fa[i][j-1]][j-1];cin>>q;while(q--){int x,y;cin>>x>>y;if(find(x)!=find(y))cout<<-1<<'\n';else cout<<a[lca(x,y)]<<'\n';}return 0;
}
//marisa
without O2:

with O2:

还是过于大材小用了。

方法3:离线+启发式合并

据说是当时的官方题解。

将每个询问挂在两个端点上,将所有边按边权自大到小排序,随后将每条边依次插到最大生成树中。

对于每条边,遍历两个端点中所在集合较小的,如果另一边的询问集合有该集合中的询问,则将该询问的答案记为边权。

遍历结束后,将两端的集合合并,每次合并较小的集合至少扩大一倍,因此总的时间复杂度为 \(\Theta((n + q )\log ^2 n)\)。(有一个 \(\log\) 来自 set)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
const int N=1e5+5;
int n,m,q;
struct eg{int x,y,v;}g[N<<1];
inline bool cmp(eg x,eg y){return x.v>y.v;}
set<int>s[N];
int fa[N];
inline int find(int f){while(f!=fa[f])f=fa[f]=fa[fa[f]];return fa[f];
}
int ans[N];
int main(){ios::sync_with_stdio(0);    cin.tie(0);cin>>n>>m;for(int i=1;i<=m;i++)cin>>g[i].x>>g[i].y>>g[i].v;sort(g+1,g+1+m,cmp);cin>>q;for(int i=1,x,y;i<=q;i++){cin>>x>>y;s[x].insert(i),s[y].insert(i);ans[i]=-1;}for(int i=1;i<=n;i++)fa[i]=i;for(int i=1;i<=m;i++){int x=g[i].x,y=g[i].y;x=find(x),y=find(y);if(x==y)continue;if(s[x].size()>s[y].size())swap(x,y);for(auto it:s[x]){if(s[y].count(it))ans[it]=g[i].v;else s[y].insert(it);}s[x].clear();fa[x]=y;}for(int i=1;i<=q;i++)cout<<ans[i]<<'\n';return 0;
}
//marisa
without O2:

with O2:

没有氧气加持 set 还是太慢了,但启发式合并的小常数使得两个 \(log\) 能与一个 \(log\) 拼一拼。

方法4:按秩合并+暴力求 LCA

常用的路径压缩并查集在 merge 时丢失了很多信息(比如树的形态)。

如果采用按秩合并,一来可以保证并查集的总复杂度仍是 \(\Theta(n \log n)\) ,同时可以保证树高为 \(O(\log n)\) 级别,每次询问时,可以让两个端点暴力向上跳 LCA,寻找路径 min,时间复杂度依旧是 \(\Theta(n \log n + q \log n)\)

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e5+5,inf=1e9+5;
int n,m,q;
struct eg{int x,y,v;}g[N<<1];
inline bool cmp(eg x,eg y){return x.v>y.v;
}
int fa[N],sz[N],a[N];
inline int find(int f){if(fa[f]!=f)return find(fa[f]);return f;
}
int main(){ios::sync_with_stdio(0);cin.tie(0);cin>>n>>m;for(int i=1;i<=m;i++)cin>>g[i].x>>g[i].y>>g[i].v;sort(g+1,g+1+m,cmp);for(int i=1;i<=n;i++)fa[i]=i,sz[i]=1;for(int i=1;i<=m;i++){int x=g[i].x,y=g[i].y;x=find(x),y=find(y);if(x==y)continue;if(sz[x]>sz[y])swap(x,y);fa[x]=y,sz[y]+=sz[x],a[x]=g[i].v;}cin>>q;while(q--){int x,y,res=inf;cin>>x>>y;if(find(x)!=find(y))cout<<-1<<'\n';else{while(x!=y){if(sz[x]>sz[y])swap(x,y);res=min(res,a[x]),x=fa[x];}cout<<res<<'\n';}}return 0;
}
//marisa
without O2:

with O2:

令人出乎意料而又理所当然的第一,按秩合并的常数比倍增小不少,或许数据也没能卡满。

方法5:整体二分

这里的“整体二分”和与常见的处理序列问题的那个“整体二分”有点不同。

先考虑只有一次询问的情况,每次二分一个答案,然后将所有边权大于等于 \(mid\) 的边合并,然后检查询问的端点是否联通,复杂度为 \(\Theta(m \log n \log Z)\) ,其中 \(Z\) 为所有 \(z_i\) 的最大值。

乍一看这东西很没前途:效率不如暴力,\(q\) 次询问就成了 \(\Theta(q m \log n \log Z)\),复杂度瓶颈在于每次 \(check\) 都必须给并查集重新赋值。

于是我们可以将所有询问放在一起二分,每次从大到小的将所有询问和边一起处理,\(q\) 个询问可以共用一个并查集。

具体实现上,可以将每个询问用存为 \(\big( l,r,mid \big)\) ,每次将边与询问放入并查集。

其实简单处理一下可以让询问和边一同排序,这就和一般的整体二分有点像了(把边看作修改)。

时间复杂度为 \((q + m) \log n \log Z\),如果离散化一下可以让 \(\log Z\) 变成 \(\log m\),但是无所谓了。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m,q;
struct eg{int x,y,v;}g[N];
inline bool cmp(eg x,eg y){return x.v>y.v;}
struct ques{int x,y,l,r,mid,id,ans;}Q[N];
int ans[N];
int fa[N];
inline int find(int f){while(fa[f]!=f)f=fa[f]=fa[fa[f]];return fa[f];
}
inline void merge(int x,int y){x=find(x),y=find(y);if(x!=y)fa[y]=x;
}
inline bool cmp1(ques x,ques y){return x.mid>y.mid;}
int main(){ios::sync_with_stdio(0);cin.tie(0);cin>>n>>m;for(int i=1;i<=m;i++)cin>>g[i].x>>g[i].y>>g[i].v;sort(g+1,g+1+m,cmp);cin>>q;for(int i=1;i<=q;i++){cin>>Q[i].x>>Q[i].y;Q[i].l=g[m].v,Q[i].r=g[1].v,Q[i].id=i,Q[i].mid=(g[1].v+g[m].v)>>1,Q[i].ans=-1;}for(int k=1;k<=16;k++){//这里的16为二分次数(即logZ)int j=1;for(int i=1;i<=n;i++)fa[i]=i;sort(Q+1,Q+1+q,cmp1);for(int i=1;i<=q;i++){while(j<=m&&g[j].v>=Q[i].mid)merge(g[j].x,g[j].y),j++;if(find(Q[i].x)==find(Q[i].y))Q[i].l=Q[i].mid+1,Q[i].ans=Q[i].mid;else Q[i].r=Q[i].mid-1;Q[i].mid=(Q[i].l+Q[i].r)>>1;}}for(int i=1;i<=q;i++)ans[Q[i].id]=Q[i].ans;for(int i=1;i<=q;i++)cout<<ans[i]<<'\n';return 0;
}
//marisa
without O2:

with O2:

理论复杂度是最大的,实际表现已经很不错了,肉眼可见的大材小用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/733069.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一张图让你看懂10种软件架构风格

软件架构风格是构建各种软件系统的基本蓝图,确保它们满足特定的要求和质量属性。通过坚持合适的架构风格,组织可以确保其软件系统的构建与其战略目标保持一致,适应未来的变化,并在面对不断发展的技术环境和用户需求时具有弹性。 以下是最常见的样式:(单体架构):将整个…

linux三剑客-grep、sed、awk

Linux三剑客是Linux系统中最重要的三个命令,它们以其强大的功能和广泛的应用场景而闻名。这三个工具的组合使用几乎可以完美应对Shell中的数据分析场景,因此被统称为Linux三剑客。 1、grep grep是一个强大的文本搜索工具,用于在文件内容中查找指定的字符串,并将匹配到的行输…

联通网络无法使用FTP,无法使用21端口连接的解决方法

最近家里换了联通的网络,结果发现连接不上FTP了,本来以为是软件的问题。最后发现只有21端口的FTP连接不上,其它的端口没问题。 首先想到的是肯定是联通的光猫把21端口给关闭了。然后就想着通过192.168.1.1来设置一下光猫。专业网站制作、系统开发订制、微信公众号开发、接外…

LINUX命令-sed

sed命令是用于对文本文件做内容操作的神器,常见的增删改都可以,熟练运用可提高shell脚本编写能力和在terminal下的工作效率。本文编辑小绝技-sed sed命令是用于对文本文件做内容操作的神器,常见的增删改都可以,查没必要用它,用grep或者gvim打开用vim的搜索匹配就行。 sed …

毕业好几年了还要考研吗?

其实,毕业多少年都不影响我们考研,因为考研本身并没有年限或者年龄上的限制。所以,在是否考研这个问题上,我们真正应该思考的是,我们是否已经对未来做了一个比较合理的规划,考研这件事是否在未来的规划中有着重要的影响,如果是,而且现实条件也允许我们去考,那么,就应…

ubuntu 下使用netplan配置网络

一个yaml走遍天下。 netplan 是Ubuntu底层网络配置的封装,它允许使用yaml的方式“声明式”的配置底层网络,不管底层网络是NetworkManager还是networkd. netplan 官网,使用静态配置的示例: https://netplan.readthedocs.io/en/stable/netplan-tutorial/#using-static-ip-add…

(并查集+双向映射)

题意: 思路: 题目就是让我们实现把一个代表数x的集合加到另一个代表数y的集合中多次操作,这个很容易想到用并查集维护,将相同数字的下标放到一个集合中,集合所代表的数字,用“集合的首领”和代表的数字做一个双射,这样既能表示出集合所带表的数,还能辅助之后输出集合,…

2024年Java学习路线

java 最新学习路线2024Java学习路线(快速版) 核心基础:Java基础→MySQL→JDBC→JavaWeb 微服务核心:Maven→Gradle→Spring6→SpringMVC→MyBatis→MyBatisPlus→SSM →Redis7→SpringBoot2→SpringCloud 微服务生态:Git→Docker→Elasticsearch→ZooKeeper→Nginx→Sprin…

DApp设计与开发 课程笔记(二)remix | hardhat | 测试驱动开发

笔记对应课程内容为成都信息工程大学区块链产业学院老师梁培利的DApp 设计与开发 04-06 课 笔记中提到的名词不做过多解释 不懂就搜!Remix IDE的基本使用 官网:https://remix.ethereum.org/建议使用其网页版而不是桌面版,侧重于比较实用的特性而不是全部的介绍。 支持编写合…

DApp设计与开发 课程笔记(二)

笔记对应课程内容为成都信息工程大学区块链产业学院老师梁培利的DApp 设计与开发 04-06 课 笔记中提到的名词不做过多解释 不懂就搜!Remix IDE的基本使用 官网:https://remix.ethereum.org/建议使用其网页版而不是桌面版,侧重于比较实用的特性而不是全部的介绍。 支持编写合…

OOP第三次博客

write_by_23201707_gongjunjie oop第三次博客 一:前言 这次博客不出意外是oop课程的最后一次博客了,不过这次博客pta只有两题,但是我想说的是,最后一次pta也是够难的, 但是好像我自己的设计也有很大的问题,第七次pta遗留下了一点问题,导致第八次出现了很多问题 二:关于…

cpp

没有合适的默认构造函数可用 加上这个地方就ok