一、题目描述:
给你一个长度为 $n$ 的序列 $a_1\sim a_n$,$0 \le a_i \le 1\times 10^9$。
现在有 $m$ 次操作,第 $i$ 次操作将位置 $p_i$ 的数变为 $v_i$,$1\le v_i\le 1\times 10^9$。
操作仅对本次有效,并不会真的改变序列。求出每次操作后的最长严格上升子序列。
数据范围:$1\le n,m\le 4\times 10^5$;
二、解题思路:
其实这个题就是想让你掌握树状数组求 $LIS$ 的方法。
也许你会说:这不是小学生都会!是的,我就不会。
实际上真的很简单,考虑到转移方程 $f_i=max_{j=1}^{i-1}f_j+1[a_j<a_i]$。
很明显的线段树区间修改不就行了吗。
大概是我学 $LIS$ 的时候还太年轻,什么都想不到。
现在看来很显然,因为区间是前后缀所以可以用树状数组代替线段树。
然后就容易求出以 $a_i$ 结尾的 $LIS$ 长度 $f_i$,以 $a_i$ 开头的 $LIS$ 长度 $g_i$。
求 $g_i$ 的时候将 $a_i$ 视为 $tot-a_i+1$ 倒着做就行了,$tot$ 为总的不同元素数量。
不难发现一次修改后新的的 $LIS$ 长度范围在原来的 $[LIS - 1,LIS+1]$。
如果 $+1$ 当前位置 $i$ 一定满足 $f_i+g_i-1=LIS+1$。
这里的 $LIS$ 指原来的 $LIS$,$f_i,g_i$ 为修改后的 $f_i,g_i$。
离线下来全部跑一边就可以求出新的 $f_i,g_i$。
如果 $-1$ ,则新的 $f_i+g_i-1=LIS-1$,且位置 $i$ 为原来 $LIS$ 必经的点。
怎么求某一个点是不是 $LIS$ 必经的点呢?这大概是这个题唯一的难点了。
不过我当然是想不到的,直接上经典结论:
$记以第\ i\ 个数结尾的\ LIS\ 为\ f_i,记以第\ i\ 个数开头的\ LIS\ 为\ g_i$
$如果\ f_i+g_i-1=ans,则\ i\ 在某些\ LIS\ 中,且\ i\ 一定在\ LIS\ 的第\ f_i\ 位。(考虑反证,很简单)$
$于是我们得到判断位置\ i\ 为\ LIS\ 的必经点的充要条件:$
$f_i+g_i-1=ans\ 且不存在一个可以在\ LIS\ 中的\ j\not =i\ 满足\ f_j=f_i。$
现在你按照上述结论跑就可以了,结论很好,也很简单,时间复杂度 $O(nlog_2^n)$。
三、完整代码:
1 #include<iostream> 2 #include<algorithm> 3 #define N 800010 4 #define rep(i,l,r) for(int i=l;i<=r;i++) 5 #define per(i,r,l) for(int i=r;i>=l;i--) 6 using namespace std; 7 int n,m,num,lis,a[N],f[N],g[N],b[N],c[N],t[N],k[N],ans[N]; 8 int qry(int u){ 9 int res=0; while(u)res=max(res,c[u]),u-=u&-u; return res; 10 } 11 void upt(int u,int val){ 12 while(u<=num) c[u]=max(c[u],val),u+=u&-u; 13 } 14 struct Query{ 15 int pos,val,f,g,id; 16 bool operator < (const Query &t) const { 17 return pos<t.pos; 18 } 19 }q[N]; 20 signed main(){ 21 ios::sync_with_stdio(false); 22 cin.tie(0); cout.tie(0); 23 cin>>n>>m; rep(i,1,n) cin>>a[i],b[++num]=a[i]; 24 rep(i,1,m) cin>>q[i].pos>>q[i].val,q[i].id=i; 25 rep(i,1,m) b[++num]=q[i].val; 26 sort(b+1,b+1+num); num=unique(b+1,b+1+num)-b-1; 27 rep(i,1,m) q[i].val=lower_bound(b+1,b+1+num,q[i].val)-b; 28 rep(i,1,n) a[i]=lower_bound(b+1,b+1+num,a[i])-b; 29 rep(i,1,n) f[i]=qry(a[i])+1,upt(a[i]+1,f[i]); 30 rep(i,1,num) c[i]=0; 31 per(i,n,1) g[i]=qry(num+1-a[i])+1,upt(num+2-a[i],g[i]); 32 rep(i,1,n) lis=max(lis,f[i]+g[i]-1); 33 rep(i,1,n) if(f[i]+g[i]-1==lis) t[f[i]]++; 34 rep(i,1,n) if(f[i]+g[i]-1==lis&&t[f[i]]==1) k[i]=1; 35 sort(q+1,q+1+m); rep(i,1,num) c[i]=0; int now=1; 36 rep(i,1,n){ 37 while(now<=m&&q[now].pos==i) 38 q[now].f=qry(q[now].val)+1,now++; 39 upt(a[i]+1,qry(a[i])+1); 40 } 41 rep(i,1,num) c[i]=0; now=m; 42 per(i,n,1){ 43 while(now>=1&&q[now].pos==i) 44 q[now].g=qry(num-q[now].val+1)+1,now--; 45 upt(num+2-a[i],qry(num+1-a[i])+1); 46 } 47 rep(i,1,m){ 48 int res=q[i].f+q[i].g-1; 49 if(res>=lis) ans[q[i].id]=res; 50 else ans[q[i].id]=lis-k[q[i].pos]; 51 } 52 rep(i,1,m) cout<<ans[i]<<'\n'; 53 return 0; 54 }
四、写题心得:
对 $LIS$ 又有了新的理解,收获如下:
$1、掌握了一个点为序列\ LIS\ 必经点的充要条件,很厉害!=> Exp++!$
$2、掌握了树状数组,线段树求\ LIS\ 的方法,现在可以输出一组合法的\ LIS\ 了!=> Exp++!$
$NOIP\ RP++!$