题意整理:
基本题意
给出一个长度为 的整数数列
。
每次操作可以做以下操作中的一个:
- 让所有
都减
。
- 选择一个
,让
减少
,剩余的
都减少
。
如果一个数减少到 了知乎再被减少,则依然将它视为
。
问最少操作多少次可以使得整个数列不存在正数。
数据范围与性质
。
解法1:枚举答案
【知识点】枚举
【算法】枚举+检验
分析与算法描述
值得注意的是,把任何一个第一种操作用第二种操作取代,效果一定不比第一种操作劣,因此我们可以认为只有第二种操作。
我们考虑把最优性问题转换成判定性问题,从小到大枚举一个次数 ,检验
次操作能否达成目标,如果检验成功,那么就找到了答案。
当次数 确定的时候,说明每个数字都至少减少了
,于是我们可以先把所有的
变成
。
而我们操作的时候,除了所有数字都减少 ,我们可以选一个数字,让它再次减少
的值。
剩余的非零数字,我们每次只能分配一个大小为 的减少量给其中一个数字,因此每个数字需要分配
次操作。所以,只要这时候满足
,那么操作次数
就是可行的,否则是不行的。
注意这个算法需要特判 的情况,因为
的时候上述计算公式没有意义。这时候答案就是
。
举个例子,对于样例:
n=3, a=[2,3,9], k=5
刚开始 数组是这样的:
当取 的时候,全部减去
:
可以发现,剩下的部分至少需要 次操作,
因此这种情况不行。
这样因为需要枚举 ,而
最大可达
,每次检验还需要遍历枚举
个数,因此时间复杂度
。
空间复杂度只需要 。
参考代码(会超时)
class Solution { public: bool check(int m,vector<int>& a,int k)//验证答案的合法性 { long long need=0;//用于累加计算需要的最少操作数 for(int i=0;i<a.size();i++) { int v=a[i]; v=max(v-m,0); need += ceil(v*1.0/(k-1)); } return m>=need; } int dryClothes(int n, vector<int>& a, int k) { int max_a=0; for(auto &i:a) max_a=max(max_a,i);//取a中最大值 if(k==1)//特判k=1的情况 return max_a; for(int m=1;m<=max_a;m++)//枚举答案 if(check(m,a,k)) return m; return -1; } };
解法2:二分答案
【知识点】二分查找
【算法】二分+检验
实现与分析
我们考虑如何优化上面的做法。
值得的注意一个性质:
如果
次操作没有办法成功,那么
,
次操作也一定不可能成功。
如果
次操作可以成功,那么
,
次操作也一定有成功的方案。
也就是说,如果我们的答案,即合法的最少操作次数是 ,那么比
小的操作次数一定不够,而比
大的操作次数一定够。
我们的答案是一个分界点。我们要在答案区间 上找一个分界点。并且我们可以通过
的一次检验来确定一个值是否可以成功。于是可以采用二分法。
假设我们的答案在区间 上,我们取
,检验一次
是否可行,如果不可行,说明答案在
范围中,反之在
范围中,于是我们缩小了一半范围。反复做下去,只需要执行
次检验即可将范围缩小到
找出答案。
这样的时间复杂度降为 。
空间复杂度仍然为 。
参考代码
//c++ class Solution { public: bool check(int m,vector<int>& a,int k)//验证答案的合法性 { long long need=0;//用于累加计算需要的最少操作数 for(int i=0;i<a.size();i++) { int v=a[i]; v=max(v-m,0); need += ceil(v*1.0/(k-1)); } return m>=need; } int dryClothes(int n, vector<int>& a, int k) { int max_a=0; for(auto &i:a) max_a=max(max_a,i);//取a中最大值 if(k==1)//特判k=1的情况 return max_a; int L=1,R=max_a;//二分答案 while(L!=R) { int mid=(L+R)/2; if(!check(mid,a,k)) L=mid+1; else R=mid; } return L;//最后区间长度为 1 ,就是答案。 } };