A positive integer is magical if it is divisible by either A or B.html
Return the N-th magical number. Since the answer may be very large, return it modulo 10^9 + 7
.git
Example 1:github
Input: N = 1, A = 2, B = 3 Output: 2
Example 2:函數
Input: N = 4, A = 2, B = 3 Output: 6
Example 3:rest
Input: N = 5, A = 2, B = 4 Output: 10
Example 4:code
Input: N = 3, A = 6, B = 4 Output: 8
Note:htm
1 <= N <= 10^9
2 <= A <= 40000
2 <= B <= 40000
這道題定義了一種神奇正整數,就是能同時被給定的正整數A或B整除的數,讓咱們返回第N個神奇的數字,暗示了這個數字可能很大,要對一個超大數取餘。又是對這個 1e9+7 取餘,博主下意識的反應是用動態規劃 Dynamic Programming 來作,但實際上是不能的,由於每一個數字能不能被A或B整除是相對獨立的,並不會跟以前的狀態有聯繫,這樣就很差寫出狀態轉移方程了,因此這並非一道 DP 題。首先來想,對於 [1, n] 中的數,能整除A的有多少個,舉例來講吧,假如 n=17,A=2,那麼 17 之內能整除2的就有 2,4,6,8,10,12,14,16,這八個數字,貌似正好是 n/A=17/2=8。再來看其餘例子,好比 n=17,B=3,那麼 17 之內能整除3的就有 3,6,9,12,15,這五個數字,貌似也是 n/B=17/3=5。那麼能被A或B整除的個數呢,好比 n=17,A=2,B=3,那麼 17 之內能整除2或3的數字有 2,3,4,6,8,9,10,12,14,15,16,這十一個數字,並非 n/A + n/B = 8+5 = 13,爲啥呢?由於有些數字重複計算了,好比 6,12,這兩個數字都加了兩次,咱們發現這兩個數字都是既能夠整除A又能夠整除B的,只要把這兩個數字減去 13-2=11,就是所求的了。怎麼找同時能被A和B整除的數呢,其實第一個這樣的數就是A和B的最小公倍數 Least Common Multiple,全部能被A和B的最小公倍數整除的數字必定能同時整除A和B。那麼最小公倍數 LCM 怎麼算呢?這應該是小學數學的知識了吧,就是A乘以B除以最大公約數 Greatest Common Divisor,這個最大公約數就不用多說了吧,也是小學的內容,是最大的能同時整除A和B的數。blog
明白了這些,咱們就知道了對於任意小於等於數字x的且能被A或B整除的正整數的個數爲 x/A + x/B - x/lcm(A,B)。因此咱們須要讓這個式子等於N,而後解出x的值即爲所求。直接根據式子去求解x獲得的不必定是正整數,咱們能夠反其道而行之,帶肯定的x值進入等式,算出一個結果,而後跟N比較大小,根據這個大小來決定新的要驗證的x值,這不就是典型的二分搜索法麼。肯定了要使用 Binary Search 後,就要來肯定x值的範圍了,x值最小能取到A和B中的較小值,因爲A和B最小能取到2,因此x的最小值也就是2。至於最大值,仍是根據上面的等式,x能取到的最大值是 N*min(A,B),根據題目中N和A,B的範圍,能夠推出最大值不會超過 1e14,這個已經超過整型最大值了,因此咱們初始化的變量都要用長整型。而後就是進入 while 循環了,斷定條件是上面寫的那個等式,其實這是博主以前的總結帖 LeetCode Binary Search Summary 二分搜索法小結 中的第四類,用子函數看成判斷關係,不是簡單的用 mid 來判斷,而是要經過 mid 來計算出須要比較的值。若計算值比N小,則去右半段,反之左半段,最後別忘了對M取餘便可,參見代碼以下:ip
解法一:leetcode
class Solution { public: int nthMagicalNumber(int N, int A, int B) { long lcm = A * B / gcd(A, B), left = 2, right = 1e14, M = 1e9 + 7; while (left < right) { long mid = left + (right - left) / 2; if (mid / A + mid / B - mid / lcm < N) left = mid + 1; else right = mid; } return right % M; } int gcd(int a, int b) { return (b == 0) ? a : gcd(b, a % b); } };
下面這種方法就比較 tricky 了,徹底是利用數學功底來解的,連二分搜索都不用,常數級的時間複雜度,碉堡了有木有?!這裏主要是參考了大神 jianwu 的帖子,博主也不能說是徹底理解透徹了,嘗試着去講解一下吧。咱們用這個例子來說解吧 A=3,B=5,N=10,前面的分析提到了,只有在A和B的最小公倍數 LCM 處,或者是能除以這個 LCM 的數字的地方,纔會出現重複。3和5的最小公倍數是 15,因此對於全部小於15的數字x,能整除A的數字有 x/A 個,能整除B的數字有 x/B 個,若把每個 LCM 看做一個 block 的話,這個區間內分別能整除A和B的數字是沒有重複的,因此這個區間的長度是 len = lcm/A + lcm/B - 1 = 15/3 + 15/5 - 1 = 7。下面要算的就是N裏面有多少個 block,而且餘數是多少,由於咱們能夠經過 LCM 快速來定位 block 的邊界位置,只要知道了偏移量,就能求出正確的神奇數字了。block 的個數是經過 N/len = 10/7 = 1,餘數是經過 N%len = 10%7 = 3 計算的。咱們能夠經過 block 的個數和長度快速定位到 15,這裏是不能直接加上餘數3的,由於餘數表示的是 15 後面的第三個能被3或5整除的數,18,20,21,因此答案是 21,這裏咱們須要算出這個偏移量 21-15=6,從而才能獲得正確結果。怎麼計算呢?想一下,15 後面第三個能被3整除的數是 18,21,24,因此只看3的話偏移量是 3/(1.0/3)=9,而 15 後面第三個能被5整除的數是 20,25,30,偏移量是 3/(1.0/5)=15,可是正確的偏移量應該是考慮兩種狀況的總和,因此應該是 nearest=3/(1.0/3 + 1.0/5)=5.625,但咱們的偏移量必定要是個整數,因此咱們再用一個取整的過程 min(ceil(nearest/3) x 3, ceil(nearest/5) x 5) = 6,最終就能夠獲得正確的偏移量,加上 15,就是正確的結果了,參見代碼以下:
解法二:
class Solution { public: int nthMagicalNumber(int N, int A, int B) { long lcm = A * B / gcd(A, B), M = 1e9 + 7; long len = lcm / A + lcm / B - 1, cnt = N / len, rem = N % len; double nearest = rem / (1.0 / A + 1.0 / B); int remIdx = min(ceil(nearest / A) * A, ceil(nearest / B) * B); return (cnt * lcm + remIdx) % M; } int gcd(int a, int b) { return (b == 0) ? a : gcd(b, a % b); } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/878
參考資料:
https://leetcode.com/problems/nth-magical-number/
https://leetcode.com/problems/nth-magical-number/discuss/154613/C%2B%2BJavaPython-Binary-Search