圆形牛棚

圆形牛棚

作为当代建筑的爱好者,农夫约翰建造了一个完美圆环形状的新牛棚。

牛棚内部有$n$个房间,围成一个环形,按顺时针编号为$1 \sim n$,所有相邻房间之间的距离均为$1$。

每个房间都既有通向相邻两个房间的门,也有通向牛棚外部的门。

约翰想让第$i$个房间内恰好有$r_{i}$头牛。

为了让奶牛们有序的进入牛棚,他计划打开一个外门,让牛从该门进入。

然后,每头牛顺时针(即当$i<n$时,第$i$个房间只能走到第$i+1$个房间;当$i=n$时,第$i$个房间只能走到第$1$个房间)穿过房间,直到到达合适的房间为止。

约翰希望通过合理选择打开的门,使得所有奶牛的行走距离之和尽可能小(这里只考虑每头牛进入牛棚以后的行走距离)。

请确定他的奶牛需要行走的最小总距离。

输入格式

第一行包含整数$n$。

接下来$n$行,包含$r_{1}$,...,$r_{n}$。

输出格式

输出所有奶牛需要行走的最小总距离。

数据范围

$3 \leq n \leq 1000$,

$1 \leq r_{i} \leq 100$

输入样例:

5
4
7
8
6
4

输出样例:

48

样例解释

最佳方案是让奶牛们从第二个房间进入。

 

解题思路

  题意大概就是,选择一个房间作为入口,则代价就是每个房间牛的数量乘上该房间到入口房间的距离。

  例如,如果我们选择$r_{0}$作为入口,则代价大小为$r_{0} \cdot 0 + r_{1} \cdot 1 + r_{2} \cdot 2 + ... + r_{n - 1} \cdot \left( {n - 1} \right)$。

  如果我们选择$r_{1}$作为入口,则代价大小为$r_{1} \cdot 0 + r_{2} \cdot 1 + ... + r_{n - 1} \cdot \left( {n - 2} \right) + r_{0} \cdot \left( {n - 1} \right)$。以此类推。

  所以我们有个最朴素的做法,就是枚举所有牛棚将其选择作为入口,然后总代价又可以通过$O\left( n \right)$的复杂度算出来,所以整个算法的时间复杂度为$O\left( n^{2} \right)$。

  需要注意的是需要对每个牛棚的下标编号进行取模$n$。当枚举到下标$n - 1$时,因为顺时针走动,因此下一个牛棚的下标编号应该为$0$。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1010;
 6 
 7 int a[N];
 8 
 9 int main() {
10     int n;
11     scanf("%d", &n);
12     for (int i = 0; i < n; i++) {
13         scanf("%d", a + i);
14     }
15 
16     int ans = 5e7;
17     for (int i = 0; i < n; i++) {
18         int ret = 0;
19         for (int j = 1; j < n; j++) {
20             ret += a[(i + j) % n] * j;
21         }
22         ans = min(ans, ret);
23     }
24 
25     printf("%d", ans);
26 
27     return 0;
28 }

  下面进行优化,讲讲时间复杂度为$O\left( n \right)$的算法思路和实现。

  我们先把$n$个牛棚列出来,同时把每个牛棚到入口的距离列出来。

