NOIP专题复习2 图论-生成树

目录

一、知识概述

在上一节中,我们讲了最短路径问题。而这一节,我们要讲和最短路径相关的一个知识点:生成树。

1、什么是生成树呢?

• 对于一个n个点,m条边的无向图,我们需要求出它的生成树,即边为n-1条且均属于m条边的无环连通图。
• 我们一般有最小生成树和最大生成树,即边权和最小或最大的生成树我们以最小生成树为例例讲几种常见的求生成树的方法。

2、生成树长什么样呢?

最小生成树

二、典型例题

1、口袋的天空

题目背景

小杉坐在教室里,透过口袋一样的窗户看口袋一样的天空。
有很多云飘在那里,看起来很漂亮,小杉想摘下那样美的几朵云,做成棉花糖。

题目描述

给你云朵的个数N,再给你M个关系,表示哪些云朵可以连在一起。
现在小杉要把所有云朵连成K个棉花糖,一个棉花糖最少要用掉一朵云,小杉想知道他怎么连,花费的代价最小。

输入输出格式

输入格式:
每组测试数据的第一行有三个数N,M,K(1<=N<=1000,1<=M<=10000,1<=K<=10).
接下来M个数每行三个数X,Y,L,表示X云和Y云可以通过L的代价连在一起.1<=X,Y<=N,0<=L<10000。30%的数据N<=100,M<=1000。

输出格式:
对每组数据输出一行,仅有一个整数,表示最小的代价。
如果怎么连都连不出K个棉花糖,请输出'No Answer'。

输入输出样例

输入样例#1:
3 1 2
1 2 1
输出样例#1:
1

三、算法分析

(一)Prim算法

最小生成树

(二)Kruskal

1、算法简述

此处输入图片的描述

2、问题解决(例题1)

这道题可以说是Kruskal的模板题吧。

Kruskal核心思想:
一开始一共有n个集合。我们到最后要只剩下k个集合,故我们要减少n-k个集合。
那怎么才能减少这些集合呢?
把不属于同一个集合的两个集合合并(即把不直接或间接连的两条个点合并)。

实现方法:
我们就可以用并查集来实现。
因为要求最小生成树,所以按照边权值把边进行排序(下讲从小到大),也是一种贪心思想。
然后一条边一条边的处理,如果这一条边的两个顶点不在同一个集合,我们就把他合并,并且加上权值。知道只剩k个集合,就break。
这我已经讲的很详细了。
具体看代码吧。

#include<bits/stdc++.h>
using namespace std;
int n,m,k,s,ans,fa[10005];
struct zwc
{
    int x,y,z;
}a[10005];
bool cmp(zwc w,zwc e)
{
    return w.z<e.z;
}
int findfa(int p)
{
    if (p!=fa[p]) fa[p]=findfa(fa[p]);
    return fa[p];
}
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    for (int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
    }
    for (int i=1;i<=n;i++)
    fa[i]=i;
    sort(a+1,a+m+1,cmp);
    for (int i=1;i<=m;i++)
    {
        if (s==n-k) break;
        int p1=findfa(a[i].x);
        int p2=findfa(a[i].y);
        if (p1!=p2)
        {
            s++;
            fa[p2]=p1;
            ans+=a[i].z;
        }
    }
    if (s==n-k)
    printf("%d",ans);
    else printf("No Answer");
}

四、算法应用

1、[NOIP2013]货车运输

题目描述

A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。现在有 q 辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。

输入

第一行有两个用一个空格隔开的整数 n,m,表示 A 国有 n 座城市和 m 条道路。
接下来 m 行每行 3 个整数 x、y、z,每两个整数之间用一个空格隔开,表示从 x 号城市到 y 号城市有一条限重为 z 的道路。注意:x 不等于 y,两座城市之间可能有多条道路。
接下来一行有一个整数 q,表示有 q 辆货车需要运货。
接下来 q 行,每行两个整数 x、y,之间用一个空格隔开,表示一辆货车需要从 x 城市运输货物到 y 城市,注意:x 不等于 y。

输出

输出共有 q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。如果货车不能到达目的地,输出-1。

样例输入

4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3

样例输出

3
-1
3

提示

对于 30%的数据,0 < n < 1,000,0 < m < 10,000,0 < q < 1,000;
对于 60%的数据,0 < n < 1,000,0 < m < 50,000,0 < q < 1,000;
对于 100%的数据,0 < n < 10,000,0 < m < 50,000,0 < q < 30,000,0 ≤ z ≤ 100,000。

Solution

于是我们思考,可以发现有一些权值较小的边是不会被走过的。正如样例中的第三条边,就算有其他的很多条边,这条边无论如何也是不会被走过的。于是我们想到了可以将图中这样的边去掉,按照这个思路我们便想到了构造最大生成树,将其余的边去除。

