最长子序列相关问题

子序列:不一定连续
子串:一定连续

最长连续序列—Leetcode.128

题目描述:给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。

答:
map的复杂度超了,unordered_map存元素和下标,逐个计算以该点为起点的最长元素;

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_map<int,int> a;
        for(auto i:nums)
            a[i]=i;
        int ans=0;
        for(int i=nums.size()-1;i>=0;--i){
            if(!a.count(nums[i]-1)){
                int cur=nums[i];
                while(a.count(cur+1)){
                    ++cur;
                }
                ans=max(ans,cur-nums[i]+1);
            }
        }
        return ans;
    }
};

用unordered_set,不需要存键值对,仅从最小的元素开始计算 以该节点为起点的连续子序列长度;

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set s(nums.begin(), nums.end());
        int res = 0;
        for(int& num : nums)
        {
            if(s.find(num-1) != s.end()) continue;
            int length = 1;
            while(s.find(num+1) != s.end())
            {
                ++length;
                ++num;
            }
            res = max(res, length);
        }
        return res;
    }
};

最长上升子序列—Leetcode.300

题目描述:给定一个无序的整数数组,找到其中最长上升子序列的长度。

答:动态规划,逐个遍历数组元素,dp数组保存以当前元素为结尾的最长递增子序列长度;

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.size() == 0) return 0;
        int n = nums.size();
        vector<int> dp(n, 1);
        for(int i = 0; i < n; ++i)
        {
            for(int j = 0; j < i; ++j)
            {
                if(nums[j] < nums[i])
                    dp[i] = max(dp[i], dp[j]+1);
            }
        }
        return *max_element(dp.begin(), dp.end());
    }
};

优化:当子序列长度相同时,末尾数越小,在遍历数组后续元素时,能够组成更长的序列;
以tail数组保存 所有长度为i+1的子序列中 末尾数最小的值;
每遍历一个新的数,若该数大于tail中所有的数,该数可以放在任一子序列后组成更长的子序列;
若该数小于长为i+1的子序列的末尾数,将末尾数替换为该值;
那么最长升序子序列就是tail数组的长度;

class Solution {
public:
    int lengthOfLIS(vector<int> &nums) {
        int len = nums.size();
        if (len < 2) {
            return len;
        }
        vector<int> tail(len, 0);
        tail[0] = nums[0];

        int end = 0;
        for (int i = 1; i < len; ++i) {
            int left = 0;
            int right = end + 1;
            while (left < right) {  //二分查找第一个比nums[i]大的数
                int mid = (left + right) >> 1;
                if (tail[mid] < nums[i]) {
                    left = mid + 1;
                } else {
                    right = mid;   //从mid开始向左搜是否有更小的mid满足>nums[i]
                }
            }
            tail[left] = nums[i]; 
            if (left == end + 1) {
                end++;
            }
        }
        return end + 1;
    }
};

最长递增子序列的个数—Leetcode.673

题目描述:给定一个未排序的整数数组,找到最长递增子序列的个数。

答:动态规划

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int len = nums.size();
        if(len < 2) return len;
        vector<int> lengths(len, 1);
        vector<int> counts(len, 1);
        for(int r = 1; r < len; ++r)//从第二个元素开始
            for(int l = 0; l < r; ++l)//每个元素都遍历在它之前的所有元素
                if(nums[l] < nums[r])//左边有比自身小的,就可以组成递增子序列
                {
                    if(lengths[l] >= lengths[r])
                    {
                        lengths[r] = lengths[l] + 1;
                        counts[r] = counts[l];
                    }
                    else if(lengths[l] + 1 == lengths[r])
                        counts[r] += counts[l];
                }
        int cur = 0, res = 0;
        for(int& length : lengths)
            cur = max(cur, length);
        for(int i = 0; i < len; ++i)
            if(lengths[i] == cur)
                res += counts[i];
        return res;
    }
};

线段树(纯转载,代码为java,理解思路了自己试试写)

节点的含义:

区间,range_left-range_right,表示nums可以中落在这个区间的值(根节点root是nums_min-nums_max,所以所有值都能落在这root区间)

val: {length, count} 在节点的区间中,最长的长度以及其对应个数

left: 左子树,细分当前节点的区间,range_left-mid,nums中落在该区间的值,所能组成的val:最长的长度以及其对应个数

