1. 程式人生 > >【Facebook】等差子序列個數

【Facebook】等差子序列個數

example 來源 n-1 -a 多少 是我 () 鍵值對 solution

題目:

給定一整數數列,問數列有多少個子序列是等差數列。

即對於包含N個數的數列A,A(0),A(1),……,A(N-1),有多少組(P(0),P(1),……,P(k))滿足0<=P(0)<P(1)<……<P(k)<N,且A(P(0)),A(P(1)),……,A(P(k))為等差數列。

等差數列至少包含3個數,故必有k>=2,同時等差數列相鄰兩個數的差都是一樣的,即A(P(1))-A(P(0) = A(P(2))-A(P(1)) = …… = A(P(k))-A(P(k-1)) = d,d被稱為公差。

輸入保證N個整數的取值範圍均為-2^31 ~ 2^31-1,並且0<=N<=1000,同時保證輸出小於2^31-1。

Example:

輸入: [2, 4, 6, 8, 10]

輸出: 7

題解:

來源:九章算法公眾號(侵刪)。

時間復雜度為O(N^2)的動態規劃:

Ⅰ.我們令f(i,d)表示以A(i)結尾,公差為d的等差子序列的個數,這裏我們允許存在長度為2的等差子序列(所以對於數列中任意兩個數組成的子序列,我們都暫時認為其為等差子序列)。

那麽對於一對(i,j),j<i,A(i)-A(j)=d,對於所有以A(j)結尾,公差為d的等差子序列來說,後面再跟上A(i)之後還是公差為d的等差子序列,但變成了以A(i)結尾,再加上一對(A(j),A(i)),就得到了所有形如(……,A(j),A(i))的等差子序列。

換言之,j將對f(i,d)貢獻f(j,d)+1。故f(i,d)等於所有滿足j<i且A(i)-A(j)=d的(f(j,d)+1)之和。

Ⅱ.一個問題是d的範圍其實很大(-2^32+1 ~ 2^32-1),如果要對所有可能的d進行枚舉,那麽在時間上和空間上都是受不了的。

雖然d的取值範圍很大,但是對於N個數來說,兩兩之差最多只可能有N(N-1)/2種;而對於1個數A(i)來說,只需考慮所有小於i的j所產生的d=A(i)-A(j),最多有i種可能。

所以,對於每一個i,可以用一個HashMap來存儲鍵值對(d,f(i,d))。另一個問題是,我們在計算f(i,d)時,允許等差子序列長度為2(這一點是必要的,因為沒有長度為2的序列的話,就沒法在其末尾加上一個數得到更長的子序列),但答案要求的是所有長度至少為3的等差子序列的個數。

解決這個問題的方法有很多:在計算f(i,d)時,f(j,d)所表示的所有子序列長度都至少為2,在末尾加上A(i)之後,就成了滿足條件的等差子序列,故可以在計算f(i,d)的同時累加所有f(j,d),最後即可得到正確的答案(這種寫法比較簡潔但不太直觀);

也有一種比較容易理解的方法,那就是對所有f(i,d)之和,即所有長度至少為2的等差子序列的個數,減去長度為2的等差子序列的個數,而由於任意兩個數都構成長為2的等差子序列,所以其個數為N(N-1)/2,兩者相減得到的差即為正確答案。

Ⅲ.總結一下這個動態規劃算法:對於每個i=0,1,2,……,N-1,創建一個HashMap存儲鍵值對(d,f(i,d)),f(i,d)的初值為0,枚舉j<i,d=A(i)-A(j),則f(i,d)增加f(j,d)+1,同時對答案增加f(j,d)。計算完所有的i之後即可得到答案。

一個小細節是,如果d不在[-2^31+1 , 2^31-1]的範圍內,那麽以這個d為公差的數列長度不可能是3或3以上,故對於d在這個範圍外的情況可以直接跳過。

利用HashMap存取f(i,d),f(j,d)的復雜度為O(1),i,j枚舉的復雜度為O(N^2),故總的時間復雜度為O(N^2)。

Solution 1 :

int getNum(const vector<int> &nums) {
    if (nums.size() < 3) {
        return 0;
    }
    vector<unordered_map<int, int>> map(nums.size());
    int res = 0;
    for (int i = 0; i < nums.size(); ++i){
        for (int j = 0; j < i; ++j) {
            if (abs((long)nums[i] - nums[j]) > INT_MAX) {
                continue;
            }
            int d = nums[i] - nums[j];
            int map_i_d, map_j_d;
            map_i_d = map[i].count(d) ? map[i][d] : 0;
            map_j_d = map[j].count(d) ? map[j][d] : 0;
            map_i_d += map_j_d + 1;
            map[i][d] = map_i_d;
            res += map_j_d;
        }
    }
    return res;
}

事實上,確定一個等差數列只需要三個數,一個是等差數列的長度L,還有兩個是等差數列的最後兩個數(也可以是任意兩個中間的下標確定的數)。

記最後一個為E1,最後第二個為E2,則得公差d=E1-E2,通過公差可以推出等差數列中其余的數。

一個以E2,E1,結尾的等差數列,在末尾加上一個數E1+d後仍然是等差數列。於是我們可以使用動態規劃求解:令g(i,j)為以A(j),A(i)結尾的等差子序列的個數(j<i),(即形如(……,A(j),A(i))的等差數列的個數),然後我們可以通過枚舉倒數第三個數A(k)來統計g(i,j)。

對於形如(……,A(k),A(j))的等差子序列來說,如果有A(i)-A(j)=A(j)-A(k),那麽對應的(……,A(k),A(j),A(i))也為等差子序列,同時由於(A(k),A(j))長度為2,不計入g(j,k)的中,但(A(k),A(j),A(i))應計入g(i,j)中,故將g(j,k)計算入g(i,j)時還要額外加1。

於是我們有g(i,j)=Σ(g(j,k)+1),其中k滿足k<j且A(i)-A(j)=A(j)-A(k)。將所有得到的g(i,j)相加即可得到所有等差子序列的個數。這個算法的時間復雜度為O(N^3),考慮到N的範圍,這樣的時間復雜度可以接受,而且與上面講的算法相比簡潔許多。

Solution 2 :

int getAns(const vector<int> &nums) {
    if (nums.size() < 3)
        return 0;
    int n = nums.size();
    vector<vector<int>> v(n, vector<int>(n, 0));
    int res = 0;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < i; ++j) {
            for (int k = 0; k < j; ++k) {
                if (nums[i] - nums[j] == nums[j] - nums[k]) {
                    v[i][j] = v[j][k] + 1;
                    res += v[i][j];
                }
            }
        }
    }
    return res;
}

【Facebook】等差子序列個數