1414. 和为 K 的最少斐波那契数字数目 : 详解为何「每次选择不超过当前 k 的最大数」的可行解为最优解

简介: 1414. 和为 K 的最少斐波那契数字数目 : 详解为何「每次选择不超过当前 k 的最大数」的可行解为最优解

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 1414. 和为 K 的最少斐波那契数字数目 ,难度为 中等


Tag : 「数学」、「二分」、「贪心」


给你数字 k ,请你返回和为 k 的斐波那契数字的最少数目,其中,每个斐波那契数字都可以被使用多次。


斐波那契数字定义为:


  • F1 = 1
  • F2 = 1
  • Fn = Fn-1 + Fn-2 , 其中 n > 2 。


数据保证对于给定的 k ,一定能找到可行解。


示例 1:


输入:k = 7
输出:2 
解释:斐波那契数字为:1,1,2,3,5,8,13,……
对于 k = 7 ,我们可以得到 2 + 5 = 7 。
复制代码


示例 2:


输入:k = 10
输出:2 
解释:对于 k = 10 ,我们可以得到 2 + 8 = 10 。
复制代码


示例 3:


输入:k = 19
输出:3 
解释:对于 k = 19 ,我们可以得到 1 + 5 + 13 = 19 。
复制代码


提示:


  • 1 <= k <= 10^91<=k<=109


打表 + 贪心 + 二分



利用 k 的数据范围为 1 <= k <= 10^91<=k<=109,可以先使用 static 进行打表,预处理出值不超过 10^9109 的斐波那契数,存入数组 list 中。


然后考虑如何使用 list 中的数字(可重复)凑成 k


一个直观的想法是:每次从 list 中找到不超过 k 的最大数,用于进行对 k 的消减,直到 k 被消减到 00 为止,消减的次数即是答案。


而「从 list 中找到不超过 k 的最大数」这一操作,可使用「二分」。


下面证明该做法的正确性:


假定该做法所得到的可行解序列为 A(序列长度为 ansans),真实最优解序列为 B(序列长度为 minmin)。


假设两者长度不等(只能是 ans > minans>min),这意味着我们将 AB 之间的相同的元素去掉后,剩余元素序列有如下结论:


  • A 序列剩余元素之和」等于「B 序列剩余元素之和」;
  • A 序列剩余元素个数」大于「B 序列剩余元素个数」。


A'A 的剩余元素序列,B'B 的剩余元素序列。


我们知道 A' 中的最大数必然大于 B' 中的最大数。不可能是等于关系(相等元素均被去掉),也不可能是小于关系,否则在构造 A 序列的时候就会按照生成 B 序列的方向进行构造。


但要只靠该性质,要证明不存在满足上述结论的斐波那契数组合仍是困难。


我们可以从「斐波那契数」性质出发,挖掘可行解 A 的某些性质,然后证明不存在不满足该性质的方案比 A 更优即可。


对于可行解 A 而言,由于我们「每次都选择不超过当前 k 的最大数」,因此必然可行解中必然不存在两项相邻的斐波那契数,否则会在选择 f_ifif_{i+1}fi+1 之前先因为「每次都选择不超过当前 k 的最大数」而先选择 f_{i+2}fi+2


同时,由于 f_i=f_{i-1}+f_{i-2}fi=fi1+fi2f_{i+1}=f_{i}+f_{i-1}fi+1=fi+fi1f_{i+2}=f_{i+1}+f_{i}fi+2=fi+1+fi 可得 2 * f_{i}=f_{i+1}+f_{i-2}2fi=fi+1+fi2


也就是说可行解 A 中必然不会出现相同值,否则会在选择 f_ifi 之前先因为「每次都选择不超过当前 k 的最大数」而先选择 f_{i+1}fi+1


该推理对于边界特殊值 11 同样成立,如果可行解 A 中存在多个 11,则必然会先因为「每次都选择不超过当前 k 的最大数」选择 f_3=2f3=2


再根据「斐波那契数奇偶数项求和」可知:a_1 + a_3 + ... + a_{2n - 1} = a_{2n}a1+a3+...+a2n1=a2na_2 + a_4 + ... + a_{2n} = a_{2n+1} - 1a2+a4+...+a2n=a2n+11


