上课睡觉

上课睡觉

有 $N$ 堆石子,每堆的石子数量分别为 $a_1,a_2, \ldots, a_N$。

你可以对石子堆进行合并操作,将两个相邻的石子堆合并为一个石子堆,例如,如果 $a=[1,2,3,4,5]$,合并第 $2,3$ 堆石子,则石子堆集合变为 $a=[1,5,4,5]$。

我们希望通过尽可能少的操作,使得石子堆集合中的每堆石子的数量都相同。

请你输出所需的最少操作次数。

本题一定有解,因为可以将所有石子堆合并为一堆。

输入格式

第一行包含整数 $T$,表示共有 $T$ 组测试数据。

每组数据第一行包含整数 $N$。

第二行包含 $N$ 个整数 $a_1,a_2, \ldots, a_N$。

输出格式

每组数据输出一行结果。

数据范围

$1 \leq T \leq 10$ ,
$1 \leq N \leq {10}^{5}$,
$0 \leq a_i \leq {10}^{6}$,
$\sum\limits_{i=1}^{n}{a_i} \leq {10}^{6}$,

每个输入所有 $N$ 之和不超过 ${10}^{5}$。

输入样例:

3
6
1 2 3 1 1 1
3
2 2 3
5
0 0 0 0 0

输出样例:

3
2
0

样例解释

第一组数据,只需要用 $3$ 个操作来完成:

   1 2 3 1 1 1
-> 3 3 1 1 1
-> 3 3 2 1
-> 3 3 3

第二组数据,只需要用 $2$ 个操作来完成:

   2 2 3
-> 2 5
-> 7

第三组数据,我们什么都不需要做。

 

解题思路

  先给出我一开始的思路。题目要求每次只能合并相邻的两堆石子,并且最终每堆的价值是相同的,这个问题就等价于能否将序列分成若干段,使得每一段的和都等于同一个数。

  因此容易想到先枚举出第一段,然后看能不能把剩余的部分分成与第一段总和相同的若干段,这种做法的时间复杂度为$O(n^2)$。

  假设第一段是区间$[1,i]$,第一段的总和是$x$,整个序列的和为$\text{sum}$,那么区间$[i+1, n]$就应该被划分为$\frac{\text{sum}}{x}-1$段,并且还要满足$x \mid \text{sum}$。假设有序列的前缀和数组$s$,那么这个条件就等价于$s_i \mid s_n$,而区间$[i+1, n]$恰好被划分成$\frac{\text{sum}}{x}-1$段,就等价于在$[i+1, n]$中存在$\frac{\text{sum}}{x}-1$个$k$使得$s_k$依次是$s_i$的$2$倍、$3$倍、......、$\frac{\text{sum}}{s_i}$倍。

  因此现在我们先预处理出前缀和并开个哈希表记录所有下标的前缀,然后枚举出第一段的右端点$i$,然后再枚举判断$s_i$的每个倍数是否都存在,即从$j$从$1$开始枚举,直到$\frac{\text{sum}}{s_i}$,每一个$s_i \times j$都有对应的前缀和存在。

  有公式$$\sum\limits_{i=1}^{n}{\frac{1}{i}} = 1 + \frac{1}{2} + \frac{1}{3} + \ldots + \frac{1}{n} \approx \ln{n} + \gamma \leq \log{n}$$

  因此时间复杂度最糟糕的情况是$$\sum\limits_{j = 1}^{n}{\frac{\text{sum}}{j}} = \text{sum} \times \left( 1 + \frac{1}{2} + \frac{1}{3} + \ldots + \frac{1}{n} \right) \approx \text{sum} \times \log{n}$$

  AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 1e5 + 10, M = 1e6 + 10;
 5 
 6 int s[N];
 7 bool vis[M];
 8 
 9 void solve() {
10     int n;
11     scanf("%d", &n);
12     memset(vis, 0, sizeof(vis));
13     for (int i = 1; i <= n; i++) {
14         scanf("%d", s + i);
15         s[i] += s[i - 1];
16         vis[s[i]] = true;
17     }
18     int ret = n;
19     for (int i = 1; i <= n; i++) {
20         if (s[i] == 0) {    // 跳过前i个均为0的情况
21             if (s[n] == 0) {    // 如果整个序列的数都为0,那么不需要划分
22                 ret = 0;
23                 break;
24             }
25         }
26         else if (s[n] % s[i] == 0) {    // 满足s[i] | s[n]
27             bool flag = true;
28             for (int j = 1; j <= s[n] / s[i]; j++) {
29                 if (!vis[s[i] * j]) {   // 每个s[i]的倍数都要存在
30                     flag = false;
31                     break;
32                 }
33             }
34             if (flag) ret = min(ret, n - s[n] / s[i]);
35         }
36     }
37     printf("%d\n", ret);
38 }
39 
40 int main() {
41     int t;
42     scanf("%d", &t);
43     while (t--) {
44         solve();
45     }
46     
47     return 0;
48 }

  再给出y总的做法。如果存在一组解,意味着等式$s \times \text{cnt} = \text{sum}$成立,其中$s$是每一段的和,$\text{cnt}$是段的数量,$\text{sum}$是整个序列的和,可以发现$s$和$\text{cnt}$都是$\text{sum}$的约数。因此我们可以枚举$\text{cnt}$,满足$\text{cnt} \mid \text{sum}$,那么$s = \frac{\text{sum}}{\text{cnt}}$,然后看看能不能把序列划分成每一段的和均为$s$,如果可以那么答案就是$n - \text{cnt}$。

  在${10}^{6}$的范围内,一个数最多有$240$个约数,因此时间计算量大概是$2.4 \times {10}^{7}$。

  补充:估算$n$的约数个数。先考虑$1 \sim n$这$n$个数所有的约数,$1 \sim n$中是$2$的倍数的数有$\frac{n}{2}$个(意味着$1 \sim n$这些数中含有因子$2$的数量),是$3$的倍数的数有$\frac{n}{3}$个,......,是$n$的倍数的数有$\frac{n}{n}$,那么$1 \sim n$中所有数的约数个数的和就是$\sum\limits_{i=1}^{n}{\frac{n}{i}} \approx n\log{n}$,因此平均下来每个数含有的约数个数大概有$\log{n}$个。

  AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 1e5 + 10;
 5 
 6 int n;
 7 int a[N];
 8 
 9 bool check(int sum) {
10     int s = 0;
11     for (int i = 0; i < n; i++) {
12         s += a[i];
13         if (s > sum) return false;
14         if (s == sum) s = 0;
15     }
16     // 这里也可以直接写return true; 如果能运行到这里那么一定满足s==0
17     return s == 0;
18 }
19 
20 void solve() {
21     scanf("%d", &n);
22     int sum = 0;
23     for (int i = 0; i < n; i++) {
24         scanf("%d", a + i);
25         sum += a[i];
26     }
27     for (int i = n; i; i--) {
28         if (sum % i == 0 && check(sum / i)) {
29             printf("%d\n", n - i);
30             break;
31         }
32     }
33 }
34 
35 int main() {
36     int t;
37     scanf("%d", &t);
38     while (t--) {
39         solve();
40     }
41     
42     return 0;
43 }

 

参考资料

  AcWing 4366. 上课睡觉(寒假每日一题2023):https://www.acwing.com/video/4576/

posted @ 2023-01-05 19:11  onlyblues  阅读(42)  评论(0编辑  收藏  举报
Web Analytics