上课睡觉
上课睡觉
有 $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$都有对应的前缀和存在。
有公式
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/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17028629.html