[贪心_2] (含证明)将数组和减半的最少操作次数 | 最大数
目录
1.将数组和减半的最少操作次数
题解
证明
2.最大数
题解
1.将数组和减半的最少操作次数
链接: 2208. 将数组和减半的最少操作次数
给你一个正整数数组 nums
。每一次操作中,你可以从 nums
中选择 任意 一个数并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的数继续执行操作)
请你返回将 nums
数组和 至少 减少一半的 最少 操作数。
示例 1:
输入:nums = [5,19,8,1]
输出:3
解释:初始 nums 的和为 5 + 19 + 8 + 1 = 33 。
以下是将数组和减少至少一半的一种方法:
选择数字 19 并减小为 9.5 。
选择数字 9.5 并减小为 4.75 。
选择数字 8 并减小为 4 。
最终数组为 [5, 4.75, 4, 1] ,和为 5 + 4.75 + 4 + 1 = 14.75 。
nums 的和减小了 33 - 14.75 = 18.25 ,减小的部分超过了初始数组和的一半,18.25 >= 33/2 = 16.5 。
我们需要 3 个操作实现题目要求,所以返回 3 。
可以证明,无法通过少于 3 个操作使数组和减少至少一半。
题解
这道题把题意搞清楚之后,想让我们用最少的操作次数把数组和至少减少到之前的一半
- 那我们肯定会想到每次挑选的时候挑选当前数组里最大的数给它减半
- 因为挑数组最大的数减半才有可能最快的把整个数组的和减少到之前的一半。
解法:贪心
具体策略:每次挑选当前数组中,最大的那个数,然后减半,直到数组和减少到至少一半为止。
- 实现我们这个策略的核心就是循环这一步,每次挑选当前数组中,最大的那个数。
- 如果每次都去遍历数组挑最大的那个数,时间复杂度是非常恐怖的
- 这里我们可以使用优先级队列这个数据结构建立一个大根堆。维护 堆顶为最大的数
关于优先队列的使用
运用:
class Solution {
public:int lastStoneWeight(vector<int>& stones) {priority_queue<int> heap;for(auto& n:stones)heap.push(n);while(heap.size()>1){int n1=heap.top();heap.pop();int n2=heap.top();heap.pop();if(n1==n2) continue;else{int tmp=n1>n2?n1-n2:n2-n1;heap.push(tmp);}}return heap.empty()?0:heap.top(); }
};
本题解法:贪心 + 大根堆
- push
- top
- pop
- push
class Solution {
public:int halveArray(vector<int>& nums) {priority_queue<double> heap;double sum=0;for(auto& n:nums){heap.push((double)n);sum+=n;}double ret=sum;int cnt=0;while(1){double tmp=heap.top();heap.pop();cnt++;ret-=tmp/2.00;if(ret<=sum/2.00) break;heap.push(tmp/2.00);}return cnt;}
};
证明
证明策略:交换论证法
这里在说明一点,不管用什么方法证明,一定得要结合题意和贪心策略来证明。
- 我们这里的题意是每次挑选一个数直到数组和减半,
假设贪心解每次挑选数,这里用横线上面的点表示每次挑选的数。同样最优解也是这样表示。 - 贪心解挑选的数个数大于或者等于最优解的数个数。
我们只要能想到一个转化规则,在不破坏最优解的 “最优性质的” 前提下,能够将最优解调整成贪心解。就可以了。
- 那我们从左到右扫描贪心解和最优解。
- 先找到第一次两者不一样的地方。假设贪心解挑的是x,最优解挑的是y,此时我们只要能把y变成x就可以了。
如何变,紧扣题意和贪心策略,在我们贪心解里面挑的数组中最大的数,最优解如果没有挑最大那个数,那么这里有一个不等式,x > y
对于x在最优解中它有两种情况
第一种情况,x没有用过。
- 此时可以大胆将y替换成x,因为x > y,你用一个小的数减半就能让整个数组和减半
- 那选一个更大的数减半那更能让整个数组和减半。即使后面有y/2,y/4等都可以用x替换y。
第二种情况,x在后续最优解使用了,那我们依旧可以将y与x交换。
- 因为在最优解先用x减半和后用y减半并不影响最优解的最少操作次数。即使在y和x中间可能使用了y/2,y/4,但是把y交换到后面去了,那就没有y/2,y/4这些数,那逻辑不就错了吗。
- 其实可以把y和x交换完之后,把y/2,y/4,y重新排一下序,还是 y,y/2,y/4的使用。
此时我们在处理最后一个情况,贪心解的次数可能是比最优解次数多的,但是这种情况是绝对不可能的
- 因为我们从前往后扫描的时候,只要遇到不同都可以用这两种情况进行调整。
- 所以说当最优解解决完情况后,贪心解也一定减半了。所以最优解的操作次数和贪心解的操作次数是一样的。
到这里就证明完毕了。
- 原因就是从前往后扫描两个解的时候,只要有一个地方不一样,就可以通过这两个策略进行调整,所以最优解就可以在不失去最优性的前提下转化为贪心解。
- 所以当最优解是最少操作次数解决这个问题的时候,贪心解也是最少操作次数解决这个问题的时候。
- 因为对最优解通过调整,可以得到和贪心解一样的顺序
2.最大数
链接: 179. 最大数
给定一组非负整数 nums
,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:"210"
示例 2:
输入:nums = [3,30,34,5,9]
输出:"9534330"
示例1,[10,2] 它所形成的最大整数就是这个数组里面按照从左往右的顺序把数拼起来就可以了。
- 比如这个数组不去休息顺序从左往右拼起来就是102,如果调整一下顺序拼接就是210,201显然大于102,所以示例1所能拼成的最大整数就是210.
- 但是有可能输出结果可能非常大,所以你需要返回字符串,而不是整数。
题解
如果把这道题的题意搞清楚之后会发现这道题特别像是一道排序题。
- 无非就是给一个数组按照它给的规则把数组排下序使其形成一个最大的数。
- 所以先回忆之前正常的排序看能否给我们这道题带来一些启发
- 正常的排序(升序)
[4,10,8] - 不管是之前学习过的冒泡,快排,归并等排序,它们都是干一件事情,就是确定这些元素的先后顺序。只要能搞清楚这些元素谁在前面、谁在中间、谁在后面,我们就能完成这些元素的排序。
所以正常的排序本质:确定元素的先后顺序:谁在前,谁在后。
其中确定元素的先后顺序是根据一定的排序规则来确定
- 本题的排序其实也是在确定元素的先后顺序,所以我们仅需找一个排序规则就可以了。
- 排序规则,其实例一[10,2]已经给我们了,先把这两个数拼接成102和210,然后我们会发现把2放在前面把10放在后面是优于102,因此我们得到一个结果把2放前面,10放后面。
我们可以把这两个数抽象出来,a表示第一个数,b表示第二个数。
此时我们会得到两种情况,要么a在前b在后,要么b在前a在后
- 如果发现a在前b在后这种拼接是优于把b在前a在后的拼接,我们可以得到a可以放放在前面,b放在后面。
- 如果发现b在前a在后这种拼接和把a在前b在后的拼接是一样的,那就无所谓。
- 如果发现b在前a在后这种拼接是优于把a在前b在后的拼接,我们可以得到b可以放放在前面,a放在后面。
这就是我们这道题里面的排序规则。基于这样的排序规则会发现它们俩是非常一样的
这里我们可以使用sort进行排序,然后把这个比较规则传给sort。
- 我们这个专题不是贪心吗,但是这里贪心体现在哪里呢?
其实贪心就体现在了比较规则,确定谁前谁后的策略就是贪心。
- 这一个排序规则,为什么能排序呢?
之前排序规则之所以能排序最重要的就是传递性。a > b,b > c ----> a > c,可以推出来 a 在 b 前面,b 在 c 前面,又因为 a > c,所以 a 在 c 前面。所以符合 a b c。
如果此时a > b,b > c 推不出来 a > c,就不能排序。
- 因为a > b 可以推出 a 在 b 前面,b > c 可以推出 b 在 c 前面,如果推不出来 a > c 的意思是 c 可能大于 a,如果 c > a ,那 a 就在 c 的后面,此时根本就不可能。
- 数组就不能排序。
上面传递性太明显了,所以极有可能会忽略,但是我们这道题不能忽略传递性。
如果知道 ab > ba,bc > cb 推出 a 在 b 的前面,b 在 c 的前面,如果在能知道ac > ca a 在 c 的前面,才能摆出 a b c
但是能不能推出这一点,需要后面去证明。
这里写代码还有两个细节问题:
第一个细节:把数转化成字符串,然后拼接成字符串,比较字典序。
- 如果不这样搞还需要算两个数有多少位,然后一个数乘于位数在加上另一个数才能得到一个拼接的数。
第二个细节:这道题有可能传递 [0,0]这样的数组,如果按照之前的策略那就会返回一个 “00” 字符串。
- 但是我们要的是一个数,我们要把字符串搞成 “0” 才行。我们可以判断一下最后的结果ret,如果第一个字符是 “0” 说明后面应该全都是 “0”
- 那我们最终返回 “0” 这个字符串就可以了。
- 为什么后面应该全都是 “0”,如果后面的是不是0的话,0放在最后一个位置绝对不可能是最大数。
class Solution {
public:string largestNumber(vector<int>& nums) {vector<string> str;for(auto& n:nums){str.push_back(to_string(n));}sort(str.begin(),str.end(),[](string& a,string& b){return a+b>b+a; //借助了 字典序 排序});string ret;for(auto& s:str){ret+=s;}if(ret[0]=='0') return "0";else return ret;}
};
证明:
这里我们只要证明我们这个策略是可以排序的就可以了。
- 如何证明一个东西可以排序呢?这里要用到数学的知识。
- 证明方法:数学知识(全序关系)
全序关系的用处:
- 假设有一个集合,集合里有很多元素
- 全序关系的作用就是从这个集合中任意挑选两个元素
- 如果这两个元素在定义的比较规则下,如果满足全序关系的话,我们就说这个集合是可以排序的。
全序关系分为三个性质,也就是说只要满足三个性质就有全序关系。
- 完全性
从集合中任意挑选两个元素 a、b,它必定会存在两个关系的一个,要么 a <= b,要么 a >= b。如果挑选的两个元素压根没有大小关系,根本不知道谁在前谁在后。
所以完全性就是能够排序的最前提条件:任意挑选两个元素能够比大小。(即 存在大小关系
2. 反对称性
还是任意挑选两个元素a、b,如果知道 a <= b 且 a >= b,那必须能够推出 a = b
如果得不到这个结果,那么整个数组排序把a放在前面b放在后面是一种排序结果,把b放在前面a放在后面还是一种排序结果。
那么整个集合就不能排序,因为整个集合要想排序那最终结果是唯一的才行。
3. 传递性
任意挑三个元素a、b、c,如果 a >= b,b >= c,那一定要推出a >= c 。
这一点上面已经说过了如果不满足排序不出来最终结果。
我们要证明的是排序规则,任意挑两个数a、b,然后看 ab 和 ba 这个情况的大小,然后确定a和b的前后顺序。
- 证明完全性
挑出a、b,然后看a拼接b,b拼接a是能够比大小的就可以了。
第一种证明:
ab拼接后是一个数,ba拼接后也是一个数。数和数之间是能够比大小的。要么ab >= ba,要么 ba >= ab。
第二种证明:
设a的位数为 x 位, b的位数为 y 位。
ab拼接的结果是可以表示出来的:10^ya + b
ba:10^xb + a
既然这两个数能明确表示出来,那一定能比大小。
- 证明反对称性
如果 ab <= ba 且 ab >= ba,那我们要能得到 ab = ba 这个结论。
将ab和ba用刚才的进行处理,因为这里是一个一个的数,设ab为m,ba为n,此时我们能得到一个夹逼定理。m <= n <= m,我们可以得到 m = n。所以我们可以根据1、2得到3。
- 证明传递性
还是任取三个数a、b、c,如果ab >= ba 且 bc >= cb 我们必须要推出来ac >= ca
,ac >= ca的意思是a在前c在后,也就是说a在前,b在后,我们要能推出来a在前,c在后才行。
- 我们证明方法和上面一样,把不等式写成一个确切的数
这里特殊情况要考虑一下,如果a、b、c其中任意一个可能等于0, 假如a是0的话,那x位数就是为0。小学我们就知道0不是一个个位数,但是这道题里我们把0这个数当成一个个位数。如a是12,b为0,ab为120
- 如果能由1和2这两个式子能推出来第3个式子,那么这个式子就成立
- 我们观察一下三个式子发现第三个式子是没有b的,因此我们仅需通过1和2两个式子把b给消掉就可以了。我们可以把b的系数移过去就可以把b消掉,但是移系数要注意系数不能为0,但是刚才处理特殊情况的时候说过x绝对不可能等于0的,那么10^x绝对不可能等于1,所以我们可以大胆移第一个式子b的系数。同样第二个式子也是。
- 10^y - 1不可能为0,两步同时消掉,然后在移项就可以得到第三个式子
- 到此证毕,我们这个排序规则既有完全性,又有反对称性,又有对称性,因此具有全序关系。
- 全序关系 -->可排序