题意:
有两段长度为$n$的不严格单调上升序列$a$和$b$,有两种操作共$m$次。单点赋值为$x$,保证不破坏序列不严格单调上升的性质;询问$a$的$[l_1,r_1]$区间和$b$的$[l_2,r_2]$区间合并后的中间值,保证合并后的区间长度为奇数。
$1le n le 5 imes 10^5 , ; 1 le m le 10^6 , ; 0 le a_i , b_i , x le 10^9 , ; 1 le l_1 le r_1 le n , ; 1 le l_2 le r_2 le n$
分析一:
考虑暴力怎么做。
设立个区间指针,一个计数器,每次跳一个指针,计数器$++$,可以在$O(n)$内处理一个询问。
或者按照类似于归并排序的方法把两个区间写到新的数组里,再取中间位置。
但仔细思考一下,发现上述两种暴力在实现细节上是一模一样的,都是比较两个头部,向目标位置逼近;也就是说,“合并”和“中间值”并不是什么重要的东西,询问合并有序区间第$k$大的都是同一种做法。我们的问题在于取数。
怎么样优化这个取数?
一个很简单的贪心:设区间长度和为$L$,如果$a[r_1]<b[l_2]$并且$r_1-l_1+1lelfloor L/2 floor$,那么整个$[l_1,r_1]$都可以跳过。
特殊情况毕竟少见,思考一下能不能扩大这种方法的适用面。
很容易发现,如果$a[s]<b[t]$并且$s-l_1+1lelfloor L/2 floor$,那么整个$[l_1,s]$都可以跳过。
那么这个$s$和$t$怎么定?
让我们暂时转换一下方向,跳过了一段区间之后,会发生什么?
我们已经向目标位置(暂时未知)逼近了一部分,我们本来要在两个区间内取第$lfloor L / 2 floor + 1$小的数,现在舍弃了$s - l_1 + 1$个,就要在剩下的范围内取第$lfloor L / 2 floor - s + l_1$小的数,成功地缩小了问题规模。
这就是分治嘛!
那我们直接一半一半砍下去,分到两个区间上比较(注意不能越界),既不会出错(拼得区间长度不超过$k$),也会让问题规模下降得很快(两边都取了尽可能大的数)。
即,设当前要取的是区间第$k$小的数,就将$k$折半,放在两个区间的对应位置$s,t$上(注意不能越界),比较$a_s,b_t$。不妨设$a_s<b_t$,那么我们可以递归地在区间$[s+1,r_1]$和$[l_2,r_2]$上找第$k-(s-l_1+1)$小的数(因为答案一定不会出现在$[l_1,s]$中)。
这样我们的时间复杂度就是$O(m;log\,n)$的,可以通过。
但容易发现:这个时间复杂度实际上很紧凑,很容易跑满,对于题目所给数据规模,其实十分危险。
更新(2019.08.19 20:14):递归写法自带两倍常数,这个应该是重要原因(之一)。
因此,如果不加优化,交上去之后,纪中的“更慢OJ”会神秘兮兮地告诉你:“我看你脸色行事”。
实现一(70~100分):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #define IL inline using namespace std; const int N=5e5; int n,m,a[N+3],b[N+3]; IL int find(int *a,int l,int r,int k){ return (l+k-1)>r?r:(l+k-1); } int sol(int l1,int r1,int l2,int r2,int k){ if(l1>r1) return b[l2+k-1]; if(l2>r2) return a[l1+k-1]; if(k==1) return min(a[l1],b[l2]); int x=find(a,l1,r1,k>>1); int y=find(b,l2,r2,k>>1); if(a[x]<b[y]) return sol(x+1,r1,l2,r2,k-(x-l1+1)); return sol(l1,r1,y+1,r2,k-(y-l2+1)); } int main(){ freopen("median.in","r",stdin); freopen("median.out","w",stdout); scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) scanf("%d",&b[i]); for(int i=1;i<=m;i++){ int opt; scanf("%d",&opt); if(opt==1){ int x,y,z; scanf("%d%d%d",&x,&y,&z); if(x==0) a[y]=z; else b[y]=z; } else { int l1,r1,l2,r2; scanf("%d%d%d%d",&l1,&r1,&l2,&r2); printf("%d ",sol(l1,r1,l2,r2,(r1-l1+r2-l2+3)>>1)); } } return 0; }
分析二:
好像结构上并不能做什么优化。
考虑优化常数。
快读、快写、$register;intquad$搞上。
成功$[TLE]\,mslongrightarrow 637ms$,跻身最优解$Rank13$。(2019.08.17 19:56)
实现二(100分):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #define IL inline #define RI register int using namespace std; const int N=5e5; IL bool num(char ch){ return '0'<=ch&&ch<='9'; } IL void read(int &x){ char ch; while(!num(ch=getchar())); x=ch-'0'; while(num(ch=getchar())) x=(x<<3)+(x<<1)+ch-'0'; } IL void write(int x){ int s[13],k=0; while(x>0){ s[++k]=x%10; x/=10; } do putchar(s[k]+'0'); while(--k); } IL void read(int &p1,int &p2){ read(p1); read(p2); } IL void read(int &p1,int &p2,int &p3){ read(p1); read(p2); read(p3); } IL void read(int &p1,int &p2,int &p3,int &p4){ read(p1); read(p2); read(p3); read(p4); } IL void writeln(int x){ write(x); putchar(' '); } int n,m,a[N+3],b[N+3]; IL int find(int *a,int l,int r,int k){ return (l+k-1)>r?r:(l+k-1); } int sol(int l1,int r1,int l2,int r2,int k){ if(l1>r1) return b[l2+k-1]; if(l2>r2) return a[l1+k-1]; if(k==1) return min(a[l1],b[l2]); int x=find(a,l1,r1,k>>1); int y=find(b,l2,r2,k>>1); if(a[x]<b[y]) return sol(x+1,r1,l2,r2,k-(x-l1+1)); return sol(l1,r1,y+1,r2,k-(y-l2+1)); } int main(){ freopen("median.in","r",stdin); freopen("median.out","w",stdout); read(n,m); for(RI i=1;i<=n;i++) read(a[i]); for(RI i=1;i<=n;i++) read(b[i]); int opt,p1,p2,p3,p4; for(RI i=1;i<=m;i++){ int opt; read(opt); if(opt==1){ read(p1,p2,p3); if(p1==0) a[p2]=p3; else b[p2]=p3; } else { read(p1,p2,p3,p4); writeln(sol(p1,p2,p3,p4,(p2-p1+p4-p3+3)>>1)); } } return 0; }
小结:
模拟解法,分治有奇效。
时限很紧,别忘记卡常。