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。

总结

优化后的代码运行速度确实让我感到很惊讶,这样一个小小的优化竟然产生了几倍的运行时间的差距,不过优化的思想还是很有借鉴意义的,以后应该是能够用到类似的优化方法。
另外就是在大数据处理的情况下,最好还是不要用带备忘的深度优先搜索作为动态规划的实现方案,因为运行时间确实很慢。

知识点
动态规划

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