两侧循环进行状态转移
// 普通解法的动态规划
// 时间复杂度O(n^2),数组稍大就会超时
public static int lengthOfLIS1(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int ans = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
ends记录遍历过的各子序列长度的最小结尾元素,例如ends[i]表示i+1长度的子序列的最小结尾元素。注:ends数组最后中的数不是最长的最长递增子序列。
ends数组装着对于后续将遍历的元素中最可能形成以其为结尾的最长公共子序列的可能性,所以要不停的更新,因为越小的元素越有利于后续形成最长公共子序列
// 最优解
// 时间复杂度O(n * logn)
public static int lengthOfLIS2(int[] nums) {
int n = nums.length;
int[] ends = new int[n];
// len表示ends数组目前的有效区长度
// ends[0...len-1]是有效区,有效区内的数字一定严格升序
int len = 0;
for (int i = 0, find; i < n; i++) {
find = bs1(ends, len, nums[i]);
if (find == -1) {
ends[len++] = nums[i];
} else {
ends[find] = nums[i];
}
}
return len;
}
// "最长递增子序列"使用如下二分搜索 :
// ends[0...len-1]是严格升序的,找到>=num的最左位置
// 如果不存在返回-1
public static int bs1(int[] ends, int len, int num) {
int l = 0, r = len - 1, m, ans = -1;
while (l <= r) {
m = (l + r) / 2;
if (ends[m] >= num) {
ans = m;
r = m - 1;
} else {
l = m + 1;
}
}
return ans;
}
class Solution {
public:
int f(vector<int>&ends,int len,int num){
int l=0,r=len-1;
int ans=-1;
while(l<=r){
int mid=l+((r-l)>>1);
if(ends[mid]>=num){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
return ans;
}
int maxEnvelopes(vector<vector<int>>& envelopes) {
int n=envelopes.size();
vector<int>ends(n,0);
sort(envelopes.begin(),envelopes.end(),[](vector<int>a,vector<int>b){return a[0]<b[0]||(a[0]==b[0]&&a[1]>b[1]);});
int len=0;
for(int i=0,find;i<n;i++){
int cur=envelopes[i][1];
find=f(ends,len,cur);
if(find==-1)
ends[len++]=cur;
else ends[find]=cur;
}
return len;
}
};
首先将信封按长从小到大排序,长相等的信封按宽的大小从大到小排序,然后求宽排列后的数组的最长子序列
public class Solution {
public static int MAXN = 100001;
public static int[] nums = new int[MAXN];
public static int[] ends = new int[MAXN];
public static int kIncreasing(int[] arr, int k) {
int n = arr.length;
int ans = 0;
for (int i = 0, size; i < k; i++) {
size = 0;
// 把每一组的数字放入容器
for (int j = i; j < n; j += k) {
nums[size++] = arr[j];
}
// 当前组长度 - 当前组最长不下降子序列长度 = 当前组至少需要修改的数字个数
ans += size - lengthOfNoDecreasing(size);
}
return ans;
}
// nums[0...size-1]中的最长不下降子序列长度
public static int lengthOfNoDecreasing(int size) {
int len = 0;
for (int i = 0, find; i < size; i++) {
find = bs(len, nums[i]);
if (find == -1) {
ends[len++] = nums[i];
} else {
ends[find] = nums[i];
}
}
return len;
}
public static int bs(int len, int num) {
int l = 0, r = len - 1, m, ans = -1;
while (l <= r) {
m = (l + r) / 2;
if (num < ends[m]) {
ans = m;
r = m - 1;
} else {
l = m + 1;
}
}
return ans;
}
}
枚举每k间距的数组,求出其最长的不降序的子序列长度,数组长度-最长长度累加求和就是最少的操作次数
ends数组的加入和查询的数据可以是分离的,下面两题就用到了此技巧
public class Solution {
public static int findLongestChain(int[][] pairs) {
int n = pairs.length;
// 数对根据开始位置排序,从小到大
// 结束位置无所谓!
Arrays.sort(pairs, (a, b) -> a[0] - b[0]);
// 结尾的数值
int[] ends = new int[n];
int len = 0;
for (int[] pair : pairs) {
int find = bs(ends, len, pair[0]);
if (find == -1) {
ends[len++] = pair[1];
} else {
ends[find] = Math.min(ends[find], pair[1]);
}
}
return len;
}
// >= num最左位置
public static int bs(int[] ends, int len, int num) {
int l = 0, r = len - 1, m, ans = -1;
while (l <= r) {
m = (l + r) / 2;
if (ends[m] >= num) {
ans = m;
r = m - 1;
} else {
l = m + 1;
}
}
return ans;
}
// 最优解利用贪心
public static int findLongestChain2(int[][] pairs) {
int pre = Integer.MIN_VALUE, ans = 0;
Arrays.sort(pairs, (a, b) -> a[1] - b[1]);
for (int[] pair : pairs) {
if (pre < pair[0]) {
pre = pair[1];
ans++;
}
}
return ans;
}
}
ends数组加入数对的最后一个元素,查询的时候用数对的第一个元素查询
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Main {
public static int MAXN = 100001;
public static int[] arr = new int[MAXN];
public static int[] right = new int[MAXN];
public static int[] ends = new int[MAXN];
public static int n, k;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
n = (int) in.nval;
in.nextToken();
k = (int) (in.nval);
for (int i = 0; i < n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
if (k >= n) {
out.println(n);
} else {
out.println(compute());
}
}
out.flush();
out.close();
br.close();
}
public static int compute() {
right();
int len = 0;
int ans = 0;
for (int i = 0, j = k, find, left; j < n; i++, j++) {
find = bs2(len, arr[j]);
left = find == -1 ? len : find;
ans = Math.max(ans, left + k + right[j]);
find = bs2(len, arr[i]);
if (find == -1) {
ends[len++] = arr[i];
} else {
ends[find] = arr[i];
}
}
ans = Math.max(ans, len + k);
return ans;
}
// 生成辅助数组right
// right[j] :
// 一定以arr[j]做开头的情况下,arr[j...]上最长不下降子序列长度是多少
// 关键逻辑 :
// 一定以arr[i]做开头的情况下,arr[i...]上最长不下降子序列
// 就是!从n-1出发来看(从右往左遍历),以arr[i]做结尾的情况下的最长不上升子序列
public static void right() {
int len = 0;
for (int i = n - 1, find; i >= 0; i--) {
find = bs1(len, arr[i]);
if (find == -1) {
ends[len++] = arr[i];
right[i] = len;
} else {
ends[find] = arr[i];
right[i] = find + 1;
}
}
}
// 求最长不上升子序列长度的二分
// ends[0...len-1]是降序的,找到<num的最左位置
// 不存在返回-1
public static int bs1(int len, int num) {
int l = 0, r = len - 1, m, ans = -1;
while (l <= r) {
m = (l + r) / 2;
if (ends[m] < num) {
ans = m;
r = m - 1;
} else {
l = m + 1;
}
}
return ans;
}
// 求最长不下降子序列长度的二分
// ends[0...len-1]是升序的,找到>num的最左位置
// 不存在返回-1
public static int bs2(int len, int num) {
int l = 0, r = len - 1, m, ans = -1;
while (l <= r) {
m = (l + r) / 2;
if (ends[m] > num) {
ans = m;
r = m - 1;
} else {
l = m + 1;
}
}
return ans;
}
}