1. 程式人生 > >[LeetCode] Find the Derangement of An Array 找陣列的錯排

[LeetCode] Find the Derangement of An Array 找陣列的錯排

In combinatorial mathematics, a derangement is a permutation of the elements of a set, such that no element appears in its original position.

There's originally an array consisting of n integers from 1 to n in ascending order, you need to find the number of derangement it can generate.

Also, since the answer may be very large, you should return the output mod 109

 + 7.

Example 1:

Input: 3
Output: 2
Explanation: The original array is [1,2,3]. The two derangements are [2,3,1] and [3,1,2].

Note:
n is in the range of [1, 106].

這道題給了我們一個數組,讓我們求其錯排的個數,所謂錯排就是1到n中的每個數字都不在其原有的位置上,全部打亂了,問能有多少種錯排的方式。博主注意到了這道題又讓對一個很大的數取餘,而且每次那個很大的數都是109 + 7,為啥大家都偏愛這個數呢,有啥特別之處嗎?根據博主之前的經驗,這種結果很大很大的題十有八九都是用dp來做的,那麼就建一個一維的dp陣列吧,其中dp[i]表示1到i中的錯位排列的個數。那麼難點就是找遞推公式啦,先從最簡單的情況來看:

n = 1 時有 0 種錯排

n = 2 時有 1 種錯排 [2, 1]

n = 3 時有 2 種錯排 [3, 1, 2], [2, 3, 1]

然後博主就在想知道了dp[2],能求出dp[3]嗎,又在考慮是不是算加入數字3的情況的個數。結果左看右看發現沒有啥特別的規律,又在想是不是有啥隱含的資訊需要挖掘,還是沒想出來。於是看了一眼標籤,發現是Math,我的天,難道又是小學奧數的題?掙扎了半天最後還是放棄了,上網去搜大神們的解法。其實這道題是組合數學種的錯排問題,是有專門的遞迴公式的。

我們來想n = 4時該怎麼求,我們假設把4排在了第k位,這裡我們就讓k = 3吧,那麼我們就把4放到了3的位置,變成了:

x x 4 x

我們看被4佔了位置的3,應該放到哪裡,這裡分兩種情況,如果3放到了4的位置,那麼有:

x x 4 3

那麼此時4和3的位置都確定了,實際上只用排1和2了,那麼就相當於只排1和2,就是dp[2]的值,是已知的。那麼再來看第二種情況,3不在4的位置,那麼此時我們把4去掉的話,就又變成了:

x x x

這裡3不能放在第3個x的位置,在去掉4之前,這裡是移動4之前的4的位置,那麼實際上這又變成了排1,2,3的情況了,就是dp[3]的值。

再回到最開始我們選k的時候,我們當時選了k = 3,其實k可以等於1,2,3,也就是有三種情況,所以dp[4] = 3 * (dp[3] + dp[2])。

那麼遞推公式也就出來了:

dp[i] = (i - 1) * (dp[i - 1] + dp[i - 2])

有了遞推公式,程式碼就不難寫了吧,參見程式碼如下:

解法一:

class Solution {
public:
    int findDerangement(int n) {
        if (n < 2) return 0;
        vector<long long> dp(n + 1, 0);
        dp[1] = 0; dp[2] = 1;
        for (int i = 3; i <= n; ++i) {
            dp[i] = (dp[i - 1] + dp[i - 2]) * (i - 1) % 1000000007;
        }
        return dp[n];
    }
};

下面這種解法精簡了空間,因為當前值只跟前兩個值有關係,所以沒必要保留整個陣列,只用兩個變數來記錄前兩個值,並每次更新一下就好了,參見程式碼如下:

解法二:

class Solution {
public:
    int findDerangement(int n) {
        long long a = 0, b = 1, res = 1;
        for (int i = 3; i <= n; ++i) {
            res = (i - 1) * (a + b) % 1000000007;
            a = b;
            b = res;
        }
        return (n == 1) ? 0 : res;
    }
};

下面這種方法是對之前的遞推公式進行了推導變形,使其只跟前一個數有關,具體的推導步驟是這樣的:

我們假設 e[i] = dp[i] - i * dp[i - 1]

遞推公式為:  dp[i] = (i - 1) * (dp[i - 1] + dp[i - 2])

將遞推公式帶入假設,得到:

e[i] = -dp[i - 1] + (n - 1) * dp[i - 2] = -e[i - 1]

從而得到 e[i] = (-1)^n

那麼帶回假設公式,可得: dp[i] = i * dp[i - 1] + (-1)^n

根據這個新的遞推公式,可以寫出程式碼如下:

解法三:

class Solution {
public:
    int findDerangement(int n) {
        long long res = 1;
        for (int i = 1; i <= n; ++i) {
            res = (i * res + (i % 2 == 0 ? 1 : -1)) % 1000000007; 
        }
        return res;
    }
};

參考資料: