【学习笔记】基础算法——生成树
最小生成树
应该都知道最小生成树是什么吧。
不知道的左转先理解最小生成树的定义!
好的上例题!
水。
两种做法,一个是 K 啥的,还有一个是 Prim 吧。
K 算法
时间复杂度 \(O(m\log m)\),瓶颈在于排序。
非常常用!代码很短!但是适用于稀疏图。
代码不贴了。
P 算法
时间复杂度应该是 \(O(n^2)\) 吧。
K 算法是加边,而 P 算法是加点。
很简单。适用于稠密图。但是并不很常用,因为一般 K 算法都是首选。
最小生成树的运用
我问你呢,有多少个题目会在脑袋上写着大大的“最小生成树”五个大字?不,不可能。都是藏着掖着的!
把“最小生成树”的提示藏在字里行间,需要你仔细剖析题目意思,详细分析题目要求才能发现。
运用很巧妙哇!
直说不好说啊。看几个题吧。
例题——Dungeons and Candies
乍一看,你看得出来这是最小生成树?不,当然看不出来。
它是藏着的!你需要去剖析,它的这个结构确实是一个树的形态,然后整个你需要自己去枚举,去连边,去造生成树。最后输出的时候还得遍历!
还是有难度的。
来,看一下代码:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1e3+5 , M = 2e6+5;
struct line{int u,v,w;}ln[M];vector<int> g[N];
char c[N][15][15];int n,m,k,w,fa[N],cnt,ans[N],sum;
bool cmp(line l1,line l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
bool Can_It_Merge(int u,int v){return (FF(u)!=FF(v));}
void Merge(int u,int v){if(FF(u)!=FF(v))fa[FF(v)]=FF(u);return;}
void DFS(int u,int Fat){
ans[u]=Fat;if(u)cout<<u<<" "<<Fat<<"\n";
for(int v:g[u])if(v^Fat)DFS(v,u);return;
}
int main(){
cin>>n>>m>>k>>w;
for(int x=1;x<=k;x++)for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)cin>>c[x][i][j];
for(int x=1;x<=k;x++)ln[++cnt]={0,x,n*m};
for(int x=1;x<k;x++)for(int y=x+1;y<=k;y++){
int tmp=0;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)
tmp+=(c[x][i][j]!=c[y][i][j]);
ln[++cnt]={x,y,tmp*w};
}
sort(ln+1,ln+cnt+1,cmp);for(int x=1;x<=k;x++)fa[x]=x;
for(int i=1;i<=cnt;i++){
if(!Can_It_Merge(ln[i].u,ln[i].v))continue;
Merge(ln[i].u,ln[i].v),sum+=ln[i].w;
g[ln[i].u].push_back(ln[i].v),g[ln[i].v].push_back(ln[i].u);
}
cout<<sum<<"\n";DFS(0,-1);
return 0;
}
例题——Daleks' Invasion (easy)
这个倒是一眼看出最小生成树。但是整个后面的思路需要一系列的推导!
你需要推式子,然后要理解最小生成树的本质,才能知道我们最终的那个答案究竟是啥。最后还要记得判,叫啥呢,无解。
很难吧。
上代码!
#include<bits/stdc++.h>
#define LL long long
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5 , M = 1e6+5;
struct line{int u,v,w;}ln[M];
int n,m,fa[N],num;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(line l1,line l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
void Merge(int u,int v){if(FF(u)!=FF(v))fa[FF(u)]=FF(v);return;}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++){int x,y,z;cin>>x>>y>>z;ln[i]={x,y,z};}
sort(ln+2,ln+m+1,cmp);for(int i=1;i<=n;i++)fa[i]=i;
for(int i=2;i<=m;i++){
Merge(ln[i].u,ln[i].v);++num;
if(FF(ln[1].u)==FF(ln[1].v)){cout<<ln[i].w<<"\n";return 0;}
}
if(num<n-1)cout<<"1000000000\n";
return 0;
}
例题——Omg Graph
这个的话,你在构建最小生成树的同时需要维护很多东西,然后才能计算出最终答案,有难度!
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5+5;
const LL Inf = 0x3f3f3f3f3f3f3f3f;
struct line{LL u,v,w;}ln[N];
LL T,n,m,ans,fa[N],mx[N],mn[N];
bool cmp(line l1,line l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
void Merge(int u,int v,LL K){
int fu=FF(u),fv=FF(v);if(fu==fv)return;
fa[fv]=fu;mx[fu]=max({mx[fu],mx[fv],K});
mn[fu]=min({mn[fu],mn[fv],K});return;
}
int main(){
cin>>T;
while(T--){
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>ln[i].u>>ln[i].v>>ln[i].w;
sort(ln+1,ln+m+1,cmp);ans=Inf;
for(int i=1;i<=n;i++)fa[i]=i,mx[i]=-Inf,mn[i]=Inf;
for(int i=1;i<=m;i++){
Merge(ln[i].u,ln[i].v,ln[i].w);
if(FF(1)==FF(n))ans=min(ans,mx[FF(1)]+mn[FF(1)]);
}
cout<<ans<<"\n";
}
return 0;
}
总结
生成树(为啥不叫最小生成树?因为也有最大生成树啊喂!)的运用很广,一般都是需要去挖掘生成树其本质,有时还需要维护更多东西。直白告诉你让你生成一棵生成树的题那是少之又少。
所以想要拿下生成树的所有题目,第一步要学会从题目中挖掘出生成树的影子,第二步要学会分析这个生成树具体都要维护些什么,第三步要能确保你的算法准确无误没有瑕疵,第四步才是上手写代码!