Increasing Subsequences

一. Increasing Subsequences

Given an integer array, your task is to find all the different possible increasing subsequences of the given array, and the length of an increasing subsequence should be at least 2 .

Example:

Input: [4, 6, 7, 7]
Output: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

Note:

  1. The length of the given array will not exceed 15.
  2. The range of integer in the given array is [-100,100].
  3. The given array may contain duplicates, and two equal integers should also be considered as a special case of increasing sequence.

Difficulty:Medium

TIME:TIMEOUT

解法

题目是求一个序列的所有递增子序列,由于之前思考过求最长递增子序列的方法,最大复杂度是 O(n2) ,因此以为这道题也能套用求最长递增子序列的方法,能在多项式的时间内求解,然而并不能。
其实认真想一想也就知道,这道题是求所有的增序列,也就是求序列的所有组合,时间复杂度应该约为 O(2n) 才对,既然已经限定了数组长度不会超过15,就算是 O(2n) 的复杂度也不会超时,因此可以放心大胆地用暴力搜索。
搜索的方式当然不止一种,我这里采用的是加或不加的方式,也就是如果看到数字大于等于子序列的最后一个数,一种情况是将这个数字加入子序列中,另一种是不加入子序列中,然后分别搜索。
另外一点就是判重的问题,首先想到的当然是哈希,然后就是如何把子序列映射为哈希的键的问题,这里采用的是将子序列转换为字符串,然后将这个字符串作为键来存入map中。

void dfs(vector<int>& nums, vector<vector<int> >& result, int index, vector<int>& seq, string s, map<string, int> &m) {
    while(index < nums.size()) {
        if(seq.size() == 0 || nums[index] >= seq.back()) {
            dfs(nums, result, index + 1, seq, s, m);
            seq.push_back(nums[index]);
            /*加200再转化为字符串是必须的,因为可以少考负数以及两位数和一位数的情况*/
            dfs(nums, result, index + 1, seq, s + to_string(nums[index] + 200), m);
            seq.pop_back();
            break;
        }
        index++;
    }
    if(!m[s] && seq.size() >= 2) {
        result.push_back(seq);
        m[s] = 1;
    }
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> seq;
    map<string,int> m;
    dfs(nums, result, 0, seq, "", m);
    return result;
}

代码的时间复杂度为 O(2n)

优化

上面的代码存在如下问题:

  • 首先是把子序列映射为键来存储是否合适,因为数字转为字符串也需要花费时间,如果采用全排列的方式或许会更好一点。因为总共就 2n 种情况,那么可以把每种情况映射为 1 2n之间的任意一个数,然后把这个数作为map的键来存储。
  • 另外一点就是剪枝的问题,上面的程序并没有任何剪枝操作。对于递增子序列来说,很重要的一个特性就是如果后面有两个同样的数字都可以加入序列,那么就可以只考虑前面的那一种情况。比如序列<1,2,1,4>,只需要考虑前面的那一个1,而不需要考虑后面的那一个1,因为前面那一个1包含的情形比肯定后面那一个1的多(前面那一个包含<1,2,4>,<1,2>,<1,4>,<1,1>,<1,1,4>等序列,后面那一个只包含<1,4>)。当然这需要另一种搜索方式,也就是每次加一的方式,加或不加的搜索方式并不能获取这些信息。
  • 如果采用另一种搜索方式的话,那么就可以省略哈希的步骤了,因为另一种搜索方式加上剪枝就可以避免所有的重复情况。

优化后的代码如下:

void dfs(vector<int>& nums, vector<vector<int> >& result, int index, vector<int>& seq) {
    if(seq.size() > 1)
        result.push_back(seq);
    set<int> u;
    for(int i = index; i < nums.size(); i++) {
        if((seq.size() == 0 || nums[i] >= seq.back()) && u.find(nums[i]) == u.end()) {
            seq.push_back(nums[i]);
            u.insert(nums[i]);
            dfs(nums, result, i + 1, seq);
            seq.pop_back();
        }
    }
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> seq;
    dfs(nums, result, 0, seq);
    return result;
}

时间复杂度为 O(2n)

速度快了10%左右,更重要的是代码会简化很多。

其他解法

当然,很多时候,递归操作都比较耗时间,因此,最好能够用非递归的方法来解决问题。对于这道题来说,确实有非递归的方法。那就是从序列的右边开始来求解,记录每个数字到序列末尾的所有递增子序列。相同的数字只保留最左边的。

vector<vector<int>> findSubsequences(vector<int>& nums) {
    vector<vector<int>> result;
    map<int, vector<vector<int>>> m; //记录每个数字到末尾的所有递增子序列
    int len = nums.size();
    for (int i = len - 1; i >= 0; i--) {
        /*当map中已经存储了当前数字的所有递增子序列,那么就在所有递增子序列前面插入当前值,这种情况要优先处理*/
        if(m[nums[i]].size() != 0) {
            for (int k = 0; k < m[nums[i]].size(); k++) {
                m[nums[i]][k].insert(m[nums[i]][k].begin(), nums[i]);
            }
        }
        for (auto j = m.begin(); j != m.end(); j++) {
            if(j->first > nums[i]) {
                for (int k = 0; k < j->second.size(); k++) {
                    vector<int> x(j->second[k]);
                    /*在所有的递增子序列中插入当前值*/
                    x.insert(x.begin(), nums[i]);
                    m[nums[i]].push_back(x);
                }
            }
        }
        vector<int> x{ nums[i] };
        m[nums[i]].push_back(x);
    }
    for (auto i = m.begin(); i != m.end(); i++) {
        for (int j = 0; j < i->second.size(); j++) {
            if(i->second[j].size() > 1)
                result.push_back(i->second[j]);
        }
    }
    return result;
}

时间复杂度为 O(2n)

运行时间和剪枝版本的递归算法并没有特别大的差别。

总结

对搜索来说,剪枝是很重要的,虽然剪枝并不能让整体的复杂度降低,但很多时候都能极大地优化代码的运行。这道题就是一个很好的例子。

知识点
深度优先搜索
递增子序列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值