1553. Minimum Number of Days to Eat N Oranges
- 题目描述
- 思路分析
- 弱智动态规划
- 可行的动态规划
- 直觉上正确的思路
- 代码实现
- 证明正确性
- 复杂度分析
- 总结
题目描述
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。
下面我们来进行推导
先给一个简单结论:
这个太弱智了,不证了。
不妨设下一步做除法的位置在i - k,除以3,(i - k) % 3 等于0.
我们可以找到好多个满足要求的k。
举个例子:假如i是19,k可以是1,4,7,10,13,16,19
设第一个满足条件的k是x,那么
我们选定kl位置
得到了一种计算dp[i]的情况,即
如果我们选定kl+1
得到了第二种计算dp[i]的情况,即
从k的定义我们得出
得出如下
又因为(参考前面的弱智结论)
所以
结论,同样是找除以3的位置,越往后越亏,找第一个最好
同理,对于除以2的位置,也可以得出同样的结论
经过证明,第二段代码就可以顺理成章改成第一段代码。
复杂度分析
对于数字n,要么推给n/2,要么推给n/3。
计算dp[n]需要的所有状态是
满足条件的a,b组合有多少对呢?
不太好算,但是不会超过log2(n)*log3(n)对
时间复杂度(上界)
总结
这个题目通过率应该很高,但是想弄清楚背后的原理不是那么容易。这个题目向我们展示了同一套规则的两种不用的遍历搜索方法。正所谓条条大路通罗马。
第一种方法对应无脑动态规划。
第二种方法其实更无脑不说,还升了一个维度。但是第二种可以通过数学推导有效减少dp的状态数,实现了指数跳跃。塞翁失马焉知非福啊。