LeetCode 202次周赛 1553. Minimum Number of Days to Eat N Oranges

3 篇文章 0 订阅
1 篇文章 0 订阅

题目描述

There are n oranges in the kitchen and you decided to eat some of these oranges every day as follows:

  • Eat one orange.
  • I f the number of remaining oranges (n) is divisible by 2 then you can eat n/2 oranges.
  • If the number of remaining oranges (n) is divisible by 3 then you can eat 2*(n/3) oranges.
    You can only choose one of the actions per day.

Return the minimum number of days to eat n oranges.

Example 1:

Input: n = 10
Output: 4
Explanation: You have 10 oranges.
Day 1: Eat 1 orange, 10 - 1 = 9.
Day 2: Eat 6 oranges, 9 - 2*(9/3) = 9 - 6 = 3. (Since 9 is divisible by 3)
Day 3: Eat 2 oranges, 3 - 2*(3/3) = 3 - 2 = 1.
Day 4: Eat the last orange 1 - 1 = 0.
You need at least 4 days to eat the 10 oranges.

Example 2:

Input: n = 6
Output: 3
Explanation: You have 6 oranges.
Day 1: Eat 3 oranges, 6 - 6/2 = 6 - 3 = 3. (Since 6 is divisible by 2).
Day 2: Eat 2 oranges, 3 - 2*(3/3) = 3 - 2 = 1. (Since 3 is divisible by 3)
Day 3: Eat the last orange 1 - 1 = 0.
You need at least 3 days to eat the 6 oranges.

Example 3:

Input: n = 1
Output: 1
Example 4:

Input: n = 56
Output: 6

Constraints:

1 <= n <= 2*109

n个橙子,有3种吃法,
吃一个
如果橙子个数是偶数,可以吃掉一半
如果橙子个数能被3整除,吃掉2/3,剩余1/3
一天只能选择一种方式,问最少几天能把n个橙子吃完

思路分析

弱智动态规划

这题看完以后立马就能想到一个无脑动态规划
dp[i]表示吃完i个橙子最少要几天
对应3种吃法
dp[i] = min(dp[i], 1 + dp[i - 1])
dp[i] = min(dp[i], 1 + dp[i / 2]) if i % 2 == 0
dp[i] = min(dp[i], 1 + dp[i / 3]) if i % 3 == 0

这么算下去,o(n)时间就能求出答案。也容易证明答案是对的,把题目要求种所有的可能性都尝试了一边,等效于无脑暴力破解。

这么写肯定超时了,题目数据量可以给到2*109,20亿的数据量o(n)是不行的。

可行的动态规划

直觉上正确的思路

若当前有n个橙子,根据规则,n只能越吃越少。直觉告诉我们,要想迅速吃完,最好是遇到条件2和条件3,这样能一次吃一半或者吃三分之二。要真的每次都能这么吃,那很快就吃完了。于是乎,我们就想到找个最近的能被2或者3整除的数字,这样就可以在下一步的时候至少能用一个除法来痛快干一轮。

但是到底找最近的2还是3呢?不好说。那就都找找看
如何找到小于等于n最近的能被2整除的数?就是(n/2)*2,因为n = (n/2)*2 + n %2
同理,对于最近的能被3整除的数,就是(n/3)*3,因为n = (n/3)*3 + n % 3
这里的除法都是整数除法

那么dp[n]就可以有从dp[n/2]和dp[n/3]来算了。
dp[n] = min(dp[n], n % 2 + 1 + dp[n / 2])
dp[n] = min(dp[n], n % 3 + 1 + dp[n / 3])
其中n % 3是一个一个吃的,1是做除法的那一轮吃的

代码实现

class Solution {
    unordered_map<int, int> memo;
public:
    int minDays(int n) {
        memo.clear();
        return dp(n);
    }
    
    int dp(int n) {
        if ( n <= 2 ) return n;
        if ( memo.count(n) ) return memo[n];
        int res = n;
        res = min(res, 1 + n % 2 + dp(n / 2));
        res = min(res, 1 + n % 3 + dp(n / 3));
        memo[n] = res;
        return res;
    }
};

证明正确性

看完上面那个思路,有没有觉得有点不服气?这么选择有没有可能漏掉某些情况呢?如果你也有同样的疑虑,说明老哥你上道了,你是个严谨的程序员。我本人在当时做周赛的时候有这么想过,上面贴的代码也不是我当时写的,当时我就觉得这么想好像是对的,但是总是觉得特别心虚。我周赛的时候提交的代码是bfs + memo,是真的考虑了3种情况。虽然也通过了,但是感觉这题目因该不是我当初那么想的。

事后看了大神们的高票答案,直接找最近的2或者3是可行的,但是讨论区里面也没人给出详细的说明,总是感觉有点虚。我这里就尝试着证明一下为什么这个思路是完备的。

