数组子序列
一、最大整除子集
给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足:
-
answer[i] % answer[j] == 0
,或 -
answer[j] % answer[i] == 0
如果存在多个有效解子集,返回其中任何一个均可。
示例1:
输入:nums = [1,2,3]
输出:[1,2]
解释:[1,3] 也会被视为正确答案。
示例2:
输入:nums = [1,2,4,8]
输出:[1,2,4,8]
1.1 动态规划
思路: 根据整除关系具有传递性,如果 a%c==0 b%c==0 , 那么 a%c ==0 ,解释了问题转移的特点,因此可以用动态规划求解
- 状态定义: dp[i] 表示在输入数组 nums 升序排列的前提下,以 nums[i] 为最大整数的
整除子集
的大小(在这种定义下 nums[i] 必须被选择) - 状态转移方程: 枚举 j=0…i−1 的所有整数 nums[j],如果 nums[j] 能整除 nums[i],说明 nums[i] 可以扩充在以 nums[j] 为最大整数的整除子集里成为一个更大的整除子集。
- 如果 nums[j] % nums[i] == 0 : dp[i]=dp[j]+1
- 如果 nums[j] % nums[i] != 0 : dp[i]=dp[i]
- 即得方程为: dp[i] = Math.max(dp[i], dp[j] + 1);
- 初始化: 由于 nums[i] 必须被选择,因此对于任意 i=0…n−1,初始的时候 dp[i]=1,这里 n 是输入数组的长度。
- 输出:
- 倒序遍历数组 dp,直到找到 dp[i]=maxSize 为止,把此时对应的 nums[i] 加入结果集,此时 maxVal=nums[i];
- 然后将 maxSize 的值减 1,继续倒序遍历找到 dp[i]=maxSize,且 nums[i] 能整除 maxVal 的 i 为止,将此时的 nums[i] 加入结果集,maxVal 更新为此时的 num[i];
- 重复上述操作,直到 maxSize 的值变成 0,此时的结果集即为一个目标子集。
function largestDivisibleSubset(nums) {
nums.sort((a,b)=>a-b);
let dp=new Array(nums.length).fill(1);
let maxSize=dp[0];
let maxVal=nums[0];
for(let i=1;i<nums.length;i++){
for(let j=0;j<i;j++){
if(nums[i]%nums[j]==0){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
if(dp[i]>maxSize){
maxSize=dp[i];
maxVal=nums[i];
}
}
if(maxSize==1){
return [nums[0]]
}
let result=new Array();
for(let i=nums.length-1;i>=0&&maxSize>0;i--){
if(dp[i]==maxSize&&maxVal%nums[i]==0){
result.push(nums[i]);
maxVal=nums[i];
maxSize--;
}
}
return result;
};
性能分析:
- 时间复杂度:O(n^2),其中 n 为输入数组的长度。
- 空间复杂度:O(n),其中 n 为输入数组的长度。需要创建长度为 n 的数组 dp。
二、最长等差数列
给定一个整数数组 A,返回 A 中最长等差子序列的长度。
回想一下,A 的子序列是列表 A[i_1], A[i_2], ..., A[i_k] 其中 0 <= i_1 < i_2 < ... < i_k <= A.length - 1
。并且如果 B[i+1] - B[i]( 0 <= i < B.length - 1)
的值都相同,那么序列 B 是等差的。
示例1:
输入:[3,6,9,12]
输出:4
解释:
整个数组是公差为 3 的等差数列。
示例2:
输入:[9,4,7,2,10]
输出:3
解释:
最长的等差子序列是 [4,7,10]。
2.1 动态规划
思路:
-
状态定义:
dp[i][j]
为以A[i]
和A[j]
为最后两个元素的等差数列的长度- 二维坐标转化为一维坐标:
dp[i][j]=dp[i*len+j]
- 二维坐标转化为一维坐标:
-
状态转移方程:
-
A[i]、A[j] 之前得一个元素
target
为:A[i]-(A[j]-A[i]) = 2*A[i]-A[j]
-
可得状态方程:
dp[i][j]=dp[map.get(target)][i]+1
-
二维坐标转化为一维坐标:
dp[i*len+j]=dp[map.get(target)*len+i]+1
-
通过 map 记录 A[i] 元素 value-key
-
-
初始化状态: 因为等差数列最少元素数量是2,在
i<j
的时候初始化值为2 -
问题答案: dp 数组元素中的最大值
function longestArithSeqLength(nums) {
const len=nums.length;
const map=new Map();
const dp=new Array(len*len).fill(2);
let result=0;
for(let i=0;i<len;i++){
for(let j=i+1;j<len;j++){
let target=2*nums[i]-nums[j];
if(map.has(target)){
dp[i*len+j]=dp[map.get(target)*len+i]+1;
}
result=Math.max(result,dp[i*len+j]);
}
map.set(nums[i],i);
}
return result;
};
三、最长递增子序列
给你一个整数数组nums
,找到其中最长严格递增子序列的长度。
示例1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例2:
输入:nums = [0,1,0,3,2,3]
输出:4
3.1 贪心+二分查找
问题: 给你一个整数数组 nums
, 找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例2:
输入:nums = [0,1,0,3,2,3]
输出:4
思路: 贪心+二分查找
-
贪心思路: 如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小
-
初始化:
-
维护一个数组
d[i]
: 表示长度为i
的最长上升子序列的末尾元素的最小值,d[1]=nums[0]
-
用
len
记录目前最长上升子序列的长度: 起始时len
为1
-
-
算法流程: 从前往后遍历数组
nums
,在遍历到nums[i]
时:-
如果
nums[i]>d[len]
, 则直接加入到d
数组末尾,并更新len=len+1
-
否则,在
d
数组中二分查找(因为d
数组是单调递增的,可以通过二分查找),找到第一个比nums[i]
小的数d[k]
,并更新d[k+1]=nums[i]
-
实现:
function binarySearch(left, right, target, array) {
let l = left;
let r = right;
while (l < r) {
let middle = (l + r) >> 1;
if (array[middle] < target) {
l = middle + 1;
} else {
r = middle;
}
}
return l;
}
function lengthOfLIS(nums) {
let len = 1;
const dp = new Array(nums.length + 1);
for (let i = 0; i < nums.length; i++) {
const num = nums[i];
if (num > dp[len]) {
dp[++len] = num;
} else {
let pos = binarySearch(1, len, num, dp);
dp[pos] = num;
}
}
return len; // 返回递增序列长度
return dp.filter(i => i); // 返回递增序列
}
测试
const nums = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(lengthOfLIS(nums));
-
时间复杂度:
O(nlogn)
-
空间复杂度:
O(n)
四、最长递增子序列的个数
问题: 给定一个未排序的整数数组 nums
, 返回最长递增子序列的个数 。
示例:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
4.1 贪心 + 前缀和 + 二分查找
-
我们将数组
d
扩展成一个二维数组,其中d[i]
数组表示所有能成为长度为i
的最长上升子序列的末尾元素的值。具体地,我们将更新d[i]=nums[j]
这一操作替换成将nums[j]
置于d[i]
数组末尾。这样d[i]
中就保留了历史信息,且d[i]
中的元素是有序的(单调非增)。 -
类似地,我们也可以定义一个二维数组
cnt
,其中cnt[i][j]
记录了以d[i][j]
为结尾的最长上升子序列的个数。为了计算cnt[i][j]
,我们可以考察d[i−1]
和cnt[i−1]
,将所有满足d[i−1][k]<d[i][j]
的cnt[i−1][k]
累加到cnt[i][j]
,这样最终答案就是cnt[maxLen]
的所有元素之和。 -
在代码实现时,由于
d[i]d[i]d[i]
中的元素是有序的,我们可以二分得到最小的满足d[i−1][k]<d[i][j]
的下标k
。另一处优化是将cnt
改为其前缀和,并在开头填上0
,此时d[i][j]
对应的最长上升子序列的个数就是cnt[i−1][−1]−cnt[i−1][k]
,这里[−1]
表示数组的最后一个元素。
function binarySearch1(left,right,target,array){
let l = left;
let r = right;
while(l < r){
const middle = (l + r) >> 1;
const list = array[middle];
if(list[list.length - 1] < target){
l = middle + 1;
}else{
r = middle;
}
}
return l;
}
function binarySearch2(left,right,target,array){
let l = left;
let r = right;
while(l < r){
const middle = (l + r) >> 1;
if(array[middle] < target){
r = middle;
}else{
l = middle + 1;
}
}
return l;
}
function findNumberOfLIS(nums){
const d = [];
const cnt = [];
for(const num of nums){
const i = binarySearch1(0, d.length,num,d);
let c = 1;
if(i > 0){
const k = binarySearch2(0,d[i-1].length,num,d[i-1]);
c = cnt[i-1][cnt[i-1].length -1] - cnt[i-1][k];
}
if(i === d.length){
const dList = [];
dList.push(num);
d.push(dList);
const cntList = [];
cntList.push(0);
cntList.push(c);
cnt.push(cntList);
}else{
d[i].push(num);
const cntSize = cnt[i].length;
cnt[i].push(cnt[i][cntSize - 1]+c);
}
}
const size1 = cnt.length;
const size2 = cnt[size1 - 1].length;
return cnt[size1 - 1][size2 -1];
}
复杂度分析
-
时间复杂度:
O(nlogn)
-
空间复杂度:
O(n)