圆形牛棚

圆形牛棚

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

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

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

约翰想让第i个房间内恰好有ri头牛。

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

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

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

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

输入格式

第一行包含整数n

接下来n行,包含r1,...,rn

输出格式

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

数据范围

3n1000,

1ri100

输入样例:

5
4
7
8
6
4

输出样例:

48

样例解释

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

 

解题思路

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

  例如,如果我们选择r0作为入口,则代价大小为r00+r11+r22+...+rn1(n1)

  如果我们选择r1作为入口,则代价大小为r10+r21+...+rn1(n2)+r0(n1)。以此类推。

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

  需要注意的是需要对每个牛棚的下标编号进行取模n。当枚举到下标n1时,因为顺时针走动,因此下一个牛棚的下标编号应该为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(n)的算法思路和实现。

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

  下面我们定义P0为选择r0为入口的总代价,即P0=r00+r11+...+rn2(n2)+rn1(n1)

  接下来我们考虑一下P1怎么算,也就是怎么用P0来表示。可以发现,在上图中,用第2行减去第1行就可以得到P1P0的表达式,即P1=P0+(n1)r0(r1+r2+...+rn1)=P0+nr0(r0+r1+r2+...+rn1)=P0+nr0s这里定义s=r0+r1+...+rn1

  同理可得P2=P1+nr1s=P0+n(r0+r1)2s

  以此类推,我们可以发现Pi有两种表达式,Pi=Pi1+nri1sPi=P0+n(r0+r1+...+ri1)is

  其中对于第一种表达式,我们可以发现每一种状态都可以用上一种状态来表示,一共有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 }
复制代码

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

  首先在这个表达式中,我们是选择r0作为入口的,也就是选择P0作为初始状态,事实上我们可以选择任意一个Pi作为初始状态,因为这是一个环,每一个点的地位都是等同的。也就是说,无论我们选择的是P0还是P1还是其他作为初始状态,都是可以推出P0Pn1的。

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

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

  因为有Pi=P0+n(r0+r1+...+ri1)is,这就等价于n(r0+r1+...+ri1)is0

  进行移项和化简n(r0+r1+...+ri1)isr0+r1+...+ri1isnr0+r1+...+ri1irr0+r1+...+ri1ir0r0+r1+...+ri1iri0(r0r)+(r1r)+...+(ri1r)i0(r0r)+(r1r)+...+(ri1r)0上式中,由于i大于0,所以可以消去。同时定义r=sn,即平均值。

  这里的初始状态为P0,那么对于任意的i,上面的表达式中的每一项的和要满足大于等于0。更一般的情况,等价于我们要找的初始状态为k,满足Pk就是最小的代价,对于任意的i都要满足表达式(rk%nr)+(r(k+1)%nr)+...+(r(k+i1)%nr)0

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

  对于表达式式(rk%nr)+(r(k+1)%nr)+...+(r(k+i1)%nr)0要成立,相当于在这个长度为n的区间上,任意一个rir的前缀和都要大于等于0。这里记rir=ri,所以这就等价于在这个长度为n的区间上,任意一个ri的前缀和都要大于等于0。需要注意的是,现在线段的长度为环的两倍,不需要再取模了。

  所以可以在2n的区间定义前缀和si=r1+r2+...+ri(前缀和的下标从1开始)。等价于在这个长度为n区间上任意一个点的前缀和满足sisk10

  其中,对于点nsn=r1+r2+...+rn=snr=ss=0

  所以只要满足sk1是最小值,那么就可以保证在长度为n的区间,sisk10恒成立。因为在一个集合里面,集合中的任意一个元素减去集合中最小的那个元素,得到的值一定是大于等于0的。

  现在我们整理一下思路。

  首先,我们先把这个环延长为长度为2n的线段,然后算一下ri,接着算前缀和数组,在前缀和数组中找到一个k1满足sk1是最小的那个值(k1在区间0n1中),那么我们从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/

posted @   onlyblues  阅读(129)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示