圆形牛棚

  下面我们定义$P_{0}$为选择$r_{0}$为入口的总代价,即$P_{0} = r_{0} \cdot 0 + r_{1} \cdot 1 + ... + r_{n - 2} \cdot \left( {n - 2} \right) + r_{n - 1} \cdot \left( {n - 1} \right)$。

  接下来我们考虑一下$P_{1}$怎么算,也就是怎么用$P_{0}$来表示。可以发现,在上图中,用第$2$行减去第$1$行就可以得到$P_{1} - P_{0}$的表达式,即$$\begin{align*} P_{1} &= P_{0} + \left( {n - 1} \right) \cdot r_{0} - \left( {r_{1} + r_{2} + ... + r_{n - 1}} \right) \\ &= P_{0} + n \cdot r_{0} - \left( {r_{0} + r_{1} + r_{2} + ... + r_{n - 1}} \right) \\ &= P_{0} + n \cdot r_{0} - s \end{align*}$$这里定义$s = r_{0} + r_{1} + ... + r_{n - 1}$。

  同理可得$$\begin{align*} P_{2} &= P_{1} + n \cdot r_{1} - s \\ &= P_{0} + n \cdot \left( {r_{0} + r_{1}} \right) - 2s \end{align*}$$

  以此类推,我们可以发现$P_{i}$有两种表达式,$$P_{i} = P_{i - 1} + n \cdot r_{i - 1} - s$$$$P_{i} = P_{0} + n \cdot \left( {r_{0}{+ r}_{1}{+ ... + r}_{i - 1}} \right) - i \cdot s$$

  其中对于第一种表达式,我们可以发现每一种状态都可以用上一种状态来表示,一共有$n$种状态(选择$n$个不同的牛棚作为入口),然后又因为一定存在其中一种代价最小的状态,因此我们把$n$种状态通过迭代的方式枚举一遍,就可以找到最小的那种状态,这个状态对应的值就是最小代价。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1010;
 6 
 7 int r[N];
 8 
 9 int main() {
10     int n;
11     scanf("%d", &n);
12 
13     int sum = 0, p = 0;
14     for (int i = 0; i < n; i++) {
15         scanf("%d", r + i);
16         sum += r[i];
17         p += r[i] * i;
18     }
19 
20     int ret = 2e9;
21     for (int i = 0; i < n; i++) {
22         p += n * r[i] - sum;
23         ret = min(ret, p);
24     }
25 
26     printf("%d", ret);
27 
28     return 0;
29 }

  接下来我们对第二种表达式进行更深一步的挖掘。

  首先在这个表达式中,我们是选择$r_{0}$作为入口的,也就是选择$P_{0}$作为初始状态,事实上我们可以选择任意一个$P_{i}$作为初始状态,因为这是一个环,每一个点的地位都是等同的。也就是说,无论我们选择的是$P_{0}$还是$P_{1}$还是其他作为初始状态,都是可以推出$P_{0} \sim P_{n - 1}$的。

  现在我们考虑一下,我们应该怎么选择,使得初始状态就是对应最小代价的那个状态。

  现在我们就假设$P_{0}$就是那个最小代价的状态。如果P_{0}是最小代价,那么对于任意一个的$P_{i}$,都应该满足$P_{i} - P_{0} \geq 0$(所有的状态都满足大于等于$0$)。

  因为有$P_{i} = P_{0} + n \cdot \left( {r_{0}{+ r}_{1}{+ ... + r}_{i - 1}} \right) - i \cdot s$,这就等价于$n \cdot \left( {r_{0}{+ r}_{1}{+ ... + r}_{i - 1}} \right) - i \cdot s \geq 0$。

  进行移项和化简$$\begin{align*} n \cdot \left( {r_{0}{+ r}_{1}{+ ... + r}_{i - 1}} \right) &\geq i \cdot s \\ \frac{r_{0} + r_{1} + ... + r_{i - 1}}{i} &\geq \frac{s}{n} \\ \frac{r_{0} + r_{1} + ... + r_{i - 1}}{i} &\geq \overset{-}{r} \\ \frac{r_{0} + r_{1} + ... + r_{i - 1}}{i} - \overset{-}{r} &\geq 0 \\ \frac{r_{0} + r_{1} + ... + r_{i - 1} - i \cdot \overset{-}{r}}{i} &\geq 0 \\ \frac{\left( {r_{0} - \overset{-}{r}} \right) + \left( {r_{1} - \overset{-}{r}} \right) + ... + \left( {r_{i - 1} - \overset{-}{r}} \right)}{i} &\geq 0 \\ \left( {r_{0} - \overset{-}{r}} \right) + \left( {r_{1} - \overset{-}{r}} \right) + ... + \left( {r_{i - 1} - \overset{-}{r}} \right) &\geq 0 \end{align*}$$上式中,由于$i$大于$0$,所以可以消去。同时定义$\overset{-}{r} = \frac{s}{n}$,即平均值。

  这里的初始状态为$P_{0}$,那么对于任意的$i$,上面的表达式中的每一项的和要满足大于等于$0$。更一般的情况,等价于我们要找的初始状态为$k$,满足$P_{k}$就是最小的代价,对于任意的$i$都要满足表达式$\left( {r_{k\% n} - \overset{-}{r}} \right) + \left( {r_{{({k + 1})}\% n} - \overset{-}{r}} \right) + ... + \left( {r_{{({k + i - 1})}\% n} - \overset{-}{r}} \right) \geq 0$。

  我们把环变成一条链,线段的长度为环的两倍,也就是$2n$。环上从任意一点走长度为$n$的一段,等价于从线段中前一半长度的区间中找一个点,划出长度为$n$的一段。

