[最小生成树] 篱笆

目录

题目描述

解题思路 

巧解思路


BZOJ  3075 篱笆



题目描述

农夫FJ的奶牛们有空旷恐惧症,所以,FJ打算在他的农场围上篱笆。他的农场是一个矩形区域。左上角的坐标是(0,0),右下角的坐标是(A,B),FJ修建了n(0<=n<=2000)个竖直的篱笆,其横坐标分别为a1,a2,a3,……,an,其中0<ai<A,,每一个篱笆从(ai,0)到(ai,B)也修建了m个水平的篱笆,其纵坐标为b1,b2,b3,……,bm,其中0<bi<B,每一个篱笆从(0,bi)到(A,bi)。这些篱笆把整个农场分成(n+1)*(m+1)个区域。

不幸的是FJ忘了在篱笆上装门了。这导致奶牛无法在不同的区域之间移动。于是他决定将某些篱笆拆掉。现在要使得所有的区域都联通,请问最少要拆掉多长的篱笆。

比如下面这个篱笆

+---+--+

|      |    |

+---+--+

|      |   |  

扫描二维码关注公众号,回复: 4765637 查看本文章

|      |   |

+---+--+

可以这么拆:

+---+--+

|          |  

+---+  +  

|          |  

|          |

+---+--+

输入

第一题包含四个数A,B,n,m。(0<=A,B<=1000000000).

接下来有两行,第二行n个数,表示a1,a2,……,an,表示竖直的n个篱笆的横坐标,第三行m个数,表示b1,b2,b3,……,bm,表示m个水平的篱笆的纵坐标。

输出

最少要拆除的篱笆的总长度。结果可能超过int。请用long long int

样例输入

15 15 5 2
2
5
10
6
4
11
3

样例输出

44 


解题思路 

首先,这道题的题目描述比较抽象,反正我是不能直接理解的(不排除有大佬能看懂的情况),所有我们就此样例来画一个图辅助理解:

很明显,这些篱笆将这一整块地分成了大大小小的几部分,那么我们可以直接得出,一共被分成了(n + 1) * (m + 1)块,而我们的任务就是把这些地给连接起来,使它们彼此之间可以联通,没错,就是最小生成树

相信最小生成树大家都会,但这道题的难点之一就在于如何来将这些篱笆分成的小块建为一个点呢?很容易想到将这些小块编号,再用Kruskal来求解。那么问题又出来了,怎么来建点呢?

我们就将这写小块按照从左到右,从上到下的顺序来编号,可以得出下图:

这个图就很直观的表现出了我们建点的方式,这样我们就可以得出这一个图:

1、如果是竖直的篱笆,假设为第i条,如果此时扫到了第j行,那么这条篱笆就连接 (i - 1) * (m + 1) + j i * (m + 1) + j 这两个点

2、如果是水平的篱笆,假设为第i条,如果此时扫到了第j列,那么这条篱笆就连接 (j - 1) * (m + 1) + i 和 (j - 1) * (m + 1) + i + 1 这两个点

所有我们就可以拿出所有的边,并且用Kruskal求解。

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
  
#define reg register
#define LL long long
#define M 2005
  
struct node{
    LL num;
    int l, r;
    node() {}
    node(LL Num, int L, int R) {
        num = Num;
        l = L;
        r = R;
    }
};
node dis[2 * M * M];
  
int A, B, n, m, len, k;
int a[M], b[M], p[M * M];
  
LL ans;
  
inline void read(int &num) {
    num = 0; int f = 1; char s = getchar();
    while(s < '0' || s > '9') {if(s == '-') f = -1; s = getchar();}
    while(s >= '0' && s <= '9') {num = (num << 3) + (num << 1) + s - 48; s = getchar();}
    num *= f;
}
  
inline void write(LL num) {
    if(num < 0) {num = -num; putchar('-');}
    if(num / 10) write(num / 10);
    putchar(num % 10 + 48);
}
  
inline void makeSet(int num) {
    for(reg int i = 0;i <= num;i ++)
        p[i] = i;
}
  
inline int findSet(int num) {
    if(p[num] != num) p[num] = findSet(p[num]);
    return p[num];
}
  
