蹄球

蹄球

为了准备即将到来的蹄球锦标赛,Farmer John 正在训练他的 $N$ 头奶牛(方便起见,编号为 $1…N$)进行传球。

这些奶牛在牛棚一侧沿直线排列,第 $i$ 号奶牛位于距离牛棚 $x_{i}$ 的地方。

每头奶牛都在不同的位置上。

在训练开始的时候,Farmer John 会将若干个球传给不同的奶牛。

当第 $i$ 号奶牛接到球时,无论是从 Farmer John 或是从另一头奶牛传来的,她会将球传给最近的奶牛(如果有多头奶牛与她距离相同,她会将球传给这些奶牛中最左边的那头奶牛。)。

为了使所有奶牛都有机会练习到传球,Farmer John 想要确保每头奶牛都持球至少一次。

帮助他求出为了达到这一目的他开始时至少要传出的球的数量。

假设他在开始的时候能将球传给最适当的一组奶牛。

输入格式

输入的第一行包含 $N$。

第二行包含 $N$ 个用空格分隔的整数,其中第 $i$ 个整数为 $x_{i}$。

输出格式

输出 Farmer John 开始的时候最少需要传出的球的数量,使得所有奶牛至少持球一次。

数据范围

$1 \leq N \leq 100$,
$1 \leq x_{i} \leq 1000$

输入样例:

5
7 1 3 11 4

输出样例:

2

 

基环树

  每个结点的出度均为$1$,满足这个性质的图叫基环树。

  相当于每个结点都有一个父节点。在基环树中一共有$n$个结点,$n$条边,存在且只存在一个环。类似于这样的图:

 

  一个环上挂着若干棵树。

 

解题思路

   根据题目性质,当每头牛接到球后,一定会传给其两边的其中一头牛。如果我们把每头牛看作是一个点,那么对应的是每个点的出度必定为$1$。同时还可以发现,每个点的入度最大为$2$(这个点两边的点都把球传到这个点)。边的方向就是传球的方向。

  所以我们可以发现每个点的出度必定为$1$,入度不超过$2$。这个图就是一个基环树的模型。

  同时根据题目性质我们可以发现,当$n > 1$时,图一定存在环,且环的大小为$2$(指环中结点的个数为$2$)。这是因为球只能够往两边的一个方向传,等价于一个点只能够指向其左右两边的其中一个点。如果一直往同一个方向传球,又因为数轴上的点是有限的,当传到最后一个点时,那么一定会往相反的方向传,这就会形成一个大小为$2$的环了。当然在传的过程中如果某一个点发现往相反的方向传球距离会更小,那么就会往相反的方向传球,这也会形成一个大小为$2$的环。

  对于这个题目,我们可以发现基环树有这三种类型,也就是根据环挂树的数量来分。

  对于环中的两个结点,每个结点最多只会挂一棵树。这是因为在这道题目中,我们分析得到每个结点的入度最大为$2$,又因为这个结点在大小为$2$环中,环中的另外一个结点会指向这个结点,因此最多还能挂一棵树。如果再挂一棵树的话入度就为$3$了。

  下面我们分析传球数量的问题。

  我们分析上图这$3$种情况。首先看最右边那个只有一个环的图,此时我们只需要往这个环的任意一个结点传一个球就可以实现两个结点都可以传一次球,因为球会在这两个结点循环传球。

  然后再看剩下的两个图,我们应该选择在一开始给入度为$0$的点传球。因为球是往边的方向传的,对于入度为$0$的点是不可能被传到球的,所以这就需要我们一开始给入度为$0$的点传球,这样球再顺着边的方向把球传给剩余的点,所有的结点至少会传一次球。

  总结一下,其实可以发现一共就两种情况。由于图可能不连通,对于每一个块,如果只有两个结点,我们只需要传一个球。如果这个环上有树的话(图中结点数量不为$2$),则找到所有入度为$0$的结点,传入对应数量的球。

  一开始我想了很久才抽象出这个模型,然后又在判断图是两类中的哪一类这个问题上想了很久。最后我是用并查集来实现判断的。具体思路是对于每一个结点,如果它是属于某一个图的结点,就把它并到对应的那个点集中。同时并查集还会维护一个数组,用来记录这个图中结点的个数。最后在把所有的点扫描一遍,如果这个结点的入度为$0$,则答案加$1$;或者这个结点属于某个图的那个代表结点(根节点)且这个图中结点数量为$2$,答案也要加$1$。

  AC代码如下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 110;
 7 
 8 int a[N], deg[N];
 9 int fa[N], cnt[N];
10 
11 int find(int x) {
12     return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
13 }
14 
15 int main() {
16     int n;
17     scanf("%d", &n);
18     for (int i = 1; i <= n; i++) {
19         scanf("%d", a + i);
20     }
21     a[0] = -2e9, a[n + 1] = 2e9;
22     
23     sort(a + 1, a + n + 1);
24     
25     for (int i = 1; i <= n; i++) {
26         fa[i] = i;
27         cnt[i] = 1;
28     }
29     
30     for (int i = 1; i <= n; i++) {
31         int k = a[i] - a[i - 1] <= a[i + 1] - a[i] ? i - 1 : i + 1;
32         deg[k]++;
33         
34         int px = find(i), py = find(k);
35         if (px != py) cnt[px] += cnt[py];
36         fa[py] = px;
37     }
38     
39     int ret = 0;
40     for (int i = 1; i <= n; i++) {
41         if (deg[i] == 0 || fa[i] == i && cnt[i] == 2) ret++;
42     }
43     
44     printf("%d", ret);
45     
46     return 0;
47 }

  上面的写法麻烦一些。其实还有一种方法可以用来判断一个图是否为大小为$2$的环。我们可以发现,如果这个结点在一个大小为$2$的环上,那么从这个结点走一步到另外一个结点,然后再走一步,就会回到原来自己那个点上。所以判断一个图是不是环,只需要沿着后继走两步,看看是不是回到自己就可以了,同时还要满足这个结点以及后继结点的入度均为$1$。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 110;
 6 
 7 int a[N], ne[N], deg[N];
 8 
 9 int main() {
10     int n;
11     scanf("%d", &n);
12     for (int i = 1; i <= n; i++) {
13         scanf("%d", a + i);
14     }
15     a[0] = -2e9, a[n + 1] = 2e9;
16     
17     sort(a + 1, a + n + 1);
18     
19     for (int i = 1; i <= n; i++) {
20         int k = a[i] - a[i - 1] <= a[i + 1] - a[i] ? i - 1 : i + 1;
21         ne[i] = k;
22         deg[k]++;
23     }
24     
25     int ret = 0;
26     for (int i = 1; i <= n; i++) {
27         // 因为只需给环传一个球,等价于环中的两个结点的贡献率分别为0.5,为了保证精度,我们把传一次球变为传两次球,最后答案除2就好了
28         if (deg[i] == 0) ret += 2;
29         else if (ne[ne[i]] == i && deg[i] == 1 && deg[ne[i]] == 1) ret++;
30     }
31     printf("%d", ret >> 1);
32     
33     return 0;
34 }

 

参考资料

  AcWing 1738. 蹄球(寒假每日一题2022):https://www.acwing.com/video/3701/

posted @ 2022-02-10 21:10  onlyblues  阅读(71)  评论(0编辑  收藏  举报
Web Analytics