数组相关问题学习指南
前置芝士
最长上升子序列 (LIS)
平衡子序列的最大和
[problem description]
给你一个下标从 0 开始的整数数组 nums
。
nums
一个长度为 k
的 子序列 指的是选出 k
个 下标 \(i_0、i_1、 ... 、i_{k-1}\) ,如果这个子序列满足以下条件,我们说它是 平衡的 :
- 对于范围
[1, k - 1]
内的所有j
,\(nums[i_j] - nums[i_{j-1}] >= i_j - i_{j-1}\) 都成立。
nums
长度为 1
的 子序列 是平衡的。
请你返回一个整数,表示 nums
平衡 子序列里面的 最大元素和 。
一个数组的 子序列 指的是从原数组中删除一些元素( 也可能一个元素也不删除 )后,剩余元素保持相对顺序得到的 非空 新数组。
[input]
输入:nums = [3,3,5,6]
输出:14
解释:这个例子中,选择子序列 [3,5,6] ,下标为 0 ,2 和 3 的元素被选中。
nums[2] - nums[0] >= 2 - 0 。
nums[3] - nums[2] >= 3 - 2 。
所以,这是一个平衡子序列,且它的和是所有平衡子序列里最大的。
包含下标 1 ,2 和 3 的子序列也是一个平衡的子序列。
最大平衡子序列和为 14 。
[datas]
1 <= nums.length <= 10^5
-10^9<= nums[i] <= 10^9
[solved]
定义 b[i]=nums[i]−i,问题变成从 b 中选出一个非降子序列,求对应的 nums的元素和的最大值。
这可以用 权值树状数组 (或者权值线段树)来优化。树状数组用来维护前缀最大值,设下标为x=b[i],维护的值为max(f[x],f[x-1],f[x-2],...)。
# 树状数组模板(维护前缀最大值)
class BIT:
def __init__(self,n):
# 空间需要多开大一些
self.tr=[-inf]*n
def change(self,i:int,val:int)->None:
while i<len(self.tr):
self.tr[i]=max(self.tr[i],val)
i+=i&-i
def query(self,i:int)->int:
mx=-inf
while i>0:
mx=max(mx,self.tr[i])
i-=i&-i
return mx
class Solution:
def maxBalancedSubsequenceSum(self, nums: List[int]) -> int:
n=len(nums)
b=sorted(set(x-i for i,x in enumerate(nums)))
t=BIT(len(b)+1)
res=-inf
for i,x in enumerate(nums):
j=bisect_left(b,x-i)+1 # nums[i]-i 离散化后的值(从 1 开始)
f=max(t.query(j),0)+x
res=max(res,f)
t.change(j,f)
return res
求解两个有序数组的第 K 小乘积
先统计分负数乘积个数neg、正数乘积个数pos以及乘积为0的个数 zero,
然后分三种情况讨论:
k≤negk,我们可以二分负数答案,统计不超过二分值的乘积个数;
neg<k≤neg+zero,此时返回0;
k>neg+zero,我们可以二分正数答案,统计不超过二分值的乘积个数。
字段和问题
不限制长度——在一个数列里找两个不相交区间使得他们权值和最大
找 m个长度为 k 的不相交区间使得他们的权值和最大 (1≤n≤5000)
f[i][j]=max(f[i−k][j−1]+sum[i]−sum[i−k],f[i−1][j])
区间数目变多且不限制长度——找 m 个不相交长度不限的区间使得他们权值和最大(1≤n≤5000)
选2个不重叠的长度为k的区间
使子数组和最大
因为区间大小是固定为k的,所以显然需要前缀和处理一下处理之后我们去维护前缀中长度为k的最大值ma,枚举第二个长度为k的起点,那么答案就是max(ma+当前长度为k的序列和)复杂度为O(n).
int T,n,k;
ll a[2000100];
void solve(){
cin>>T;
while(T--){
cin>>n>>k;
for(int i=1;i<=n;i++) {cin>>a[i];a[i]+=a[i-1];}
ll ma=-1e18,ans=-1e18;
for(int i=k;i+k<=n;i++){
ma=max(ma,a[i]-a[i-k]);
ans=max(ans,ma+a[i+k]-a[i]);
}
cout<<ans<<endl;
}
使子数组元素和相等(循环数组)
给你一个下标从 0 开始的整数数组 arr 和一个整数 k ,数组 arr 是一个循环数组,数组中的最后一个元素的下一个元素是数组中的第一个元素,数组中第一个元素的前一个元素是数组中的最后一个元素。
选中 arr 中任意一个元素,并使其值加上 1 或减去 1。
执行运算使每个长度为 k 的 子数组 的元素总和都相等,返回所需要的最少运算次数。
1 <= k <= arr.length <= 105
1 <= arr[i] <= 10^9
【解题思路分析】
1)解决 a不是循环数组的情况
考虑从 i 和 i+1开始的两个长为 k的子数组的和,如果要求这两个和相等,则有
\(a[i]+a[i+1]+⋯+a[i+k−1]=a[i+1]+a[i+2]+⋯+a[i+k]\)
及\(a[i]=a[k+i]=a[2k+i]....\)
2)分组处理
按照 i mod k的结果将 a 分组,对每一组(记作 b)
我们需要解决:
让数组 b的所有元素相等的最少运算次数。
根据中位数贪心,将 b的所有元素变为 b 的中位数是最优的。
3)翡翠定理
比如 n=6,k=4,那么 a[2]循环后是 a[8],和 a[0]在同一组,而 a[1]无论怎么循环都无法和 a[0] 在同一组。((1+6n) mod 4≠0
一个循环数组如果既有周期 n,又有周期 k,则必然有周期 gcd(n,k)。证明:根据 裴蜀定理,有:
a[i]=a[i+nx+ky]=a[i+gcd(n,k)]
这样就转换成了不是循环数组的情况。
【java】
public long makeSubKSumEqual(int[] arr, int k) {
int n=arr.length;
k=gcd(n,k);
System.out.println(k);
long res=0;
for(int i=0;i<k;i++){
ArrayList<Integer> list=new ArrayList<>();
for(int j=i;j<n;j+=k){
list.add(arr[j]);
}
Collections.sort(list);
int mid=list.get((list.size())/2);
for(int x:list){
res+=Math.abs(x-mid);
}
}
return res;
}
int gcd(int a,int b){
while(b!=0){
int tmp=b;
b=a%b;
a=tmp;
}
return a;
}