inline void unionSet(int num1, int num2) {
    int u = findSet(num1), v = findSet(num2);
    p[u] = v;
}
  
inline bool cmp(node idx1, node idx2) {
    return idx1.num < idx2.num;
}
  
int main() {
    read(A), read(B), read(n), read(m);
    for(reg int i = 1;i <= n;i ++)
        read(a[i]);
    for(reg int i = 1;i <= m;i ++)
        read(b[i]);
    sort(a + 1, a + 1 + n);    //初始输入的排序
    sort(b + 1, b + 1 + m);
    a[++ n] = A, b[++ m] = B;
    for(reg int i = 1;i <= n;i ++)    //建图
        for(reg int j = 1;j <= m;j ++) {
            if(j != 1)
                dis[++ len] = node(1ll * (a[i] - a[i - 1]), (i - 1) * m + j - 1, (i - 1) * m + j);
            if(i != 1)
                dis[++ len] = node(1ll * (b[j] - b[j - 1]), (i - 2) * m + j, (i - 1) * m + j);
        }
    sort(dis + 1, dis + 1 + len, cmp);    //对所有的边进行排序
    makeSet(n * m);
    for(reg int i = 1;i <= len;i ++) {    //Kruskal求解MST
        if(findSet(dis[i].l) != findSet(dis[i].r)) {
            unionSet(dis[i].l, dis[i].r);
            ans += 1ll * dis[i].num;
            k ++;
            if(k == n * m - 1)
                break;
        }
    }
    write(ans);
    putchar('\n');
    return 0;
}
 

但是,如果我们细细揣摩一下这个代码,会发现在排序时需要对8000000个数据排序,很明显会超时,所以,我们应该对这个代码进行一点改变,以防超时

通过观察,我们可以发现,同一列或者是同一行的边权是相同的,也就是说,如果有一条边被选中,那么与这条边处于同一列或者同一行的所有边都应该加入最小生成树里,所有我们就只需要存储 n + m 条边,在最后用循环处理这条边所在的行或者列

参考代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
using namespace std;
 
#define reg register
#define LL long long
#define M 2005
 
struct node{
    LL num;    //num表示篱笆长度,p表示在同一方向上的第几条,f记录水平和竖直
    int p, f;
    inline node() {}
    inline node(LL Num, int P, int F) {
        num = Num;
        p = P;
        f = F;
    }
    inline bool operator < (const node &rhs) const {    //重载排序
        return num < rhs.num;
    }
};
node t[M + M];
 
int A, B, n, m, k, lt;
int a[M], b[M], p[M * M];
 
LL ans;
 
inline void read(int &num) {
    num = 0; int f = 1; char s = getchar();
    while(s < '0' || s > '9') {if(s == '-') f = -1; s = getchar();}
    while(s >= '0' && s <= '9') {num = (num << 3) + (num << 1) + s - 48; s = getchar();}
    num *= f;
}
 
inline void wri(LL num) {
    if(num < 0) {num = -num; putchar('-');}
    if(num / 10) wri(num / 10);
    putchar(num % 10 + 48);
}
 
inline void write(LL num, char s) {
    wri(num);
    putchar(s);
}
 
inline void makeSet(int num) {
    for(reg int i = 0;i <= num;i ++)
        p[i] = i;
}
 
inline int findSet(int num) {
    if(p[num] != num) p[num] = findSet(p[num]);
    return p[num];
}
 
inline void unionSet(int num1, int num2) {
    int u = findSet(num1), v = findSet(num2);
    p[u] = v;
}
 
inline void solve(int idx1, int idx2, int num) {    //Kruskal的处理,将边加入最小生成树
    if(findSet(idx1) != findSet(idx2)) {
        unionSet(idx1, idx2);
        ans += num;
        k ++;
        if(k == n * m - 1) {
            write(ans, '\n');
            exit(0);
        }
    }
}
 
