[LeetCode] 878. Nth Magical Number 第N個神奇數字



A positive integer is *magical* if it is divisible by either A or B.

Return the N-th magical number.  Since the answer may be very large, return it modulo 10^9 + 7.

Example 1:

Input: N = 1, A = 2, B = 3
Output: 2

Example 2:

Input: N = 4, A = 2, B = 3
Output: 6

Example 3:

Input: N = 5, A = 2, B = 4
Output: 10

Example 4:

Input: N = 3, A = 6, B = 4
Output: 8

Note:

  1. 1 <= N <= 10^9
  2. 2 <= A <= 40000
  3. 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的數。

明白了這些,我們就知道了對於任意小於等於數字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取余即可,參見代碼如下:


解法一:
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 的帖子](https://leetcode.com/problems/nth-magical-number/discuss/154965/o(1)-Mathematical-Solution-without-binary-or-brute-force-search),博主也不能說是完全理解透徹了,嘗試着去講解一下吧。我們用這個例子來講解吧 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

https://leetcode.com/problems/nth-magical-number/discuss/154965/o(1)-Mathematical-Solution-without-binary-or-brute-force-search


[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM