【AcWing每日一题】4366. 上课睡觉
有 N 堆石子,每堆的石子数量分别为 a1,a2,…,aN。
你可以对石子堆进行合并操作,将两个相邻的石子堆合并为一个石子堆,例如,如果 a=[1,2,3,4,5],合并第 2,3 堆石子,则石子堆集合变为 a=[1,5,4,5]。
我们希望通过尽可能少的操作,使得石子堆集合中的每堆石子的数量都相同。
请你输出所需的最少操作次数。
本题一定有解,因为可以将所有石子堆合并为一堆。
输入格式
第一行包含整数 T,表示共有 T 组测试数据。
每组数据第一行包含整数 N。
第二行包含 N 个整数 a1,a2,…,aN。
输出格式
每组数据输出一行结果。
数据范围
1≤T≤10,
1≤N≤105,
0≤ai≤106,
∑ai≤106,
每个输入所有 N 之和不超过 105。
自己的解法:
- 用堆排序的方法,对于每一组数据,建立初始小根堆,进行堆排序。
- 每次排好一个元素后,进行对已排序序列检查,如果已经完成堆排序的序列的部分和(从前往后)可以大于等于目前的最大值,则合并这部分。(全是0的情况除外)
- 合并后更新下次排序需要的参数,继续进行堆排序,合并。
- 直至所有的数都一样或者合成一堆,算法停止,输出答案,进行下一组的堆排序。
- 这需要每次合并后都检查是否每个数都一样。
- 因为考虑到数据量的问题,用枚举是O(n2)的复杂度,会超时,用堆排序降到O(nlog2n)的复杂度。
我的推导过程:
但是考虑到如果每次排序后都进行检查,那么时间复杂度也会上升到O(n2)
自己的代码(没写完):
#include<iostream>
#include<vector>
using namespace std;
vector<vector<int> > v; //v存放所有的数组
int max_vj = -0xffff; //每组数据的最大值
int res = 0;
//小根堆的建立
void HeapAdjust(vector<int> &A, int k, int len){
int t = A[k]; //暂存这个分支结点
for(int i = 2*k; i <= len; i *= 2){ //从这个分支结点开始向下调整
if(i < len && A[i] > A[i+1]) i++; //右孩子更小
if(t <= A[i]) break; //分支结点已是子堆中的最小值,符合特性
else{ //不符合特性,需要调整
A[k] = A[i]; //换的时候只是覆盖
k = i; //下标要交换,下次还说与A[0]比较
}
}
A[k] = t; //放到最终符合特性的位置上
}
void BuildMinHeap(vector<int> &A, int len){
//从最后一个分支结点开始,逐级向上建堆
for(int i = len/2; i > 0; i--)
HeapAdjust(A, i, len);
}
//堆排序
void HeapSort(vector<int> &A, int len){
for(int i = len; i > 1; i--){ //n-1趟交换和建堆过程
swap(A[i], A[1]); //堆顶元素和堆底元素互换
//交换后检查是否符合合并的条件
//如果已经完成堆排序的序列的部分和(从后往前)可以大于等于目前的最大值,则合并这部分
//合并后更新下一次需要的参数
if(){
A[len-1] = A[len]+A[len-1]; //合并
if(A[len-1] > max_vj) max_vj = A[len-1]; //更新最大值、长度、合并次数
A[0]--; len--; res++;
}
HeapAdjust(A, 1, i-1); //将剩余的元素调整
}
}
int main(){
int T, N, an; //T组测试数据,每组数据包含N个整数an
cin >> T; //测试数据的数量
vector<int> vj; //每组数据存放的数组
//下面搞定输入
for(int i = 0; i < T; i++){
cin >> N; //N个整数
vj.clear(); //对数组先清空
vj.push_back(N); //每个数组的第一个位置存放这个数组的长度
for(int j = 0; j < N; j++){
cin >> an; //输入a1~an
vj.push_back(an);
}
v.push_back(vj);//将每组数据放入v
}
for(int i = 0; i < T; i++){
vector<int> vj = v[i];
if(vj[0] == 2 && vj[1] != vj[2]){ //如果只有两个数且不相等
cout << 1 << endl;
continue;
}else if(vj[0] == 1){ //如果只有一个数
cout << 0 << endl;
continue;
}
//找出每组数据的最大值
for(int j = 1; j <= vj[0]; j++)
if(vj[j] > max_vj) max_vj = vj[j];
//对于每组数据建立初始的小根堆
BuildMinHeap(vj, vj[0]); //建堆
HeapSort(vj, vj[0]); //堆排序的同时合并
cout << res << endl;
max_vj = -0xffff; //重置
res = 0;
//测试堆排序结果
// for(int i = 1; i <= vj[0]; i++) cout << vj[i] << " ";
// cout << endl;
// cout << max_vj << endl;
}
return 0;
}
y总的方法:
他的方法是用的数论的推导
总的石子个数sum,最终每堆石子cnt个,cnt只有整除sum才可以
cnt的数量等于sum的约数个数,平均每个数的约数个数是O(logn)个
106以内,约数个数最多的数是720720,有240个约数
int范围内,约数个数最多的数有1600个
- 知道了这个前提,我们就可以用枚举的方法,枚举所有的约数,找到可以取到的并整除sum的数
- 在所有的数里面找到合并次数最少的方案
- 我们知道,最终合并后有sum/cnt堆石子,原来有n堆
- 所以合并的次数为 n-sum/cnt
- 我们只需要找到一个最小的符合条件的cnt
- 所以我们只需要从小到大枚举cnt,看是否成立就可以了
一开始还有一个条件我没注意到,只能合并相邻的两堆,这样堆排序好像没法用了
那我们如何判断cnt是否成立?(能否将每一堆的数量变成cnt)
- 对于每个cnt,我们对原始序列从前向后枚举
- 如果当前堆的石子数量大于cnt,那一定无解,枚举下一个cnt
- 如果当前堆的石子数量等于cnt,如果后面的数量是0,可以加到前面或后面的一堆;如果不是0,那就合并到下一段
- 根据计算,大约240个约数,最多用107次计算量,可以在规定的时间内完成
最后附一下代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int w[N];
//判断当前的约数是否符合条件
bool check(int cnt)
{
//从前向后枚举序列的每一段
for (int i = 0, s = 0; i < n; i ++ )
{
s += w[i]; //合并相邻的两段
if (s > cnt) return false; //如果合并后的石子数量大于cnt,无解
if (s == cnt) s = 0; //如果等于这个约数cnt,继续合并后面的段
}
return true;
}
int main()
{
int T;
scanf("%d", &T);
while (T -- )
{
scanf("%d", &n); //石子的堆数
int sum = 0; //石子的总数sum
for (int i = 0; i < n; i ++ ) //输入每堆石子个数
{
scanf("%d", &w[i]);
sum += w[i]; //计入到总数sum
}
for (int i = n; i; i -- ) //对于每堆石子,枚举寻找符合条件的cnt
if (sum % i == 0 && check(sum / i))//如果cnt能够整除sum,并且操作次数最小,且有解
{
printf("%d\n", n - i); //输出答案
break;
}
}
return 0;
}
//作者:yxc
//链接:https://www.acwing.com/activity/content/code/content/5027948/
//来源:AcWing
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。