int main() {
    read(A), read(B), read(n), read(m);
    for(reg int i = 1;i <= n;i ++)
        read(a[i]);
    for(reg int i = 1;i <= m;i ++)
        read(b[i]);
    sort(a + 1, a + 1 + n);
    sort(b + 1, b + 1 + m);
    a[++ n] = A, b[++ m] = B;
    for(reg int i = 1;i <= n;i ++)
        t[++ lt] = node(a[i] - a[i - 1], i, 0);    //0代表水平的篱笆
    for(reg int i = 1;i <= m;i ++)
        t[++ lt] = node(b[i] - b[i - 1], i, 1);    //1代表竖直的篱笆
    sort(t + 1, t + 1 + lt);
    makeSet(n * m);
    for(reg int i = 1;i <= lt;i ++) {    //Kruskal求解
        if(t[i].f == 1)
            for(reg int j = 1;j <= n - 1;j ++) {
                int idx1 = (j - 1) * m + t[i].p, idx2 = j * m + t[i].p;
                solve(idx1, idx2, t[i].num);
 
            }
        if(t[i].f == 0)
            for(reg int j = 1;j <= m - 1;j ++) {
                int idx1 = (t[i].p - 1) * m + j, idx2 = (t[i].p - 1) * m + j + 1;
                solve(idx1, idx2, t[i].num);
            }
    }
}
 

相信大家可以发现,这样做的话时间复杂度会很高,虽然可以AC,但我们需要更高效的算法:

巧解思路

我们在之前已经说过如果有一条边被选中,那么这条边所在的那一列或者那一行都应该被选中,而此时我们要做的就是确定应该选出多少条边

我们借助下面这个图来理解(联通的部分用黄色来表示):

上图为我们假设已经拆了2列水平篱笆,拆了3行竖直篱笆(红色的篱笆),那么此时我们需要进行判断:

此时如果我们要拆一列水平方向的篱笆,也就是想让这一列的牧场都能联通,而这一列一共有 (m + 1) 块牧场,已经有三块牧场联通,所以应该拆掉 (m + 1 - 3) 条篱笆 (m为水平篱笆的条数)

如果我们要拆的是一行竖直方向的篱笆,就是想让这一行的牧场都能联通,这一行一共是有 (n + 1) 块牧场,已经有两块联通,所以要拆掉(n + 1 - 2) 条篱笆 (n为竖直篱笆的条数)

由此,我们就可以直接对一条篱笆进行处理,省去了循环的时间

参考代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
 
#define reg register
#define LL long long
#define M 2005
 
int A, B, n, m, id1, id2;
int a[M], b[M], ai[M], bi[M];
 
LL ans;
 
inline void read(int &x) {
    x = 0; int f = 1; char s = getchar();
    while(s < '0' || s > '9') {if(s == '-') f = -1; s = getchar();}
    while(s >= '0' && s <= '9') {x = (x << 3) + (x << 1) + s - 48; s = getchar();}
    x *= f;
}
 
inline void wri(LL x) {
    if(x < 0) {x = -x; putchar('-');}
    if(x / 10) wri(x / 10);
    putchar(x % 10 + 48);
}
 
inline void write(LL x, char s) {
    wri(x);
    putchar(s);
}
 
int main() {
    read(A), read(B), read(n), read(m);
    for(reg int i = 1;i <= n;i ++)
        read(a[i]);
    for(reg int i = 1;i <= m;i ++)
        read(b[i]);
    sort(a + 1, a + 1 + n);
    sort(b + 1, b + 1 + m);
    a[++ n] = A, b[++ m] = B;    //处理每条篱笆
    for(reg int i = 1;i <= n;i ++)
        ai[i] = a[i] - a[i - 1];
    for(reg int i = 1;i <= m;i ++)
        bi[i] = b[i] - b[i - 1];
    sort(ai + 1, ai + 1 + n);    //对篱笆长度进行排序
    sort(bi + 1, bi + 1 + m);
    id1 = id2 = 1;
    ans = ai[id1 ++] * (m - 1) * 1ll + bi[id2 ++] * (n - 1) * 1ll;    //我们必须保证至少有一行和一列的篱笆被拆掉,否则无法保证所有牧场联通
    while(id1 <= n && id2 <= m) {    //对每条边进行处理
        if(ai[id1] < bi[id2])
            ans += (ai[id1 ++] * (m - id2 + 1) * 1ll);
        else
            ans += (bi[id2 ++] * (n - id1 + 1) * 1ll);
    }
    write(ans, '\n');
    return 0;
}
 

猜你喜欢

转载自blog.csdn.net/weixin_43896346/article/details/85320429