right: 右子树,细分当前节点的区间,mid+1 - range_right,nums中落在该区间的值,所能组成的val:最长的长度以及其对应个数

故root.val则表示nums中,最长长度的递增子序列的length,以及对应的个数count

root.left.val同理,表示nums中小于mid的值,所对应的递增子序列中,最长的长度是多少,以及个数(针对的是值,即[1,7, 3, 2],小于4的有1、3、2,root.left.val = {length: 2, count: 2},分别是3和2)

左右子树是用来递归获取当前节点的最长长度以及对应个数,叶子节点最后都是1-1,2-2, 3-3,代表某个值所能组成的最长递增子序列的长度和个数

插入一个nums中的值后,对应的叶子节点值会更新, 然后往上,父节点会合并两个子节点(即区间合并),有长度大的则取其,两者长度相等,则合并个数即可


线段树更新:

[1, 7, 3, 2]

初始化root: range:[1, 7], left:null, right: null,val: {0, 1}

开始: 1: 从根节点查找小于1的区间的最长递增子序列长度及个数,即区间右值为0的:此时root中区间不包括0,返回默认值{0, 1}

往root插入1,以及其对应的val,即上面查找的,使其其长度+1即可(长度为n的nums中,其后面加个大于nums中所有数的值,不影响最长子序列个数,只影响长度)

插入过程中先通过向下递归找到区间为[1, 1]的节点,更新该节点的值(merge(val,node.val)两者取length大者,相同则count相加)

完成后向上回溯更新父节点的值:左节点和右节点,两者val取length大者,相同则count相加,一直回溯到root

插入1后,所有区间包含1的节点.val都为{1, 1}

7: 从根节点查找小于7的区间的最长递增子序列长度及个数,即小于等于7-1=6,6在根节点区间1-7内,

则merge(leftTree[1-4].val, rightTree[5, 7]) = {1, 1},右子树还是初始值{0, 1},左子树经过上面一步已经是{1, 1}

往root插入7,val为 上面查找的length+1:{1+1, 1},一直向下递归找到[7, 7]区间的节点

然后往上回溯,区间5-7节点node.val = merge(node.getLeft().val, node.getRight().val);,此时左子树区间[5-6]的val还是初始值{0, 1},而右子树区间[7,7]则为{2, 1},则node.val = {2, 1}, 再往上,root.val = merge({1, 1}, {2, 1}) = {2, 1}

3: 查找小于3的区间的最长递增子序列长度及个数,即小于等于3-1=2,val: {1, 1}

往root插入3,val为merge({2, 1}, {0, 1}) = {2, 1},一直向下递归找到[3,3]区间的节点

然后往上回溯,区间3-4节点node.val = merge(node.getLeft().val, node.getRight().val);,此时右子树区间[4-4]的val还是初始值{0, 1},而右子树区间[3,3]则为{2, 1},则node.val = {2, 1}

区间1-4节点node.val = merge({1, 1}, {2, 1}) = {2, 1}, 左子树1-2,val还是插入1时的{1, 1},右子树则为此次更新的{2, 1}

区间1-7节点node.val = merge({2, 1}, {2, 1}) = {2, 2}, 右子树5-7,val还是插入7时的{2, 1},左子树则为此次更新的{2, 1}

2: 查找小于2的区间的val,即小于等于2-1,即[1, 1]的val,{1, 1}

插入2,val为{1+1, 1},向下递归找到[2, 2]的节点

node.val = merge({2, 1}, {0, 1}) = {2, 1};

向上回溯,[1-2]的节点val = merge({1, 1}, {2, 1})(左子树[1, 1], 右子树[2, 2]) = {2, 1}

向上回溯,[1-4]的节点val = merge({2, 1}, {2, 1})(左子树[1, 2], 右子树[3, 4]) = {2, 2}

向上回溯,[1-7]的节点val = merge({2, 2}, {2, 1})(左子树[1, 4], 右子树[5, 7]) = {2, 3}

结束:root.val {2, 3}即最大长度的递增子序列长度为2,有3个

------ val {length, count},最大长度递增子序列的长度,以及其个数

函数理解:

public Value merge(Value v1, Value v2) {
    if (v1.length == v2.length) {
        if (v1.length == 0) return new Value(0, 1);
        return new Value(v1.length, v1.count + v2.count);
    }
    return v1.length > v2.length ? v1 : v2;
}