圆形牛棚

  对于表达式式$\left( {r_{k\% n} - \overset{-}{r}} \right) + \left( {r_{{({k + 1})}\% n} - \overset{-}{r}} \right) + ... + \left( {r_{{({k + i - 1})}\% n} - \overset{-}{r}} \right) \geq 0$要成立,相当于在这个长度为$n$的区间上,任意一个$r_{i} - \overset{-}{r}$的前缀和都要大于等于$0$。这里记$r_{i} - \overset{-}{r} = {r'}_{i}$,所以这就等价于在这个长度为$n$的区间上,任意一个${r'}_{i}$的前缀和都要大于等于$0$。需要注意的是,现在线段的长度为环的两倍,不需要再取模了。

  所以可以在$2n$的区间定义前缀和${s'}_{i} = {r'}_{1} + {r'}_{2} + ... + {r'}_{i}$(前缀和的下标从$1$开始)。等价于在这个长度为$n$区间上任意一个点的前缀和满足${s'}_{i} - {s'}_{k - 1} \geq 0$。

  其中,对于点$n$,${s'}_{n} = {r'}_{1} + {r'}_{2} + ... + {r'}_{n} = s - n \cdot \overset{-}{r} = s - s = 0$。

  所以只要满足${s'}_{k - 1}$是最小值,那么就可以保证在长度为$n$的区间,${s'}_{i} - {s'}_{k - 1} \geq 0$恒成立。因为在一个集合里面,集合中的任意一个元素减去集合中最小的那个元素,得到的值一定是大于等于$0$的。

  现在我们整理一下思路。

  首先,我们先把这个环延长为长度为$2n$的线段,然后算一下${r'}_{i}$,接着算前缀和数组,在前缀和数组中找到一个$k-1$满足${s'}_{k-1}$是最小的那个值($k-1$在区间$0 \sim n-1$中),那么我们从$k$开始的,长度为$n$的线段,即以$k$为入口的代价最小。代码思路同上。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 2010;
 6 
 7 int r[N];
 8 double s[N];
 9 
10 int main() {
11     int n;
12     scanf("%d", &n);
13 
14     double sum;
15     for (int i = 1; i <= n; i++) {
16         scanf("%d", r + i);
17         r[n + i] = r[i];
18         sum += r[i];
19     }
20 
21     double avg = sum / n;
22     for (int i = 1; i <= 2 * n; i++) {
23         s[i] = s[i - 1] + r[i] - avg;
24     }
25 
26     int k = 0;
27     for (int i = 0; i < n; i++) {
28         if (s[i] < s[k]) k = i;
29     }
30     k++;
31 
32     int ret = 0;
33     for (int i = 0; i < n; i++) {
34         ret += i * r[k + i];
35     }
36     printf("%d", ret);
37 
38     return 0;
39 }

 

参考资料

  AcWing 1843. 圆形牛棚(寒假每日一题2022):https://www.acwing.com/video/3687/

上一篇:【笔记】正则表达式


下一篇:【SLAM基础】【矩阵】矩阵基础相关概念总结