1. 程式人生 > 其它 >社交距離 I

社交距離 I

社交距離 I

一種新型疾病,COWVID-19,開始在全世界的奶牛之間傳播。

Farmer John 正在採取儘可能多的預防措施來防止他的牛群被感染。

Farmer John 的牛棚是一個狹長的建築物,有一排共 $N$ 個牛欄。

有些牛欄裡目前有奶牛,有些目前空著。

得知“社交距離”的重要性,Farmer John 希望使得 $D$ 儘可能大,其中 $D$ 為最近的兩個有奶牛的牛欄的距離。

例如,如果牛欄 $3$ 和 $8$ 是最近的有奶牛的牛欄,那麼 $D=5$。

最近兩頭奶牛新來到 Farmer John 的牛群,他需要決定將她們分配到哪兩個之前空著的牛欄。

請求出他如何放置這兩頭新來的奶牛,使得 $D$ 仍然儘可能大。

Farmer John 不能移動任何已有的奶牛;他只想要給新來的奶牛分配牛欄。

輸入格式

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

下一行包含一個長為 $N$ 的字串,由 $0$ 和 $1$ 組成,描述牛棚裡的牛欄。

$0$ 表示空著的牛欄,$1$ 表示有奶牛的牛欄。

字串中包含至少兩個 $0$,所以有足夠的空間安置兩頭新來的奶牛。

輸出格式

輸出 Farmer John 以最優方案在加入兩頭新來的奶牛後可以達到的最大 $D$ 值(最近的有奶牛的牛欄之間的距離)。

資料範圍

$2 \leq N \leq {10}^{5}$

輸入樣例:

14
10001001000010

輸出樣例:

2

樣例解釋

在這個例子中,Farmer John 可以以這樣的方式加入奶牛,使得牛欄分配變為 $10x010010x0010$,其中 $x$ 表示新來的奶牛。

此時 $D=2$。

不可能在加入奶牛之後取到更大的 $D$ 值。

 

解題思路

  分情況來討論,一共有兩種情況。

  1. 兩頭牛在同一個區間。
  2. 兩頭牛在不同的區間。

  我們把首尾區間和中間的區間分開來看。

  對於首尾區間,如果要把兩頭牛都放在首區間中,很明顯,當把一頭牛放在左端點時,另一頭牛距離左右兩頭牛的距離才會儘可能的大。那麼另一頭牛應該放在什麼地方?假設這頭牛距離左端點的牛的距離為$d_{1}$,距離$x_{1}$的距離為$d_{2}$,那麼應該有$d_{1} + d_{2} = x_{1} - 1$。當$d_{1}$和$d_{2}$越接近時,$d_{1}$和$d_{2}$中的最小值才會越大,我們讓$d_{1} = d_{2} = \left\lfloor {\frac{x_{1}-1}{2}} \right\rfloor $。如果得到的距離是整數的話,那麼$d_{1}$和$d_{2}$取相等。如果不是整數,那麼下取整,比如$5$,那麼會得到一個是$2$,另一個是$3$,我們應該取$2$。

  尾區間同理,最小的距離應該是$\left\lfloor {\frac{n-x_{m}}{2}} \right\rfloor$,$x_{m}$是序列中最後一頭牛的下標,在上面的序列中$m=5$。

  對於中間的區間$\left[ {x_{i}, x_{i+1}} \right]$,當放入兩頭牛後會有三段距離,一段是一頭牛與區間左邊的牛的距離$d_{1}$,一段是兩頭牛之間的距離$d_{2}$,一段是另一頭牛與區間右邊的牛的距離$d_{3}$。同理會有$d_{1} + d_{2} + d_{3} = x_{i+1} - x_{i}$。要使這三個數的最小值最大,應該有讓$d_{1} = d_{2} = d_{3}$,假設$d_{1}$的值最小,那麼有$d_{1} = \left\lfloor {\frac{x_{i+1}-x_{i}}{3}} \right\rfloor$。

  現在我們要看放在哪一個區間更好,也就是可以取到最大的最小值。

  先看把牛放入中間區間的情況,假設在沒放牛之前的初始情況,任意兩個牛之間的距離為${x'}_{i}$,其中${x'}_{1} = x_{2} - x_{1}$,以此類推。假設我們把牛放入第$i$個區間,得到的最小距離是$y_{i} = \left\lfloor {\frac{x_{i+1}-x_{i}}{3}} \right\rfloor$,那麼最後所有牛間的最小值就應該是$min\left\{ {{x'}_{1}, {x'}_{2}, \dots, y_{i}, {x'}_{i+1}, \dots, {x'}_{m}} \right\}$,由因為${x'}_{i} > y_{i}$,因此我們可以把${x'}_{i}$放入進行比較,這並不會影響最終答案,因此就有$min\left\{ {{x'}_{1}, {x'}_{2}, \dots, {x'}_{m}}, y_{i} \right\}$。可以發現,這對於任何中間的區間都成立,只需要把$y_{i}$改成對應放入的區間即可。而對於首尾區間的情況,同樣只需要把$y_{i}$換成首尾區間的結果。因此為了使得最小的距離最大,應該取所有的$y_{i}$的最大值。假設一開始已存在的牛之間的最小距離為$x_{min} = min\left\{ {{x'}_{1}, {x'}_{2}, \dots, {x'}_{m}} \right\}$,因此對於把牛都放入同一個區間的情況,能夠取到的最大距離就為$min \left\{ {x_{min}, max \left\{ y_{i} \right\}} \right\}$。

  然後是把牛放入不同區間的情況。如果放在首尾區間,那麼應該放在左端點或右端點上,距離分別為$x_{1} - 1$和$n - x_{m}$。如果是放在中間的中,那麼最小距離的最大值為$\left\lfloor \frac{x_{i+1} - x_{i}}{2} \right\rfloor$。我們在每一個區間中都放一頭牛得到上述的距離,最後在這些距離中找到最大值和次大值,那麼我們應該把這兩頭牛分別放入這兩個對應區間中,最小距離可以取到最大值。假設最大值和次大值分別為$y_{1}$和$y_{2}$,和第一種情況一樣$y_{i} < {x'}_{i}$,$y_{j} < {x'}_{j}$,因此能夠取到的最大距離就為$min \left\{ {x_{min}, y_{i}, y_{j}} \right\}$,即$min \left\{ {x_{min}, y_{j}} \right\}$。

  最後的答案應該就是求出這兩種情況的結果中,取最小值。

  如果不存在首區間或尾區間,根據上面的公式,會有$x_{1} - 1 = 0$及$n - x_{m} = 0$,對於兩種放法,我們都是取所有放入後的距離的最大值,而$0$並不會影響取最大值,因此也是成立的。

  AC程式碼如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1e5 + 10;
 6 
 7 char str[N];
 8 int pos[N];
 9 
