导弹拦截做题报告
为了这道题我大概搞了6小时左右,现在终于AC了。
题意
这道题就是要求一个数列中的最长不上升子序列和最长上升子序列。
至于第二问为什么是求最长上升子序列是因为:每一套导弹拦截系统数列的性质都是保证单调下降的的,所以每一个严格上升的数就是一个导弹拦截系统数列的开头。
题解
$2019.2.2$
关于实现有好多种做法。但是搞了半天我目前只理解了线段树做法。
首先要先清楚暴力
for(int i=1; i<=n; i++) {//以求最长不上升子序列为例
for(int j=1; j<i; j++) {
if(a[j]>=a[i] && f[j]+1>f[i]) {
f[i]=f[j]+1;
}
}
}
这个做法在显然是$O(n^2)$的,在这题会TLE
有没有优化呢?
我们发现其实可以利用线段树维护区间极值来省略一个循环,即寻找最大的$f[j] (a[j]>=a[i])$。
实现:
- 用
当前导弹的高度
的值做线段树的结点编号,线段树维护当前导弹的高度
所对应的最大$f[i]$。 - 以求最长不上升子序列为例,通过结点编号是
当前导弹高度
的条件,我们可以把寻找最大的$f[j] (a[j]>=a[i])$这个任务转化为$querymax(a[i]~max(a[j])(1<=j<=n)) ;$。而每次求完之后自然要更新一下$f[i]$的值。
$2019.2.3$
今天下午打算用再重温一遍这道毒瘤题。然后一直TLE
,上面那个做法能过可能是我上次交的时候人品比较好吧。于是我打算用树状数组实现。然后我就完全照搬了线段树实现的思路。$XJB$打了一个貌似树状数组的东西。然后就$AC$了?
至于为什么不会TLE
。因为树状数组的更新和查询在常数上都是比线段树小的。而我昨天的时候还有个疑惑:你那既然这样还要线段树作甚?其实是因为线段树的应用范围比树状数组更广。比如说维护区间极值。线段树能做到,而树状数组不行。在这题树状数组充其量也就是维护了个前缀极值,即$[1,n]$的极值我当时还认为,哪像前缀和一样操作不久可以也变成区间极值了吗?很显然是不行的,因为区间极值不具有可减性。
这道题让我对树状数组和线段树的理解更深了。
线段树基本上是万能的。而树状数组只能维护一些跟前缀和同样性质的信息。
毕竟是自己打的代码,大体思路我还是清楚的。但是毕竟我打完后是蒙蔽的。这里就搬出一篇较好的题解。
$2019.2.5$
现在是大年初一$2:40$分。时隔一年,我终于掌握了这道题的二分做法(滑稽)。
我们以求最长上升子序列为例:
- 维护一个$d$数组,$d[i]$表示长度为$i$的最长上升子序列末尾的数的最小值。维护最小值是基于贪心的思想:显然一个最长上升子序列的末尾的数越小,其他的数接在该序列后面的可能性就越大。
- 枚举每一个数。
- 如果$x > d[len]$_(len为当前最长上升子序列的长度)_就让$x$接到当前最长上升子序列的下一位,并且将最长长度$+1$。
- 如果$x <= d[len] $(len为当前最长上升子序列的长度) 就利用二分查找,用$x$来替换掉$d$数组中第一个大于等于$x$的数。来增加其他数接到后面的可能性。
以上就是二分做法的思路。
但是鸡肋的是,令我搞了一个多小时的不是这道题的做法,而是二分查找的写法。因为我实在太菜了,连二分都不会写。于是翻看了十几篇博客,总结出了我自己用的二分模板。
我们借一部个不下降序列$a$举例:
-
求这个序列中第一个大于等于的数的位置
int l=0,r=n,ans=0,mid=0; while(l<=r) { mid=(l+r)>>1; if(a[mid] >= x) {//当找到了一个满足条件的答案 r=mid-1;//因为:1.这个序列是递增的 2.我们要找的是第一个 3.当前找到的这个答案不一定是第一个 (这里写成l=mid+1不行是因为此时a[mid]已经>=x了,而这个序列又是递增的,所以在mid之后的数一定都>=x,一定不是第一个) ans=mid;//此时也可能是第一个,要及时更新答案。 } else { l=mid+1;//当a[mid] <= x时,因为这个序列是递增的,所以就把范围改到[mid+1,r]。 } } printf("%d",ans);
反思
大体思路也就这样。但是我DEBUG了两三个小时。
首先是Re:0
#include <cstdio>
#define lson (o<<1)
#define rson (o<<1|1)
#define inf 0x7fffffff
#define max(a,b) (a)>(b)?(a):(b)
const int N = 50010;
struct Segment_Tree {
int maxv[N<<2],chav[N<<2];
void pushup(int o) {
maxv[o]=max(maxv[lson],maxv[rson]);
}
void bulid(int o,int l,int r) {
if(l == r) {
return ;
}
int mid=(l+r)>>1;
bulid(lson,l,mid);bulid(rson,mid+1,r);
pushup(o);
}
void change(int o,int l,int r,int q,int v) {
if(l == r) {
maxv[o]=v;
//非常明显这个地方没有return;
}
int mid=(l+r)>>1;
if(q <= mid) {
change(lson,l,mid,q,v);
} else {
change(rson,mid+1,r,q,v);
}
pushup(o);
}
int querymax(int o,int l,int r,int ql,int qr) {
if(ql<=l && r<=qr) {
return maxv[o];
}
int mid=(l+r)>>1;
int ans=0;
if(ql <= mid) {
ans=max(ans,querymax(lson,l,mid,ql,qr));
}
if(qr > mid) {
ans=max(ans,querymax(rson,mid+1,r,ql,qr));
}
return ans;
}
};
int ans=-inf,sum=-inf;
int s,n;
int a[100010];
int main(void) {
Segment_Tree T,Q;
while(scanf("%d",&a[++s])!=EOF) {
n=max(n,a[s]);
}
s--;
T.bulid(1,1,n);Q.bulid(1,1,n);
for(int i=1; i<=s; i++) {
int x=T.querymax(1,1,n,a[i],n)+1;
T.change(1,1,n,a[i],x);
ans=max(ans,x);
}
for(int i=1; i<=s; i++) {
int x=Q.querymax(1,1,n,1,a[i]-1)+1;
Q.change(1,1,n,a[i],x);
sum=max(sum,x);
}
printf("%d\n%d",ans,sum);
}
总而言之以后打线段树的时候一定要放慢速度,而且好像每一个查询和修改操作都是要return的,检查时切记这点
线段树
#include <cstdio>
#define max(a,b) (a)>(b)?(a):(b)
#define lson (o<<1)
#define rson (o<<1|1)
#define inf 0x7fffffff
const int N = 50000;
struct Segment_Tree {
int maxv[N<<2];
void pushup(int o) {
maxv[o]=max(maxv[lson],maxv[rson]);
}
void bulid(int o,int l,int r) {
if(l == r) {
return ;
}
int mid=(l+r)>>1;
bulid(lson,l,mid);bulid(rson,mid+1,r);
pushup(o);
}
void change(int o,int l,int r,int q,int v) {
if(l == r) {
maxv[o]=v;
return ;
}
int mid=(l+r)>>1;
if(q <= mid) {
change(lson,l,mid,q,v);
} else {
change(rson,mid+1,r,q,v);
}
pushup(o);
}
int querymax(int o,int l,int r,int ql,int qr) {
if(ql<=l && r<=qr) {
return maxv[o];
}
int ans=0,mid=(l+r)>>1;
if(ql <= mid) {
ans=querymax(lson,l,mid,ql,qr);
}
if(qr > mid) {
ans=max(querymax(rson,mid+1,r,ql,qr),ans);
}
return ans;
}
}T,Q;
int ans=-inf,sum=-inf;
int s,n;
int a[100010];
int main(void) {
while(scanf("%d",&a[++s])!=EOF) {
n=max(n,a[s]);
}
s--;
T.bulid(1,1,n);Q.bulid(1,1,n);
for(int i=1; i<=s; i++) {
int x=T.querymax(1,1,n,a[i],n)+1;
ans=max(ans,x);
T.change(1,1,n,a[i],x);
}
for(int i=1; i<=s; i++) {
int x=Q.querymax(1,1,n,1,a[i]-1)+1;
sum=max(sum,x);
Q.change(1,1,n,a[i],x);
}
printf("%d\n%d",ans,sum);
}
树状数组
// luogu-judger-enable-o2
#include <cstdio>
#include <cstring>
#define max(a,b) (a)>(b)?(a):(b)
const int N = 100000 + 10;
int p;
int up_ans,unup_ans;
int a[N];
int maxv[N];
int n;
void change1(int o,int v) {
while(o<=n) {
maxv[o]=max(maxv[o],v);
o+=o&(-o);
}
}
int querymax1(int o) {
int maxx=0;
while(o) {
maxx=max(maxx,maxv[o]);
o-=o&(-o);
}
return maxx;
}
void change2(int o,int v) {
while(o) {
maxv[o]=max(maxv[o],v);
o-=o&(-o);
}
}
int querymax2(int o) {
int maxx=0;
while(o<=n) {
maxx=max(maxx,maxv[o]);
o+=o&(-o);
}
return maxx;
}
void Input() {
while(scanf("%d",&a[++p]) != EOF) {
n=max(n,a[p]);
}
p--;
}
void Solve() {
for(int i=1; i<=p; i++) {
int x=querymax2(a[i])+1;
change2(a[i],x);
unup_ans=max(unup_ans,x);
}
memset(maxv,0,sizeof(maxv));
for(int i=1; i<=p; i++) {
int x=querymax1(a[i]-1)+1;
change1(a[i],x);
up_ans=max(up_ans,x);
}
printf("%d\n%d",unup_ans,up_ans);
}
int main(void) {
Input();
Solve();
return 0;
}
二分做法
#include <cstdio>
const int N = 100000+10;
int n;
int d1[N],d2[N];
int a[N];
int ans1=1,ans2=1;
int find1(int x) {
int l=1,r=ans1,mid,ans;
while(l <= r) {
mid=(l+r)>>1;
if(d1[mid] < x) {
r=mid-1;
ans=mid;
} else {
l=mid+1;
}
}
return ans;
}
int find2(int x) {
int l=1,r=ans2,mid,ans;
while(l <= r) {
mid=(l+r)>>1;
if(d2[mid] >= x) {
r=mid-1;
ans=mid;
} else {
l=mid+1;
}
}
return ans;
}
void Input() {
while(scanf("%d",&a[++n]) != EOF) {
}
n--;
}
void Solve() {
d1[ans1]=d2[ans2]=a[ans1];
for(int i=2; i<=n; i++) {
if(a[i] <= d1[ans1]) {
d1[++ans1]=a[i];
} else {
d1[find1(a[i])]=a[i];
}
}
for(int i=2; i<=n; i++) {
if(a[i] > d2[ans2]) {
d2[++ans2]=a[i];
} else {
d2[find2(a[i])]=a[i];
}
}
printf("%d\n%d",ans1,ans2);
}
int main(void) {
Input();
Solve();
return 0;
}