上课睡觉

上课睡觉

N 堆石子,每堆的石子数量分别为 a1,a2,,aN

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

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

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

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

输入格式

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

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

第二行包含 N 个整数 a1,a2,,aN

输出格式

每组数据输出一行结果。

数据范围

1T10 ,
1N105,
0ai106,
i=1nai106,

每个输入所有 N 之和不超过 105

输入样例:

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(n2)

  假设第一段是区间[1,i],第一段的总和是x,整个序列的和为sum,那么区间[i+1,n]就应该被划分为sumx1段,并且还要满足xsum。假设有序列的前缀和数组s,那么这个条件就等价于sisn,而区间[i+1,n]恰好被划分成sumx1段,就等价于在[i+1,n]中存在sumx1k使得sk依次是si2倍、3倍、......、sumsi倍。

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

  有公式i=1n1i=1+12+13++1nlnn+γlogn

  因此时间复杂度最糟糕的情况是j=1nsumj=sum×(1+12+13++1n)sum×logn

  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×cnt=sum成立,其中s是每一段的和,cnt是段的数量,sum是整个序列的和,可以发现scnt都是sum的约数。因此我们可以枚举cnt,满足cntsum,那么s=sumcnt,然后看看能不能把序列划分成每一段的和均为s,如果可以那么答案就是ncnt

  在106的范围内,一个数最多有240个约数,因此时间计算量大概是2.4×107

  补充:估算n的约数个数。先考虑1nn个数所有的约数,1n中是2的倍数的数有n2个(意味着1n这些数中含有因子2的数量),是3的倍数的数有n3个,......,是n的倍数的数有nn,那么1n中所有数的约数个数的和就是i=1nninlogn,因此平均下来每个数含有的约数个数大概有logn个。

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