二分搜索优化DP(子序列问题)
P1020 [NOIP 1999 提高组] 导弹拦截 题解
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
一行,若干个整数,中间由空格隔开。
输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
壹,问题1的求解#
其实就是求解最长不上升子序列的长度。
暴力DP每次更新
code:
for(int i=1; i<=n; i++) {
for(int j=1; j<i; j++)
if(a[j]>=a[i])
dp[i]=max(dp[i],dp[j]+1);
maxi=max(maxi,dp[i]);
}
对于每个数字a[i]都一一遍历一下它前面的所有数字,如果找到比a[i]更大的数就尝试更新dp[i],用已经求出来的dp[j]在末尾加1,这样n方复杂度只能过很小的数据点。
尝试优化。我们优化一个算法的方向其实就是将无用的信息排除出去。
我们的算法存了那些信息呢?其实只有一个dp数组表示以数字a[i]为尾部的最长不下降序列长度。但是我们更新需要哪些信息呢?当前序列的末尾元素,也就是我们每次比较的数。那么我们可以想到一种结合了所有所需信息的DP数组定义方式————dp[i]表示长度为i+1的不上升序列的末尾的最小值。
为什么要这么设定呢?
二分优化LIS的核心:用二分查找“定位替换位置”
二分优化LIS的关键是维护一个 “长度→最小结尾”的映射数组dp,而二分查找的作用是 :快速找到新元素在dp中应该插入或替换的位置。以下用 具体例子+动画式步骤 拆解二分逻辑:
一、先明确dp数组的核心意义#
dp数组定义:dp[i] = 长度为i+1的上升子序列的最小结尾元素。
(例如:dp[0]是长度1的子序列的最小结尾,dp[1]是长度2的子序列的最小结尾……)
dp数组性质:严格单调递增(因为长度更长的子序列,结尾元素必须更大才能保证“上升”)。
二、二分查找的目标:找到“第一个能替换的位置”
场景:遍历原序列元素x,需要更新dp数组以维持其性质。
核心问题:
在dp中找到 第一个大于等于x的元素位置pos,然后:
若pos等于当前dp的长度(即x比所有元素都大):直接追加到dp末尾,LIS长度+1。
否则:用x替换dp[pos](因为x更小,未来可能允许更长的子序列)。
为什么用“第一个大于等于x”?
目标是让dp数组元素尽可能小:例如,长度3的子序列结尾越小,后续元素越容易接在后面形成更长序列。
三、二分查找的详细步骤(以“非严格递增LIS”为例)
例子:原序列[3, 1, 2, 4],逐步演示dp数组更新和二分过程。
步骤1:初始状态
dp为空,LIS长度len=0。
步骤2:处理x=3
dp为空,直接追加:dp = [3],len=1。
(此时dp[0]是长度1的子序列最小结尾)
步骤3:处理x=1
目标:在dp=[3]中找第一个≥1的位置。
二分查找过程:
left=0,right=len=1(注意:right初始为dp长度,即待插入的位置范围是[0, len))。
mid = (0+1)/2 = 0,比较dp[0]=3和x=1:
dp[mid] ≥ x(3≥1),说明目标位置在[left, mid],更新right=mid=0。
循环结束(left=0,right=0),pos=0。
更新dp:dp[0] = 1(替换后dp=[1],len仍为1,但结尾更小了)。
步骤4:处理x=2
目标:在dp=[1]中找第一个≥2的位置。
二分查找过程:
left=0,right=len=1。
mid=0,比较dp[0]=1和x=2:
dp[mid] < x(1<2),说明目标位置在[mid+1, right],更新left=mid+1=1。
循环结束(left=1,right=1),pos=1(等于len)。
更新dp:追加x=2,dp=[1,2],len=2(此时dp[1]是长度2的子序列最小结尾)。
步骤5:处理x=4
目标:在dp=[1,2]中找第一个≥4的位置。
二分查找过程:
left=0,right=len=2。
mid=1,比较dp[1]=2和x=4:
dp[mid] < x(2<4),更新left=mid+1=2。
循环结束(left=2,right=2),pos=2(等于len)。
更新dp:追加x=4,dp=[1,2,4],len=3(最终LIS长度为3)。
四、二分查找的代码实现(关键片段)
// 当前dp数组长度为len,处理新元素x
int left = 0, right = len;
while (left < right) {
int mid = left + (right - left) / 2;
if (dp[mid] >= x) { // 找第一个≥x的位置(非严格递增LIS)
right = mid; // 目标在左半区间(包含mid)
} else {
left = mid + 1; // 目标在右半区间(不包含mid)
}
}
// 循环结束后,left = right = pos(插入/替换位置)
if (left == len) {
dp.push_back(x); // x是新的最长子序列结尾
len++;
} else {
dp[left] = x; // 替换为更小的结尾,为后续元素留空间
}
五、为什么二分查找能生效?
dp数组的单调性:由于dp严格递增,因此可以用二分查找(只有有序数组才能二分)。
时间复杂度优化:遍历原序列O(n),每次二分查找O(log len),总复杂度O(n log n)(而暴力DP是O(n²))。
总结:二分的本质是“高效定位长度”
与其说“在dp中找x的位置”,不如说“通过x确定它能贡献的最长子序列长度”。
二分查找的结果pos直接对应 x能形成的最长子序列长度-1(因为dp[pos]是长度pos+1的结尾)。
记住:dp数组是“长度→最小结尾”的映射,二分是“用x反查长度”的工具。
最后丑陋代码
#include<bits/stdc++.h>
using namespace std;
int f[100005],g[100005],cnt=0,ct=0,l,r,mid;
void search1(int x){
l=0,r=cnt;
while(l<r){
mid=l+(r-l)/2;
if(f[mid]<x)r=mid;
else l=mid+1;
}
if(l>=cnt)f[cnt++]=x;
else f[l]=x;
}
void search2(int x){
l=0,r=ct;
while(l<r){
mid=l+(r-l)/2;
if(g[mid]>=x)r=mid;
else l=mid+1;
}
if(l==ct)g[++ct]=x;
else g[l]=x;
}
int main(){
int x;
while(cin>>x)search1(x),search2(x);
cout<<cnt<<'\n'<<ct;
return 0;
}
但是这种代码超级丑陋,给一个新代码
include<bits/stdc++.h>
using namespace std;
int f[100005],g[100005],cnt=0,ct=0,l,r,mid;
void search1(int x){
l=1,r=cnt;
while(l<=r){
mid=l+(r-l)/2;
if(f[mid]<x)r=mid-1;
else l=mid+1;
}
if(l>cnt)f[++cnt]=x;
else f[l]=x;
}
void search2(int x){
l=1,r=ct;
while(l<=r){
mid=l+(r-l)/2;
if(g[mid]>=x)r=mid-1;
else l=mid+1;
}
if(l>ct)g[++ct]=x;
else g[l]=x;
}
int main(){
int x;
while(cin>>x)search1(x),search2(x);
cout<<cnt<<'\n'<<ct;
return 0;
}
将dp数组的定义改了,不再是长度i+1了而是i。那么就需要做出一些小的改变,在二分的边界上。

浙公网安备 33010602011771号