10 int main() {
11     int n;
12     scanf("%d %s", &n, str + 1);
13     
14     int cnt = 0;
15     for (int i = 1; i <= n; i++) {
16         if (str[i] == '1') pos[++cnt] = i;  // 記錄初始時有牛的位置
17     }
18     
19     if (cnt == 0) { // 如果一頭牛的沒有,那麼兩頭牛應該放在左右端點
20         printf("%d", n - 1);
21         return 0;
22     }
23     
24     int minx = N;   // 初始時,兩頭牛之間的最小距離
25     for (int i = 1; i < cnt; i++) {
26         minx = min(minx, pos[i + 1] - pos[i]);
27     }
28     
29     // 把兩頭牛放入同一個區間
30     int y = max(pos[1] - 1 >> 1, n - pos[cnt] >> 1);    // 先對首尾區間這種特殊情況進行判斷
31     for (int i = 1; i < cnt; i++) {
32         y = max(y, (pos[i + 1] - pos[i]) / 3);  // 處理中間區間的情況
33     }
34     
35     // 把兩頭牛放入不同的區間
36     int y1 = pos[1] - 1, y2 = n - pos[cnt]; // 先對首尾區間進行特判
37     if (y1 < y2) swap(y1, y2);  // y1是最大值,y2是次大值
38     for (int i = 1; i < cnt; i++) {
39         int t = pos[i + 1] - pos[i] >> 1;   // 處理中間區間的情況
40         
41         // 維護最大值和次大值
42         if (t >= y1) y2 = y1, y1 = t;
43         else if (t > y2) y2 = t;
44     }
45     
46     printf("%d", min(minx, max(y, y2)));
47     
48     return 0;
49 }

  這題還可以用二分。一開始的搜尋的左右端點值為$left = 1, right = x_{min}$。

  對於check函式,假設要判斷的最小距離的最大值能否為$x$,先從頭到尾掃描一遍序列,然後如果某個位置$i$為$0$,那麼把牛放入這個位置,然後判斷與前一頭牛和後一頭牛的距離,應該要滿足$i - pre \geq x ~ \&\& ~ i - ne_{i} \geq x$,其中$pre$表示前一頭牛的位置,$ne_{i}$為在$i$這個位置的後一頭牛的位置,可以預處理出來。只有滿足這個條件,才可以把牛放入,並更新pre為$i$。最後判斷能否放入兩頭牛。

  AC程式碼如下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 1e5 + 10, INF = 0x3f3f3f3f;
 7 
 8 int n;
 9 char str[N];
10 int a[N], ne[N];
11 
12 bool check(int x) {
13     int cnt = 0;
14     for (int i = 1, pre = -INF; i <= n; i++) {  // 一開始pre設為負無窮,意味著在最左邊有一頭牛
15         if (a[i] == 0 && i - pre >= x && ne[i] - i >= x) {
16             if (++cnt >= 2) return true;
17             pre = i;
18         }
19         else if (a[i] == 1) {
20             pre = i;
21         }
22     }
23     
24     return false;
25 }
26 
27 int main() {
28     scanf("%d %s", &n, str + 1);
29     
30     int left = 1, right = N;
31     for (int i = 1, pre = -INF; i <= n; i++) {
32         a[i] = str[i] - '0';
33         if (a[i] == 1) {
34             right = min(right, i - pre);
35             pre = i;
36         }
37     }
38     
39     // 預處理出來每頭牛的下一頭牛的位置,在正無窮處設一頭牛
40     memset(ne, 0x3f, sizeof(ne));
41     for (int i = n; i; i--) {
42         if (a[i] == 0) {
43             if (a[i + 1] == 1) ne[i] = i + 1;
44             else ne[i] = ne[i + 1];
45         }
46     }
47     
48     while (left < right) {
49         int mid = left + right + 1 >> 1;
50         if (check(mid)) left = mid;
51         else right = mid - 1;
52     }
53     
54     printf("%d", left);
55     
56     return 0;
57 }

 

參考資料

  AcWing 1659. 社交距離 I(春季每日一題2022):https://www.acwing.com/video/3747/

  AcWing 1659. 社交距離 I 二分:https://www.acwing.com/solution/content/103535/