0、贪心算法介绍
例三中的最优解为装两个2号物品,总价值为14。贪心算法+鼠目寸光
解释:若某个位置的最优解为20元,那么10元的就一定不超过一张,否则两张10元就可以被一张20元代替,同理5元的最大张数为1,1元的最大张数为4。
贪心:[20x1,10x2,5x3,1x4];最优解:[20y1,10y2,5y3,1y4]。若a比A多一张,那么就需要后面的面额凑出来一张a,根据上述的性质,这是不可能的,所以x1一定不大于y1。x1不可能小于y1,若20元纸币可以去y1张,那么根据贪心策略x1一定大于等于y1。综上所述,x1只可能等于y1。以此类推,x2y2,x3y3,x4==y4。所以找零钱使用贪心策略找到的一定是最优解·。
1、柠檬水找零
class Solution {
public boolean lemonadeChange(int[] bills) {
// 0、仅有三个面额,20美元的纸币不会被用
int five = 0, ten = 0;
for(int x:bills){
// 1、分类讨论
// 1.1、遇见5直接收下
if(x==5){
five++;
}else if(x == 10){
// 1.2、遇见10收下,找出一张5美元
five--;
ten++;
}else {
// 1.3、遇见20美元,贪心的现有10美元找了,再用5美元找零
if(ten>0&&five>0){
ten--;five--;
} else {
five-=3;
}
}
// 2、尝试找零后的结果非法,那么就是找不开,返回false
if(five<0 || ten<0){
return false;
}
}
return true;
}
}
有点抽象!!!
**贪心策略:**收到20美元需要找零15美元可以使用[5,10]组合或者[5,5,5];收到10美元的时候需要找零5美元;可见5美元的作用很大既可以用于找零20美元,也可以用于找零10美元,所以我的贪心策略找零时尽量先使用10美元,尽量节省5美元的时候,若提前把5美用光了,那么后面需要用五美元的时候10美元起不了作用。
贪心策略的正确性:(交换论证法)假设每次找零的情况为:[5,5,5] , [5] , [10] , [10]
可见前期某一次使用两张5美元找零代替了一张10美元,显然可以将后面某次使用10美元找零与本次使用两张5美元交换,即[5,10] , [5] , [10] , [5,5],也就是说最优解可以通过交换转化为贪心解。
2、将数组和减半的最小操作次数
class Solution {
public int halveArray(int[] nums) {
// 0、贪心策略:每次都让数组中最大的数减半
double sum=0;
PriorityQueue<Double> queue = new PriorityQueue<>((a, b) -> Double.compare(b, a));
for(int num : nums){
sum+=num;
queue.add((double)num);
}
sum/=2;
int step=0;
double deincrice=0;
while(deincrice<sum){
step++;
double max = queue.poll()/2;
deincrice+=max;
queue.add(max);
}
return step;
}
}
贪心策略:每次都将数组中最大的那个值减半,这样操作的次数就是最小操作数。
贪心策略的正确性:(交换论证法),对于某个最优解每次减半的数字为(a<b<c<d)经过四次减半操作后数组和降至原来的一半,那么对于贪心解(d>c>b>a)经过四次数字减半后数组和也一定降至了原来的一半。通过交换被减半数字的位置,最优解可以调整为贪心解。
**什么是最优解:**就是复合题意的解,可能不止一个,且包含贪心解,贪心解往往只有一个,若所有最优解都与贪心解等价,通过变换可以变成贪心解,那么这个贪心策略就是可行的
3、最大数 ★★★★
class Solution {
public String largestNumber(int[] nums) {
// 0、贪心策略:最高位越大、越靠前
// 1、类似于字符串根据字典序排序一样,对nums进行排序,而不是按照大小排序
int n = nums.length;
String[] strs = new String[n];
for(int i=0;i<n;i++) strs[i] = "" + nums[i];
// 排序
Arrays.sort(strs,(a,b)->{
// 这个排序的规则才是最细节的地方
return (b+a).compareTo(a+b);
});
StringBuffer ret = new StringBuffer();
for(String s : strs) ret.append(s);
if(ret.charAt(0)=='0') return "0";
return ret.toString();
}
}
这个题目要举几个例子充分理解题目;
若排序采用:return (b+a).compareTo(a+b); 根据数字的字典序进行倒序,如果(b+a)>(a+b),那么b一定要安排在a的前面;又有(c+a)>(c+b),(b+c)>(c+b),则最后的排序就是 bca 。
排序前:nums = [1,2,3,4,5,6,7,8,9,0,10]
排序后:strs = [9,8,7,6,5,4,3,2,1,10,0]
最大数:“987654321100”结果正确
若排序采用:return (b).compareTo(a); 根据数字的字典序进行倒序,这种排序策略看似可行实则不行
排序前:nums:[3,30,34,5,9]
排序后:strs:[“9”,“5”,“34”,“30”,“3”]
最大值:“9534303” 结果错误
回顾时所写:
首先,因为设计到数字的拼接所以将数字转化为字符串更为方便
其次就是这个题目的贪心策略:根据字典序排序则有:“abc”<“ab”,“90”<“9”
a>=b && a<=b ,由夹逼准则可得a==b
4、摆动序列 ★★★★★★
贪心策略:遍历数组[1,3,5,4,2,6,8,7]。遍历到3时,摆动数组是[1,3]。遍历到5时,摆动数组是[1,3]还是[1,5]呢?发动你贪婪的心,摆动数组应该是[1,5],因为若后面的元素小于5摆动数组就会变长,但后面的元素不一定小于3摆动数组长度不变;相反,如果数组成递减趋势[1,5,4]和[1,5,2],贪心策略会上我们选择[1,5,2];
分析完贪心策略,可以明显发现其实就是统计波峰与波谷的数量。
回顾时补写:
即使没有贪心的想法也可以看出这个题目就是在统计波峰波谷的数量,这样一写题目的思路就是围绕着波峰波谷,方法忽悠很多,我和吴老师写的就不太一样;
如果使用贪心策略的想法写代码就因该创建出一个链表存储摆动序列,一边遍历数组,一边维护摆动序列,根据上述的贪心策略,最后的摆动序列会变成波峰波谷的集合。因为是在摆动的时候统计摆动数组的长度,因此也只是关注链表的最后一个元素,所以也没必要创建一个链表。
class Solution {
public int wiggleMaxLength(int[] nums) {
int n = nums.length;
if(n==1) return 1;
int slope = 0; // 斜率,无所谓可以初始化为1或者-1
Stack<Integer> wobble = new Stack<>();// 摆动序列wobble sequence
wobble.push(nums[0]);
int step = 0;
for (int i = 1; i < n; i++) {
if (wobble.peek() != nums[i]) {
int temp = nums[i]-wobble.peek();
if(slope*temp<=0) {
step++;
wobble.push(nums[i]);
slope = temp;
}else{
wobble.pop();
wobble.push(nums[i]);
}
}
}
return step + 1;
}
}
摆动暴动序列的特殊情况还挺多的,好难
class Solution {
public int wiggleMaxLength(int[] nums) {
// 返回最长子序列而不是子数组
int n = nums.length;
if (n == 1)
return 1;
int direction = 0;// 代表斜率,等于1或者-1
int step = 1;// 步数
int i = 0; //遍历数组
// 例如:5552637
// 这段while循环就是为了遍历到开头平底的最后一个位置
while (i + 1 < n) {
if (nums[i] == nums[i + 1]) {
// 平地则不考虑
i++;
} else {
// 维护斜率
direction = nums[i] < nums[i + 1] ? 1 : -1;
step++;
break;
}
}
// 继续遍历数组,每一次斜率变化进行步数加1
for (i++; i + 1 < n; i++) {
if (nums[i] != nums[i + 1]) {
int temp = 0;
temp = nums[i] < nums[i + 1] ? 1 : -1;
if (direction != temp) {
direction = temp;
step++;
}
}
}
return step;
}
}
class Solution {
public int wiggleMaxLength(int[] nums) {
int n = nums.length;
if (n < 2)
return n;
int ret = 0, left = 0;
for (int i = 0; i < n - 1; i++) {
// 如果⽔平,直接跳过
if (nums[i + 1] != nums[i]){
int right = nums[i + 1] - nums[i]; // 计算接下来的趋势
if (left * right <= 0){
ret++; // 累加波峰或者波⾕
}
left = right;
}
}
return ret + 1;
}
}
两代码意思是一样的,但是我没能把特殊情况于普通情况合并处理,导致用了很多变量。
贪心策略可行性证明:
5、最长递增子序列★★★★★
贪心策略:
为什么这个贪心策略是对的?
ArrayList ret 中第 i 个结点存储的是 【长度为i+1递增子序列的末尾的最小值】
这跟贪心策略有关系吗?什么是贪心?就是鼠目寸光的认为局部最优解就是全局最优解啊!
数组[7,3,8,4,7,2,14,13]进行遍历:
cur=7:此时只有一个元素,[7]是唯一的递增子序列,长度为1。ret.add(7);ret.size()==1,代表当前最大子序列的长度为1;ret=[7];
**cur=3:**3比7小,不与7给构成递增子序列。此时就有两个长度为1的递增子序列[3],[7]。但实际上只需要关注[3]即可,因为如果后续有数字可以与[7]构成长度位的递增子序列,那么也一定可以与[3]构成递增子序列。ret=[3];
**cur=8:**因为8>4,可以与[3]构成长度为2的递增子序列,ret.add(8);ret.size()==2,代表当前最大子序列的长度为2;ret=[3,8];
**cur=4:**先更新长度为1的递增子序列末尾的最小值,4>3所以不能更新。再看看长度为2的递增子序列的末尾的最小值是否可以更新,因为4<8,[3,4]和[3,8]这两个长度为2的第增子序列,如果后面有数字可以与[3,8]构成长度为3的子序列,那么也一定能与[3,4]构成长度为3的递增子序列,反之则不一定。所以仅关注这两个子序列末尾值小的那个即可,更新长度为2的递增子序列的末尾的最小值为4;
ret=[3,4];
**cur=7:**显然7大于ret的尾结点,所以可以构成长度为3的递增子序列,此时ret.add(7),ret=[3,4,7].
**cur=2:**以此与ret[i]进行比较,2可以更新长度为1的递增子序列的末尾的最小值,ret=[2,4,7];
**cur=14:**更新ret长度,ret.add(14).ret=[2,4,7,14];
cur=13:更新长度为此的递增子序列的末尾的最小值,ret=[2,4,7,13];
一趟下来以后我们找到了每一种长度递增子序列的末尾的最小值,那么最长递增子序列的长度就是有多少中最长递增子序列(第一种长度为1,第二种长度为2,第n种长度为n,n最长,所以种类数等于最大长度),也就是ret.size()
贪心体现在我只需要关心每一种递增子序列中那个末尾值最小的子序列即可,上述模拟过程中的任何一个位置ret.size()都是局部最优解。
PS:最后的ret数组不是一个最长递增子序列,不要产生错觉,因我们全程的目的并不是找到一个最长递增子序列。
**优化:**遍历数组nums是O(N),存入 nums[i]到ret中是O(N)的,整体时间复杂度为O(N^2),因为ret是递增的(根据链表构造是的条件可证),所以可以使用二分插入,将时间复杂度优化到O(logN),整体的时间复杂度为O(NlogN)。
class Solution {
public int lengthOfLIS(int[] nums) {
ArrayList<Integer> ret = new ArrayList<>();
int n = nums.length;
ret.add(nums[0]);
for (int i = 1; i < n; i++) {
if (nums[i] > ret.get(ret.size() - 1)) // 如果能接在最后⼀个元素后⾯,直接放
{
ret.add(nums[i]);
} else {
// ⼆分插⼊位置
int left = 0, right = ret.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
if (ret.get(mid) < nums[i])
left = mid + 1;
else
right = mid;
}
ret.set(left, nums[i]); // 放在 left 位置上
}
}
return ret.size();
}
}
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n+1];
int ret = 0;
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(nums[i-1]>nums[j-1]){
dp[i] = Math.max(dp[i],dp[j]);
}
}
dp[i] += 1;
ret = Math.max(ret,dp[i]);
}
return ret;
}
}
// 时间复杂度O(N^2)
6、递增的三元子序列★★★★★
这一题比上一题简单,我们只需要证明有三元组(长度为三的递增子序列)即可,因为长度仅为3,可以使用两个变量来代替ret数组,更新这两个变量时(也就是更新ret时)只需两次比较即可,不用二分优化了,所以整体的时间复杂度为O(2N);
class Solution {
public boolean increasingTriplet(int[] nums) {
int n = nums.length;
int a = nums[0], b = Integer.MAX_VALUE;
for (int i = 1; i < n; i++) {
if (nums[i] < a) // 如果能接在最后⼀个元素后⾯,直接放
{
a = nums[i];
} else if (b < nums[i]) {
return true;
} else {
b = nums[i];
}
}
return false;
}
}
7、最长连续递增序列
class Solution {
public int findLengthOfLCIS(int[