综上,可以证明不存在比可行解 A 更短的最优解。


代码:


class Solution {
    static List<Integer> list = new ArrayList<>();
    static {
        list.add(1);
        int a = 1, b = 1;
        while (b <= (int)1e9) {
            int c = a + b;
            a = b; b = c;
            list.add(c);
        }
    }
    public int findMinFibonacciNumbers(int k) {
        int ans = 0;
        while (k != 0) {
            int l = 0, r = list.size() - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (list.get(mid) <= k) l = mid;
                else r = mid - 1;
            }
            k -= list.get(r);
            ans++;
        }
        return ans;
    }
}
复制代码


  • 时间复杂度:令值不超过 10^9109 的斐波那契数的数量为 C,复杂度为 O(\log{k} * \log{C})O(logklogC)
  • 空间复杂度:O(C)O(C)


贪心



上述做法,由于预处理了所有值不超过 10^9109 的斐波那契数,导致无法直接取得「值不超过 k 的最大数」,需要再套一层「二分」。


不采用预处理的方式,利用斐波那契数列的递推性质,以及凑成 k 的最优解中不包含重复数字的结论,我们可以做到 O(\log{k})O(logk) 时间复杂度和 O(1)O(1) 空间复杂度。


代码:


class Solution {
    public int findMinFibonacciNumbers(int k) {
        int a = 1, b = 1;
        while (b <= k) {
            int c = a + b;
            a = b; b = c;
        }
        int ans = 0;
        while (k != 0) {
            if (k >= b) {
                k -= b; ans++;
            }
            int c = b - a;
            b = a; a = c;
        }
        return ans;
    }
}
复制代码


  • 时间复杂度:O(\log{k})O(logk)
  • 空间复杂度:O(1)O(1)


最后



这是我们「刷穿 LeetCode」系列文章的第 No.1414 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
【Leetcode -746.使用最小花费爬楼梯 -747.至少是其他数字两倍的最大数】
【Leetcode -746.使用最小花费爬楼梯 -747.至少是其他数字两倍的最大数】
71 0
算法修炼Day52|● 300.最长递增子序列 ● 674. 最长连续递增序列 ● 718. 最长重复子数组
算法修炼Day52|● 300.最长递增子序列 ● 674. 最长连续递增序列 ● 718. 最长重复子数组
|
6月前
|
算法 测试技术 C++
【数论】【分类讨论】【C++算法】1611使整数变为 0 的最少操作次数
【数论】【分类讨论】【C++算法】1611使整数变为 0 的最少操作次数
|
6月前
|
算法 测试技术 C++
【动态规划】【C++算法】801. 使序列递增的最小交换次数
【动态规划】【C++算法】801. 使序列递增的最小交换次数
【剑指offer】-数组汇总出现次数超过一半的数字-27/67
【剑指offer】-数组汇总出现次数超过一半的数字-27/67
|
Cloud Native Go
801. 使序列递增的最小交换次数:动态规划
这是 力扣上的 801. 使序列递增的最小交换次数,难度为 困难。
剑指offer 40. 数组中出现次数超过一半的数字
剑指offer 40. 数组中出现次数超过一半的数字
78 0
|
算法 C++ Python
【每日算法Day 90】5种方法:求解数组中出现次数超过一半的那个数
【每日算法Day 90】5种方法:求解数组中出现次数超过一半的那个数
168 0
|
算法 C++ Python
【每日算法Day 91】求解数组中出现次数超过1/3的那个数
【每日算法Day 91】求解数组中出现次数超过1/3的那个数
|
测试技术
软件测试面试题:如果一个数恰好等于它的因子之和,则称该数为“完全数”,又称完美数或完备数。 例如:第一个完全数是6,它有约数1、2、3、6,除去它本身6外,其余3个数相加, 1+2+3=6。第二个完全
软件测试面试题:如果一个数恰好等于它的因子之和,则称该数为“完全数”,又称完美数或完备数。 例如:第一个完全数是6,它有约数1、2、3、6,除去它本身6外,其余3个数相加, 1+2+3=6。第二个完全
463 0
下一篇
无影云桌面