我们首先回顾一下无脑动态规划的思路
无脑的动态规划就是推给n-1,如果当前的n可以被2或者3除,那么就再多考虑这类情况。这么想当然是最接近题目规则的。直觉又告诉我们除法似乎是比减法要好的。我们拓展一下无脑动态规划,
dp[n] =min(dp[n], k + 1 + dp[(n - k) / 2]) 如果n - k可以被2整除
dp[n] =min(dp[n], k + 1 + dp[(n - k) / 3]) 如果n - k可以被3整除
我用一段代码写出求dp[n]过程,看懂下面的代码是理解后面证明的关键

int minDays(int n) {
   vector<int> dp(n + 1);
   dp[0] = 0, dp[1] = 1;
   for ( int i = 2; i <= n; ++i ) {
       dp[i] = i;
       for ( int k = 0; k <= i; ++k ) {
           if ( (i - k) % 2 == 0 ) {
               dp[i] = min(dp[i], k + 1 + dp[(i - k) / 2]);
           }
           if ( (i - k) % 3 == 0 ) {
               dp[i] = min(dp[i], k + 1 + dp[(i - k) / 3]);
           }
       }
   }
   return dp[n];
}

这么写复杂度变成o(n2)了,岂不是得不偿失。但是这个思路给我们带来一个很好得启发,我们可以选择下一步做除法的位置,在代码中体现就是满足条件的i - k。

下面我们来进行推导
先给一个简单结论: d p [ i ] ≤ d p [ i − k ] + k dp[i] \le dp[i - k] + k dp[i]dp[ik]+k
这个太弱智了,不证了。

不妨设下一步做除法的位置在i - k,除以3,(i - k) % 3 等于0.
我们可以找到好多个满足要求的k。
举个例子:假如i是19,k可以是1,4,7,10,13,16,19

设第一个满足条件的k是x,那么 k m = x + 3 ∗ m , m = 0 , 1 , 2... k_m = x + 3*m, m=0, 1,2... km=x+3m,m=0,1,2...
我们选定kl位置
得到了一种计算dp[i]的情况,即 c a n d i d a t e l = k l + 1 + d p [ ( i − k l ) / 3 ] candidate_l=k_l + 1 + dp[(i - k_l)/3] candidatel=kl+1+dp[(ikl)/3]
如果我们选定kl+1
得到了第二种计算dp[i]的情况,即 c a n d i d a t e l + 1 = k l + 1 + 1 + d p [ ( i − k l + 1 ) / 3 ] candidate_{l+1}=k_{l+1} + 1 + dp[(i - k_{l+1})/3] candidatel+1=kl+1+1+dp[(ikl+1)/3]
从k的定义我们得出 k l + 1 = k l + 3 k_{l+1}=k_l+3 kl+1=kl+3
得出如下
c a n d i d a t e l + 1 = k l + 1 + 1 + d p [ ( i − k l + 1 ) / 3 ] = k l + 3 + 1 + d p [ ( i − k l ) / 3 − 1 ] candidate_{l+1}=k_{l+1} + 1 + dp[(i - k_{l+1})/3] = k_l + 3 + 1 + dp[(i-k_l)/3-1] candidatel+1=kl+1+1+dp[(ikl+1)/3]=kl+3+1+dp[(ikl)/31]
又因为(参考前面的弱智结论)
1 + d p [ ( i − k l ) / 3 − 1 ] ≥ d p [ ( i − k l ) / 3 ] 1 + dp[(i-k_l)/3-1]\ge dp[(i - k_l)/3] 1+dp[(ikl)/31]dp[(ikl)/3]
所以 c a n d i d a t e l + 1 ≥ 2 + c a n d i d a t e l > c a n d i d a t e l candidate_{l+1}\ge 2 + candidate_l> candidate_{l} candidatel+12+candidatel>candidatel

结论,同样是找除以3的位置,越往后越亏,找第一个最好
同理,对于除以2的位置,也可以得出同样的结论

经过证明,第二段代码就可以顺理成章改成第一段代码。

复杂度分析

对于数字n,要么推给n/2,要么推给n/3。
计算dp[n]需要的所有状态是 n 2 a ∗ 3 b \frac{n}{2^a*3^b} 2a3bn
满足条件的a,b组合有多少对呢?
不太好算,但是不会超过log2(n)*log3(n)对
时间复杂度(上界)
o ( l o g 2 ( n ) ∗ l o g 3 ( n ) ) ≤ o ( ( l o g 3 ( n ) ) 2 ) o(log2(n)*log3(n))\le o((log3(n))^2) o(log2(n)log3(n))o((log3(n))2)

总结

这个题目通过率应该很高,但是想弄清楚背后的原理不是那么容易。这个题目向我们展示了同一套规则的两种不用的遍历搜索方法。正所谓条条大路通罗马。
第一种方法对应无脑动态规划。

第二种方法其实更无脑不说,还升了一个维度。但是第二种可以通过数学推导有效减少dp的状态数,实现了指数跳跃。塞翁失马焉知非福啊。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值