Arithmetic Slices II - Subsequence

一. Arithmetic Slices II - Subsequence

A sequence of numbers is called arithmetic if it consists of at least three elements and if the difference between any two consecutive elements is the same.

For example, these are arithmetic sequences:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

The following sequence is not arithmetic.

1, 1, 2, 5, 7

A zero-indexed array A consisting of N numbers is given. A subsequence slice of that array is any sequence of integers (P0, P1, …, Pk) such that 0 ≤ P0 < P1 < … < Pk < N.

A subsequence slice (P0, P1, …, Pk) of array A is called arithmetic if the sequence A[P0], A[P1], …, A[Pk-1], A[Pk] is arithmetic. In particular, this means that k ≥ 2.

The function should return the number of arithmetic subsequence slices in the array A.

The input contains N integers. Every integer is in the range of -231 and 231-1 and 0 ≤ N ≤ 1000. The output is guaranteed to be less than 231-1.

Example:

Input: [2, 4, 6, 8, 10]

Output: 7

Explanation:
All arithmetic subsequence slices are:
[2,4,6]
[4,6,8]
[6,8,10]
[2,4,6,8]
[4,6,8,10]
[2,4,6,8,10]
[2,6,10]
Subscribe to see which companies asked this question.

Difficulty:Hard

TIME:50MIN

解法一(帶備忘的深度優先搜索)

這道題輸入最大長度爲1000,因此只能用O(n2) 以內的算法來解,借鑑於之前做過一道類似的題Distinct Subsequences,而且解法極其相似,因此就不做更多的介紹。

int dfs(vector<int>& nums,int left, long dis, vector<unordered_map<long,int>> &m) {
    int num = 0;
    for(int i = left + 1; i < nums.size(); i++) {
        if((long)nums[i] - (long)nums[left] == dis) {
            num++;
            if(m[i].find(dis) != m[i].end())
                num += m[i][dis];
            else
                num += dfs(nums, i, dis, m); 
        }
    }
    m[left][dis] = num;
    return num;
}
int numberOfArithmeticSlices(vector<int>& nums) {
    int num = 0;
    vector<unordered_map<long,int>> m(nums.size());
    for(int i = 0; i + 1 < nums.size(); i++) {
        for(int j = i + 1; j < nums.size(); j++) {
            num += dfs(nums,j,(long)nums[j] - (long)nums[i],m);
        }
    }
    return num;
}

代碼的時間複雜度接近於O(n2) ,實際運行時間約爲1500ms。

解法二(動態規劃)

第一種解法雖然合理,但卻很容易超時,因爲遞歸的過程極其消耗時間,特別是遞歸的次數過多的時候。因此,第二種解法就不用遞歸來求解,不過原理也差不多。
最優子結構也很容易理解,假設使用一個二維數組m,那麼第一維表示序列的下標,第二維表示差值,值表示以這個下標爲前綴的所有相同差值的子序列的數目,而且0<j<i

  • disxixj ,那麼m[i][dis]=1
  • 如果存在m[j][dis] ,那麼m[i][dis]+=m[j][dis]
int numberOfArithmeticSlices(vector<int>& nums) {
    int num = 0;
    vector<unordered_map<long,int>> m(nums.size());
    long dis;
    for(int i = 1; i < nums.size(); i++) {
        for(int j = 0; j < i; j++) {
            dis = (long)nums[i] - (long)nums[j]; 
            m[i][dis] ++;
            if(m[j].find(dis) != m[j].end()) {
                m[i][dis] += m[j][dis];
                num += m[j][dis];
            }
        }
    }
    return num;
}

代碼時間複雜度爲O(n2) ,實際運行時間約爲850ms。

優化

上面的代碼還有一些地方可以優化:

  • 首先是並不需要long類型來記錄差值,因爲如果差值大於int類型所能表達的範圍,那麼不可能有三個數以這個差值爲等差數列
  • 然後再看一下for循環內部的操作,按理說唯一耗時的操作就應該是map的find操作了,因此,如果可以降低find操作的時間,那麼也能優化代碼的運行。如何減低find操作的時間呢,就是減少插入map中的數據。具體的做法就是先對序列元素進行一次預處理,就是用set保存所有的唯一序列元素。這樣如果遍歷到一個數,如果集合中沒有等於這個數加上差值的數,那麼就不把這個數保存進map中(反正也用不到)。這樣做的好處就是之前map中就算有兩個元素的差值還是要保存下來,而現在必須得有三個元素的差值相等才能保存進map中。因此,map中的數據就會變少很多。
int numberOfArithmeticSlices(vector<int>& nums) {
    int num = 0;
    vector<unordered_map<int,int>> m(nums.size());
    unordered_set<int> s(nums.begin(), nums.end());
    long dis;
    int tmp = 0;
    for(int i = 1; i < nums.size(); i++) {
        for(int j = 0; j < i; j++) {
            dis = (long)nums[i] - (long)nums[j];
            if(dis > INT32_MAX || dis < INT32_MIN)
                continue;
            tmp = 0;
            if(m[j].find(dis) != m[j].end())
                tmp = m[j][dis];
            num += tmp;
            if(s.find(nums[i] + dis) != s.end())
                m[i][dis] += tmp + 1;
        }
    }
    return num;
}

代碼時間複雜度爲O(n2) ,實際運行時間約爲150ms。

總結

優化後的代碼運行速度確實讓我感到很驚訝,這樣一個小小的優化竟然產生了幾倍的運行時間的差距,不過優化的思想還是很有借鑑意義的,以後應該是能夠用到類似的優化方法。
另外就是在大數據處理的情況下,最好還是不要用帶備忘的深度優先搜索作爲動態規劃的實現方案,因爲運行時間確實很慢。

知識點
動態規劃

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章