算法-贪心(T1~T3)

本文简单记录三道贪心题目的答案和求解论证过程.

目录

    • [1. 柠檬水找零](#1. 柠檬水找零)
      • [1.1 介绍+思路](#1.1 介绍+思路)
      • [1.2 参考代码如下](#1.2 参考代码如下)
      • [1.3 证明](#1.3 证明)
    • [2. 将数组和减半的最少操作次数](#2. 将数组和减半的最少操作次数)
      • [2.1 介绍 + 思路](#2.1 介绍 + 思路)
      • [2.2 参考代码如下](#2.2 参考代码如下)
      • [2.3 证明](#2.3 证明)
    • [3. 最大数](#3. 最大数)
      • [3.1 介绍 + 思路](#3.1 介绍 + 思路)
      • [3.2 参考代码](#3.2 参考代码)
      • [3.3 证明](#3.3 证明)

1. 柠檬水找零

1.1 介绍+思路

题目链接: LINK

题目思路: 思路可以简单概括为下图.

1.2 参考代码如下

cpp 复制代码
class Solution {
public:
    bool lemonadeChange(vector<int>& bills) 
    {
        int five = 0, ten = 0;
        
        for(auto& bill : bills)
        {
            // bill == 5
            if(bill == 5) five += 1;
            // bill == 10
            else if(bill == 10) 
            {
                if(five >= 1) five--, ten++;
                else return false;
            }
            // bill == 20
            else 
            {
                if(five >= 1 && ten >= 1) five--, ten--;
                else if(five >= 3) five -= 3;
                else return false;
            }
        }

        return true;
    }
};

1.3 证明

为啥贪心解就是最优解? 下面我们来进行证明, 以确定我们的贪心算法是适用本题的.

我们假设对于一个数列有解(可以找零), 那么我们可以有下面两种策略:

我们假设贪心算法的策略: ... -> a -> b -> c -> d ...

同时我们也可以知道最优解: ... -> A -> B -> C -> D ...

我们的证明方法是: 交换论证法 .

即: 如果可以把最优解通过交换或者等效替换的方式且不失去"最优性"的前提下转换成贪心解, 那么我们就说贪心解是正确的.

下面我们来详细解释:

对于前两种, 最优解和贪心解给出的找钱方式是一样的, 因为没有别的选择!

区别只在于第三种给20块钱的情况.

换言之, 上面我们假设的找钱方式的队列中有很多是相等的, 因为有很多5元和10元的情况.

我们从不相等的一个开始分类讨论, 假如说b 和 B是不相等的, 那么此时意思就是说别人给了你20元

然后我们的贪心解肯定是给的10 + 5元, 而最优解给的是5 + 5 + 5元(请注意我们说的是不相等位置).

在这个前提下, 有两种情况,

一种是10元在最优解中压根没用到, 那么就是说5+5 等价于 10元. (因为没用到啊)

另一种是10元在最优解中用到了, 只不过不是在这个位置用的, 可能在前面或者后面用的, 此时也可以把10元和5+5元进行替换. (因为5+5是更万能的)

综上, 我们说明了如果两个位置的找钱方式不同, 一定可以进行替换或者调换顺序使得在相同位置下两者的值相同.

以此类推, 我们可以证得贪心解和最优解是等价的, 也就是说我们的贪心解法是正确的.

2. 将数组和减半的最少操作次数

2.1 介绍 + 思路

题目链接: LINK

题目思路: 贪心求解, 每次挑选最大的数减半.

2.2 参考代码如下

cpp 复制代码
class Solution {
public:
    int halveArray(vector<int>& nums) 
    {
        priority_queue<double> pqueue;

        // 把nums中的元素入到大根堆中 
        double sum = 0;
        for(auto num:nums)
        {
            pqueue.push(num);
            sum += num;
        }
        sum /= 2.0;
        int count = 0;

        while(sum > 0) // 减少一半 或者 一半以上都可以
        {
            double t = pqueue.top() / 2.0;
            pqueue.pop();
            sum -= t;
            count++;
            pqueue.push(t);
        }

        // 返回结果
        return count;        
    }
};

2.3 证明

这道题的证明思路, 我们同样使用交换论证法进行证明.

我们就从第一个不相等的数开始讨论, 假如说b != B.

由"贪心"和最优可知, b >= B.

因为b!=B且b>=B, 我们只需要证明b > B这种情况是可以等效替换的即可

即 b > B:

假如说在最优解中, b没有用过, 那么 b <=> B(两者可以相互替换),

因为越大的数/=2减的越多

假如说在最优解中, 用过b, 那么最优解中的b 和 B交换顺序即可, 也可以

达到使得贪心解和最优解的顺序一致的效果.

在这种情况下, 如果存在后续也使用到B的情况, 比如下面:


并且同时, 因为贪心解每次都是/2的最大的数, 因此length(贪心) <= length(最优解)

然而最优解指的是最小的count次数, 因此length(最优解) <= length(贪心)

所以说length(贪心) == length(最优解)

综上所述, 最优解和贪心解等价, 因此贪心解可行.

3. 最大数

3.1 介绍 + 思路

题目链接: LINK

题目思路: 先排序, 按照 "a+b" > "b+a" -> a在前b在后的方式排序, 然后依次合并.

对于我们传统的排序, 我们的排序条件是: x > y就x在后y在前(如果是升序的话)

但是, 我们这题的排序方式是:

                 if("x" + "y" >= "y" + "x") ==> x在前, y在后

                 else if("x" + "y" <= "y" + "x") ==> y在前, x在后

3.2 参考代码

cpp 复制代码
class cmp
{
public:
    bool operator()(const string& s1, const string& s2)
    {
        string ab = s1 + s2;
        string ba = s2 + s1;
        return ab > ba;
        /*
        * 如果ab >= ba, 就返回true, 表示不用交换
        * 如果ab < ba, 就返回false, 表示需要进行交换
        */
    }
};

class Solution {
public:
    string largestNumber(vector<int>& nums) 
    {
        // 把nums字符串化
        vector<string> strs; 
        for(auto num : nums)
        {
            strs.push_back(to_string(num));
        }

        // 排序
        sort(strs.begin(), strs.end(), cmp());
        
        // 拼接
        string ret;
        for(auto& str: strs)
        {
            ret += str;
        }

        // 去掉前导0
        if(ret[0] == '0') return "0";
        else return ret;
    }
};

小细节:

3.3 证明

下面是图片版:

下面是文字版:

我们利用离散中的"全序关系"进行证明.

说的不严谨一点, 如果一套排序规则满足全序关系, 那么表明其是可以进行排序的

全序关系主要包含三个部分:

  1. 完全性.

    即 a, b(任意两个元素之间)是能够确定大小关系的.

  2. 反对称性.

    即a <= b, b <= a, 可以==> a == b.

  3. 传递性.

    如果a >= b, b >= c, 必须能够推得a >= c.

    我们前面排序定义的排序规则是:

    "a" + "b" > "b" + "a" ==> "a"+"b"

    "a" + "b" < "b" + "a" ==> "b"+"a"

    "a" + "b" = "b" + "a" ==> 都可以

证明完全性:

我们设 a:x位 b: y位

  • "a"+"b" : 10^y * a + b
  • "b + a" : 10^x * b + a
    显然两个元素可以由数进行表示, 因此任意两个元素之间可以确定大小关系, 满足完全性.

证明反对称性:

ab <= ba 且 ab >= ba ==> ab == ba

① ② ③

我们用数字表示一下各个表达式

①:10^y * a + b <= 10^x * b + a

②:10^y * a + b >= 10^x * b + a

==> 联立①②: 10^y * a + b <= 10^x * b + a <= 10^y * a + b

由高等数学中的迫敛性定理可知:

==> 10^y * a + b = 10^x * b + a = 10^y * a + b

因此, 可以推得满足反对称性

证明传递性:

我们需要证明: ab >= ba && bc >= cb ==> ac >= ca

① ② ③

同时, 因为"0"在两个数合并的时候是字符串相加的缘故, 因此"0"算个位数(在一般小学数学当中, 0不算个位数).

①: 10^y * a + b >= 10^x * b + a => (10^y - 1) / (10^x - 1) * a >= b

②: 10^z * b + c >= 10^y * c + b => b >= (10^y - 1) / (10^z - 1) * c

连立①②: (10^y - 1) / (10^x - 1) * a >= b >= (10^y - 1) / (10^z - 1) * c

③: 10^z * a + c >= 10^x * c + a => (10^y - 1) / (10^x - 1) * a >= b >= (10^y - 1) / (10^z - 1) * c

所以① + ② -> ③

因此, 满足传递性.

综上, 我们定义的运算规则满足全序关系, 因此可以进行排序, 也证明我们的贪心算法是正确的.

优化: 把数转换成字符串按字典序排序, 不用真的转变成数字, 因为比较麻烦.

问题: 贪心体现在哪里?

答: 比较规则是贪心算法, 以局部最优 -> 全局最优


EOF.

相关推荐
大丈夫立于天地间16 分钟前
ospf收敛特性及其他的小特性
网络·网络协议·学习·算法·智能路由器·信息与通信
勤劳的进取家2 小时前
XML、HTML 和 JSON 的区别与联系
前端·python·算法
诚丞成2 小时前
栈算法篇——LIFO后进先出,数据与思想的层叠乐章(下)
c++·算法
清风~徐~来3 小时前
【算法】枚举
算法
qingy_20464 小时前
【算法】图解二叉树的前中后序遍历
java·开发语言·算法
Catherinemin4 小时前
剑指Offer|LCR 031. LRU 缓存
javascript·算法·缓存
从零开始学习人工智能5 小时前
安装指南:LLaMA Factory、AutoGPTQ 和 vllm
人工智能·python·深度学习·算法
WeeJot嵌入式5 小时前
【Linux】进程间通信IPC
linux·运维·算法
jerry2011085 小时前
python之二维几何学习笔记
开发语言·python·算法
廖显东-ShirDon 讲编程6 小时前
《零基础Go语言算法实战》【题目 4-6】随机选择单链表的一个节点并返回
算法·程序员·go语言·web编程·go web