合并两个节点的val,即合并两个区间的值到更大区间中,则合并后的区间中, 最大长度递增子序列,为两个区间各自val的length较大者,两个区间val的length相同则只用合并count相加({0, 1}与{0, 1}合并还是返回{0, 1}

如[1,7,3,2]中,插入3落在[3,4]使得其val为{2, 1},而[1-2]在val还是此前插入1时的{1,1},所以两者合并到[1-4]取[3-4]区间取{2,1}作为val([1-2]/[3-4]的val互相独立,互不干扰,目前{2, 1}代表的是值3所能组成的最大长度,其在区间[3-4]则表示落入区间的值3和值4(如果有)所能组成的最大长度)

后面继续插入2后,则[1-2]的val会被修改为{2, 1},则再合并[1-2]/[3-4]时,两者都为{2, 1},则[3-4] val = {2, 1 + 1}

public void insert(Node node, int key, Value val) {
    if (node.range_left == node.range_right) {
        node.val = merge(val, node.val);
        return;
    } else if (key <= node.getRangeMid()) {
        insert(node.getLeft(), key, val);
    } else {
        insert(node.getRight(), key, val);
    }
    node.val = merge(node.getLeft().val, node.getRight().val);
}

在指定节点的树中,向下找到对应位置的叶子节点,更新该节点的val,然后往上回溯更新上层节点的val

树中的叶子节点都是[n, n]表示单个点的区间,首先在获取该节点的左子树或者右子树时,不存在则直接创建并返回,而代码中不存在获取[n, n]节点的操作,所以[n, n]节点没有子节点,因为不会去获取

public Value query(Node node, int key) {
    if (node.range_right <= key) return node.val;
    else if (node.range_left > key) return new Value(0, 1);
    else return merge(query(node.getLeft(), key), query(node.getRight(), key));
}

从指定节点的区间中,获取小于等于某个值时,nums中已插入的值,所能组成的最大长度子序列的val

主要是在插入某个值n时,计算n的val,即比n小的已插入值能组成的最大长度子序列和个数,则n的val即为{length+1, val},(因为n比获取的length子序列中的值都大,拼接到后面即可)

如[1, 7, 3, 2]中,root区间为[1-7],左子树区间为[1-4],右子树: [5-7];

则在插入7,获取已插入的,小于7的最大长度子序列,获取节点区间range_right 为6的val即可,首先root的range_right :7 <= 6不成立,root的range_left:1 > 6也不成立,则取左子树或右子树的val,左子树[1-4]满足第一个条件range_right :4 <= 6,返回node.val{1, 1},而右子树[5-7]的节点及子节点都是{0, 1},根据merge原则,最后值为{0, 1},两者合并,得到小于等于6的已插入值的val为{1, 1}

public int findNumberOfLIS(int[] nums) {
    if (nums.length == 0) return 0;
    int min = nums[0], max = nums[0];
    for (int num: nums) {
        min = Math.min(min, num);
        max = Math.max(max, num);
    }
    Node root = new Node(min, max);
    for (int num: nums) {
        Value v = query(root, num-1);
        insert(root, num, new Value(v.length + 1, v.count));
    }
    return root.val.count;
}

获取最大最小值,确认root区间,构造root节点

遍历nums,获取每个num的前置序列(num-1)的val{length, count},,插入(其实操作上应该是“更新”)num对应的节点及其父辈节点的val

遍历结束后,root也相应更新,root.val即nums的最长递增子序列的长度和个数

最长公共子序列—Leetcode.1143

题目描述:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。

答:动态规划
参考本例text1 = “bsbininm”; text2 = “jmjkbkjkv”;
画出dp矩阵,很好理解了
dp[i][j]表示text1前i个元素和text2的前j个元素的最长公共子序列;

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int len1 = text1.length();
        int len2 = text2.length();
        vector<vector<int> > dp(len1+1, vector<int>(len2+1, 0));
        for(int i = 0; i < len1; ++i)
            for(int j = 0; j < len2; ++j)
                dp[i+1][j+1] = (text1[i] == text2[j]) ? dp[i][j]+1 : max(dp[i+1][j], dp[i][j+1]);
        return dp[len1][len2];
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值