得到了这样一个树之后,我们便考虑如何求出两个节点之间最小边权的最大值(即为题中的最大载重),因为这两点之间的路径是唯一的,我们只需要找出这条路径便可以得到答案。我们可以通过LCA来做到这一点,我求LCA的方法是先从每一个根节点进行搜索,求出节点深度等信息,然后利用这些信息进行树上倍增。

于是我们可以得出大体思路:首先重新建图,构造出最大生成树,然后在最大生成树上求LCA来回答询问。

#include<bits/stdc++.h>
using namespace std;
struct zwc
{
    int x,y,z;
}a[100005];
int tot,n,m,Next[100005],head[100005],to[100005],f[100005],q,val[100005],fa[50005][23],dep[100005],w[50005][23];
bool vis[100005];
bool cmp(zwc x,zwc y)
{
    return x.z>y.z;
}
int findfa(int x)
{
    if (f[x]!=x) f[x]=findfa(f[x]);
    return f[x];
}
void add(int x,int y,int z)
{
    tot++;
    Next[tot]=head[x];
    to[tot]=y;
    val[tot]=z;
    head[x]=tot;
}
void dfs(int x)
{
    vis[x]=true;
    for (int i=head[x];i;i=Next[i])
    {
        int u=to[i];
        if (vis[u]) continue;
        dep[u]=dep[x]+1;
        fa[u][0]=x;
        w[u][0]=val[i];
        dfs(u);
    }
}
int lca(int x,int y)
{
    if (findfa(x)!=findfa(y)) return -1;
    int ans=1000000000;
    if (dep[x]>dep[y]) swap(x,y);
    for (int i=20;i>=0;i--)
    {
        if (dep[fa[y][i]]>=dep[x])
        {
            ans=min(ans,w[y][i]);
            y=fa[y][i];
        }
    }
    if (x==y) return ans;
    for (int i=20;i>=0;i--)
    {
        if (fa[x][i]!=fa[y][i])
        {
            ans=min(ans,min(w[x][i],w[y][i]));
            x=fa[x][i];
            y=fa[y][i];
        }
    }
    ans=min(min(w[x][0],w[y][0]),ans);
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
    }
    //kruskal
    sort(a+1,a+1+m,cmp);
    for (int i=1;i<=n;i++)
    f[i]=i;
    for (int i=1;i<=m;i++)
    {
        int p1=findfa(a[i].x);
        int p2=findfa(a[i].y);
        if (p1!=p2)
        {
            f[p1]=p2;
            add(a[i].x,a[i].y,a[i].z);
            add(a[i].y,a[i].x,a[i].z);
        }
    }
    //倍增+LCA 
    for (int i=1;i<=n;i++)
    {
        if (!vis[i])
        {
            dep[i]=1;
            dfs(i);
            fa[i][0]=i;
            w[i][0]=1000000000;
        }
    }
    for (int j=1;j<=20;j++)
    for (int i=1;i<=n;i++)
    {
        fa[i][j]=fa[fa[i][j-1]][j-1];
        w[i][j]=min(w[i][j-1],w[fa[i][j-1]][j-1]);
    }
    scanf("%d",&q);
    for (int i=1;i<=q;i++)
    {
        int x,y=0;
        scanf("%d%d",&x,&y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}

五、算法拓展

1977: [BeiJing2010组队] 次小生成树 Tree

  Time Limit: 10 Sec Memory Limit: 512 MB

Description

  小 C 最近学了很多最小生成树的算法,Prim 算法、Kurskal 算法、消圈算法等等。 正当>小 C 洋洋得意之时,小 P 又来泼小 C 冷水了。小 P 说,让小 C 求出一个无向图的次小生成
树,而且这个次小生成树还得是严格次小的,也就是说: 如果最小生成树选择的边集是 >EM,严格次小生成树选择的边集是 ES,那么需要满足:
此处输入图片的描述
这下小 C 蒙了,他找到了你,希望你帮他解决这个问题。
  

Input

  第一行包含两个整数N 和M,表示无向图的点数与边数。 接下来 M行,每行 3个数x y z >表示,点 x 和点y之间有一条边,边的权值为z。。
  

Output

  包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)
  

Sample Input 1

5 6
1 2 1
1 3 2
2 4 3
3 5 4
3 4 3
4 5 6

Sample Output 1

11

  

HINT

  数据中无向图无自环;
  50% 的数据N≤2 000 M≤3 000;
  80% 的数据N≤50 000 M≤100 000;
  100% 的数据N>≤100 000 M≤300 000 ,边权值非负且不超过 10^9 。

Solution

先求出最小生成树,然后枚举每条不在该树上的边  
求出此边两点的树上路径中小于此边长度的最大的边(题目要求严格次小)
可用树上倍增解决,因为最大的边可能等于枚举的边,所以还要求出严格次大的边

猜你喜欢

转载自www.cnblogs.com/zwcblog/p/9347782.html
今日推荐