题意整理:
基本题意
给出一个长度为 的整数数列 。
每次操作可以做以下操作中的一个:
- 让所有 都减 。
- 选择一个 ,让 减少 ,剩余的 都减少 。
如果一个数减少到 了知乎再被减少,则依然将它视为 。
问最少操作多少次可以使得整个数列不存在正数。
数据范围与性质
。
解法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 ,就是答案。 } };