自然是不包括所有题 = =
仅作 (2020.1.22) 至 (2020.1.29) 的做题记录。
注意,更新顺序是从上往下,即最新的在下面 /wq
大概都是 cf,有些没写题解的题也懒得写了,唉。
CF1473G [2800,*medium,组合数学+NTT]
首先这个绝对值的限制就保证了块数在一定限制内。
令 (f(l,r,x,y)) 表示当前从 (l) 个块中的第 (x) 个移动到 (r) 个块中的第 (y) 个,考虑这个 (f(l,r,x,y)) 怎么算。首先考虑 (l<r) 的情况,容易发现每一次每个块都有两种走法,也仅有两种走法。按理来说,前一列中的第 (i) 块可以走到下一列的第 (i) 块和第 (i+1) 块,那这样的话就有:
然后考虑 (l>r) 的情况,容易发现将路径反过来统计即可。
接着可以发现,按照上式计算,(r-l) 其实就是中间的块数(也就是 (a,b)),因此可以直接省掉。然后就可以拿一个 (dp) 了:令 (dp(i,j)) 表示做完了前 (i) 段,现在在第 (j) 块的方案数。
观察这个转移式,可以发现这就是个赤裸的卷积了吧。然后就没了,但是这样做有点卡不过去。
考虑把这个 (dp(i,j)) 列出来:
这里 (l) 枚举的是中转点的位置,换个法子枚举,枚举 (k) 向下的步数:
然后可以发现 ({bchoose k+l-j}={bchoose b-k-l+j}) ,这就是个范德蒙德卷积的形式了:
这后面的东西卷一下就没了。因为这样只需要考虑两侧的情况,这样子的话因为 (|a_i-b_i|leq 5),所以多项式长度就是 (O(n)) 而非 (O(m)) 的。所以复杂度是 (O(n^2log m)) 而不是 (O(nmlog m)),足以通过此题。
const int N=2e3+5;
const int M=2e5+5;
const int LogN=20;
int n,a[N],b[N],fac[M],inv[M];
inline void init(int L=M-5) {
fac[0]=1;
lep(i,1,L) fac[i]=mul(fac[i-1],i);
inv[L]=modpow(fac[L],-1);
rep(i,L,1) inv[i-1]=mul(inv[i],i);
}
inline int C(int n,int m) {
if(n<0||m<0||n<m) return 0;
return mul(fac[n],mul(inv[m],inv[n-m]));
}
poly dp,tmp;
int main() {
IN(n),init(),init_w();
lep(i,1,n) IN(a[i],b[i]);
int ans=0,now=1; dp.rez(2),dp[1]=1;
lep(i,1,n) {
int l=now,t=now+a[i],r=now+a[i]-b[i]; now=r;
tmp.clear(),tmp.rez(l+r+1);
lep(j,1,l+r) tmp[j]=C(a[i]+b[i],b[i]+j-l); dp=dp*tmp;
lep(j,1,r) dp[j]=dp[j+l]; dp.rez(r+1);
}
lep(i,1,now) pls(ans,dp[i]); printf("%d
",ans);
return 0;
}
CF1461F [2700,*hard,分内讨论+发掘性质+DP]
分内讨论:
-
一个符号:
显然那就只能全填是吧。
-
两个符号:
+-
:是个傻子都是直接填加号。*+
:额外情况。*-
:显然答案下限是 (0) 。对于从前往后的第一个 (0) ,显然前面都是填乘号,然后这个 (0) 前面是一个减号,后面的就都是乘号了。这样子的话后面的贡献就是 (0)。显然这是最优的情况,因为只有第一个数前面自带一个加号。
-
三个符号:
*+-
:显然减号完全没有用武之地,因此也属于额外情况。
-
额外情况(有
*
,+
):这个是本题的重点了。
首先,对于所有的 (0) ,显然这个 (0) 前后都是填
+
。这样,这么多 (0) 就把原序列分成了若干段。接下来只需要考虑同一段内的。显然这并不是直接乘起来这么简单,如果有 (1) 的话,乘起来不一定是最优的。那么这下这么多 (1) 又把一段分成了若干区间,显然这么多区间里面都是
*
,问题就在于,每个 (1) 的前后,到底是+
还是*
。然后可以发现,最差的情况莫过于,中间有一堆连续的 (1) ,然后两边是否需要连起来。很显然,如果两边乘在一起大于 (lim) ,而两边加在一起在加上中间的 (1) 的和小于等于 (lim) ,则必然是将它们连起来。因为 (1) 最多 (10^5) 个,考虑最劣情况,即有一边是 (2) ,那么如果另外一边的乘积大于等于 (10^5+3) ,这样乘在一起就是 (2 imes 10^5+6) 了,加在一起却是 (2 imes10^5+5),所以这个 (lim) 其实是 (10^5) 级别的。
那么考虑一个 (dp):(dp(i)) 表示前 (i) 个区间的最优值(显然这个 (dp(i)leq lim),因此可以直接记录),枚举上一次填加号的地方转移即可。
然后可以发现这个 (dp) 其实是 (O(n^2)) 的,但是如果整段区间的积已经大于 (lim) 了,那么一定是全部填
*
最优,因此即使要进行 (dp) ,剩下的乘积也是小于 (lim) 的,也就是说,这个 (dp) 其实是 (O(log^2 lim)) 的。
细节挺多,代码挺长,就只贴"额外情况"的代码好了。
const int N=1e5+5;
const int lim=2e5;
int n,a[N];
char str[N];
bool work;
int dp[N],pre[N],sgn[N],val[N],L[N],R[N];
inline void build(int cnt,int l,int r) {
L[cnt]=l,R[cnt]=r,val[cnt]=1,pre[cnt]=0,dp[cnt]=-1;
if(!work) return ;
lep(i,l,r) val[cnt]*=a[i]; int tmp=1;
rep(i,cnt,1) {
tmp*=val[i];
if(dp[cnt]<dp[i-1]+L[i]-R[i-1]-1+tmp) dp[cnt]=dp[i-1]+L[i]-R[i-1]-1+tmp,pre[cnt]=i;
}
}
inline void solve(int l,int r) {
if(l>r) return ;
int tmp=1,cnt=0; work=true;
lep(i,l,r) if(tmp*=a[i],tmp>lim) {work=false; break;}
std::vector <int> sta;
lep(i,l,r) if(a[i]==1) sta.pb(i); int T=sta.size()-1;
if(T==-1) {lep(j,l,r-1) printf("%d*",a[j]); printf("%d",a[r]); return ;}
R[0]=l-1;
if(l<sta[0]) build(++cnt,l,sta[0]-1);
for(int i=0;i<T;++i) if(sta[i]+1<sta[i+1]) build(++cnt,sta[i]+1,sta[i+1]-1);
if(r>sta[T]) build(++cnt,sta[T]+1,r);
if(!work) pre[1]=0,pre[cnt]=1;
lep(i,l,L[1]-1) sgn[i]=1;
lep(i,R[cnt],r) sgn[i]=1;
int now=cnt,las=pre[now];
while(now) {lep(i,R[las-1],L[las]-1) sgn[i]=1; now=las-1,las=pre[now];}
lep(i,l,r-1) printf("%d%c",a[i],sgn[i]==1?'+':'*'); printf("%d",a[r]);
}
inline void extra() {
std::vector <int> sta;
lep(i,1,n) if(a[i]==0) sta.pb(i); int T=sta.size()-1;
if(T==-1) return solve(1,n),puts(""),void();
solve(1,sta[0]-1);
for(int i=0;i<T;++i) {
if(sta[i]>1) printf("+"); printf("0"); if(sta[i]<n&&sta[i]<sta[i+1]-1) printf("+");
solve(sta[i]+1,sta[i+1]-1);
}
if(sta[T]>1) printf("+"); printf("0"); if(sta[T]<n) printf("+");
solve(sta[T]+1,n),puts("");
}
CF1458C [2700,*easy,发掘性质]
发掘一下性质,这个 IC
十分的神奇。
首先,令 ((i,j,x)) 表示 (i,j) 上的值是 (x) ,容易发现,对于 I
,其实就是将 ((i,j,x)) 变为 ((i,x,j)) ,同样对于 C
,其实就是将 ((i,j,x)) 变成 ((x,j,i)) 。
然后对于 UDLR
,其实就是将 (i) 变一下或者 (j) 变一下。
可以将 IC
看作换个位置,然后将 UDLR
看作修改值,这样子的话,因为每次操作都是全体操作,所以只需要维护位置即可,然后记录一下每个位置的修改值是多少即可。这个每次修改就是 (O(1)) 的,最后重组矩阵是 (O(n^2)) 的,足以通过此题。
const int N=1e3+5;
int n,m,mat[N][N],ans[N][N];
char str[N];
inline void solve() {
IN(n,m);
lep(i,0,n-1) lep(j,0,n-1) IN(mat[i][j]),--mat[i][j];
scanf("%s",str+1); int q=strlen(str+1);
int p1=1,p2=2,p3=3; int t[4]={0,0,0,0},o[4]={0,0,0,0}; lep(i,1,q) {
if(str[i]=='U') --t[p1]; if(str[i]=='D') ++t[p1];
if(str[i]=='L') --t[p2]; if(str[i]=='R') ++t[p2];
if(str[i]=='I') swap(p2,p3); if(str[i]=='C') swap(p1,p3);
}
t[1]%=n,t[2]%=n,t[3]%=n;
lep(i,0,n-1) lep(j,0,n-1) {
o[1]=(i+t[1]+n)%n,o[2]=(j+t[2]+n)%n,o[3]=(mat[i][j]+t[3]+n)%n;
ans[o[p1]][o[p2]]=o[p3];
}
lep(i,0,n-1) {lep(j,0,n-1) printf("%d ",ans[i][j]+1); puts("");}
puts("");
}
int main() {
int T=int(IN);
while(T--) solve();
return 0;
}
CF1455G [2900,*medium,建树+线段树合并优化 DP]
首先可以想到建树。
设 (dp(i,j)) 表示走完 (i) 的子树,出来的时候为 (j) 的最小代价。显然因为只有 if
节点有儿子,所以进入 (i) 子树的值是唯一确定的。
用线段树维护 (dp(i,j)) 。对于 (i) 的一个儿子 (u) ,如果 (u) 是 set
,那么就只需要支持单点修改和区间修改(还要维护区间 (min)),如果 (u) 是 if
,那么求出 (dp(u,j)) ,然后做区间加(进入 (u) 的代价)后线段树合并即可。
线段树合并的时间复杂度显然正确:一个 set
操作最多会使不同数字集合的大小加一。
然后这题就做完了。(建树好像有一堆细节要讨论,不想写了。
CF1455F [2800,*medium,字符串+发掘性质]
首先一个字符最多被操作 (1) 次,这意味着一个字符最多被移动到前面两格。
令 (str(i)) 表示前 (i) 个字符的最小字典序,转移的话依次考虑 (i) 的新位置为 (i,i-1,i-2) 的情况即可。
CF1468B [2900,*hard,单调性+数据结构]
发现这个结构很像一个堆栈。每一天丢进去几个,然后再取出几个栈顶。
假设第 (i) 天没有卖完,一直堆在栈中,到第 (j) 天才卖完。那么 ([i,j]) 中间的所有天的所有面包,都可以在该段天数区间内卖完。因为 (i) 对于 (k>i) ,(i) 在栈中的元素一定比 (k) 在栈中的元素距离栈顶远。
那么我们称这个区间 ([i,j]) 为一个"可以独立自主解决问题的区间"。容易发现,只需要找到这些区间,然后答案就是最长的区间的长度了。接下来的问题就是怎么找这些区间。
考虑 (k) 从 (10^9) 开始慢慢减少。最开始的时候显然区间都是 ([i,i]) 的形式。慢慢减少 (k) 的同时,会发生:
- 一个区间的面包卖不完了,将其与下一个区间合并。
现在来考虑这个合并的时间。首先需要注意几点:
- 如果拿一个空栈从一个区间的左端点做到右端点,除了到右端点,其他的时刻一定不存在空栈,否则区间可以被分成两半。这就意味着没有一次弹栈机会会被浪费。
- 当前一个区间有剩下的面包留到接下来一个区间时,因为剩下的面包是在栈底的,因此一定不会对这个区间的结构造成影响。
因为没有弹栈机会被浪费,所以一个区间可以接受的最小 (k) 其实就是 (frac{sum a_i}{len}) ,将其定义为区间的限制。
这下子用一个 set 维护就好了,每一次找到区间限制最大的区间,然后将其与下一个区间合并即可。
const int N=2e5+5;
ll a[N],sum[N];
int n,m,q[N],L[N],R[N];
struct Node {
int l,r; ll val;
bool operator < (const Node &ele) const {return val>ele.val||(val==ele.val&&r>ele.r);}
}; std::set <Node> S;
inline ll calc(int l,int r) {return (sum[r]-sum[l-1]+r-l)/(r-l+1); }
inline Node merge(Node a,Node b) {
R[a.l]=b.r,L[b.r]=a.l;
return (Node){a.l,b.r,calc(a.l,b.r)};
}
int ans[N],qwq[N],tim[N];
int main() {
IN(n,m);
lep(i,1,n) IN(a[i]); a[n]=-2e18;
lep(i,1,n) L[i]=R[i]=i,S.insert((Node){i,i,a[i]});
lep(i,1,n) sum[i]=sum[i-1]+a[i];
int c=0; tim[0]=inf;
while(S.size()>1) {
Node now=*S.begin(),nxt=(Node){now.r+1,R[now.r+1],calc(now.r+1,R[now.r+1])};
S.erase(now),S.erase(nxt),S.insert(merge(now,nxt));
if(now.val<tim[c]) ++c,qwq[c]=max(qwq[c-1],nxt.r-now.l),tim[c]=now.val;
else chkmax(qwq[c],nxt.r-now.l);
}
lep(i,1,c) --tim[i];
std::reverse(tim+1,tim+1+c),std::reverse(qwq+1,qwq+1+c);
lep(i,1,m) IN(q[i]),printf("%d ",qwq[lower_bound(tim+1,tim+1+c,q[i])-tim]);
return puts(""),0;
}
CF1450H [2900/3300,*hard]
考虑最优的方案一定是两个连在一起就消掉,然后最后剩下的一定是一个 0
、1
交错的序列,答案就为这个序列的长度除四,也就是 0
或 1
的个数除二。
怎么方便的求出这个东西?考虑原序列,一定是 0
段和 1
段交错的形式。现在考虑统计最终的 0
、1
交错序列的 0
的个数。首先,对于长度为偶数的 0
段,全部都做不出贡献。其次,对于长度为奇数的 0
段,如果要做出贡献,必须满足两个奇数段不能合并到一块去,如果要满足不合并到一块去就必须满足这两段之间有奇数个元素。
也就是说,如果两个奇数段中间有偶数个元素,那么无论如何它们都会合并到一块儿去。
现在考虑对下标按照奇偶分类,容易发现,对于偶数段,下标为奇数的和下标为偶数的个数一样,对于可以合并的两个奇数段,将奇数下标和偶数下标加起来后发现他们仍然相等。对于无法合并的两个奇数段,奇数下标的个数和和偶数下标的个数和总是相差 (2),而这两个奇数段又会同时做出贡献。
因此可以发现,如果令奇数下标的 0
有 (a) 个,偶数下标的 0
有 (b) 个,那么答案就是 (frac{|a-b|}{2}) 。
现在考虑有 (x) 个奇数下标的 ?
,有 (y) 个偶数下标的 ?
,假设从 (x) 里面找出来 (i) 个 0
,(y) 里面找出来 (j) 个 1
,然后答案就是 (frac{|a+i-b-j|}{2}),现在令人头疼的就是这个绝对值怎么去掉。
首先求一个东西:(i-j=k) 的方案数:
容易发现 (-yleq kleq x) ,枚举这个 (k) 的情况带入答案式即可,答案即为(令 (t=a-b)):
这个就是 (O(n)) 的了,足以通过 easy version 。考虑怎么修改,首先这个位置不用管吧,只需要看位置的奇偶性了。如果修改的是 0
,其实就只需要改 (t) 的值即可。如果修改的是 ?
,就修改 (x,y) 即可。
重新定义 (t=y+b-a) ,令 (p=x+y) 。
定义 (f(n,t)) 。
因为每次 (p,t) 的变化量只有 (1),所以这个 (f(p,t)) 显然可以 (O(1)) 更新。
边界细节巨 tm 多,吐了。
CF1326F [2600/3200,*hard]
这题我感觉挺牛逼的,记录一下。F1 没多想,直接开的 F2 。
此题关键:通过容斥可以将不同的状态数从 (2^{n-1}) 变为 (P(n)),实现状态数大幅度降低。
常见的容斥也多可以用来实现降低状态数。
以下为正文部分。
观察一下一个序列代表着什么。首先,如果两个 0
连在一起,则说明中间这个点是单独的一个;如果两个 0
中间有一个 1
,这说明中间这两个点有一条边。所以,连续的 (k) 个 1
就代表着一个长度为 (k+1) 的链。
考虑容斥,钦定位置集合 (S) 上都是 1
,其余位置随便的方案数为 (f(S))。这样的话将 (S) 的链集合求出来后,只需要满足链集合成立,而并不需要关系链集合之间是否成立。容易发现,位置集合 (S) 对印着原图的链大小集合 ({a_1,a_2,cdots,a_k}) ,显然有 (sum a_i=n) ,容易发现因为 (n=18),链大小集合的方案数实质上是 (n) 的划分数,因此它其实最多就是 (385) 。也就是说,最多有本质不同 (385) 个 (f(S)) ,这下就好做多了。
然后接下来考虑如何对这 (385) 个 (f(S)) 求解。
(2021.1.29) :暂时留坑。
Contest 1466 (Good bye 2020),[800~3400]
A [*easy]
暴力枚举另外两个端点即可。
B [*easy]
从大数到小数贪心即可。
C [*easy]
只要不出现形如 bab
和 bb
的串就行,(dp) 即可,记录前两个字符。
D [*easy]
从 (n-1) 往 (1) 做,容易发现每一次会消掉一个点的贡献,每次找权值最小的可以且还可以做贡献的点即可,这个可以用数据结构随便维护。
E [*easy]
在 (a_j) 处统计,就是要求全局与 (a_j) 的 (&) 和以及 (|) 和,这个只需要将位拆开统计。
F [*medium]
将每一个维度看成点,然后向量就是连边。
容易发现对于一条长度为 (n) 的链,其不同的状态数有 (2^{n-1}) 种,如果再连一条边变成环的话,状态数就有 (2^{n}) 种,但是不同的状态数仍然只有 (2^{n-1}) 。这说明环边都是没有用的。
如果从这里出发的话会发现第一个样例不好解释,因为是自环。不妨考虑建一个虚点,对于只有一个维度上有 (1) 的向量相当于将位置和虚连边。这样就解释的通了。
这个用并查集维护即可。对于 (|T|) 的话,直接用 (|S'|) 求即可。
G [*medium]
首先可以分治,将 (s_i) 的答案分成在 (s_{i-1}) 的部分和跨过 (t_{i-1}) 的部分。
(s_{i-1}) 的部分递归,然后跨过 (t_{i-1}) 的部分直接找询问串 (q) 中的相同字符,这个时候再维护一下 (s_{i-1}) 的前后缀哈希就可以算贡献了。由于 (|q|leq 10^6),因此维护前后缀哈希也就只需要维护 (10^6) 位。
然后容易发现,当 (|s_{i-1}|geq 10^6) 时,对于 (s_{i}) 的前后缀哈希,可以直接继承 (s_{i-1}) 的。这可以启发我们,也许根本不要在意是 (s_{i}) 还是 (s_{i-1}) ,反正后面的前后缀哈希都一样,需要在意的仅有中间的字符 (t) ,而不同的 (t) 只有 (26) 种,于是这题就比较可做了。
首先,处理出所有的 (|s_{i-1}|<{10^6}) 的 (s_i) 并做好前后缀哈希,因为每次字符串长度一定翻倍所以显然这样的 (i) 是 (log) 级别的,令 (lim) 是满足条件的最大 (i) 。
然后询问时的 (k) ,对于 (k>lim) ,接下来的串一定是有 (2) 的若干次方的 (s_{lim}) 出现,然后开个桶记录一下这中间的 (t) 的个数,枚举 (t) 分别统计即可。
对于 (q) 在 (s_{lim}) 中的出现次数,因为维护了前后缀哈希,所以可以很方便的求出了。(当然也可以用别的方法 ...
Contest 1375 (Codeforces Global Round 9),[1100~3500]
E [*medium]
考虑逐位还原,对于当前的第 (i) 位,找到与其形成逆序对的位置,然后显然最后要把最小的那个换到第 (i) 位,并且不破坏后面的大小关系。
显然先换大数再换小数是最优策略。对于值相同的数也需要分出个大小关系,这个最好在外面就处理好,因为在里面排序的时候再判大小关系可能会出错。
F [*hard]
发现先手必胜的状态就是,将三堆石子从小到大排序后,大小是形如 (a,a+k,a+2k) 的,并且上一次被操作过的是第三堆。假设做到了这么一个局面,但是上一次操作的并不是最大的那个,就可以给一个 (3k) ,那无论怎么操作都避免不了这种必败局面了。
事实上,我们有一个更好的方法,对于 (a,b,c) ,只需要给一个 (2c-a-b) ,这个时候只能加在 (c) 上面,否则就会直接变成必败局面。显然,加在 (c) 上后,下一次操作就不能加在 (c) 上了,这个时候就是一个妥妥的必败局面了。
G [*medium]
这题其实挺普通的。
首先很显然不会有无用操作,也就是说确立一个点为最终的菊花中心后,所有的操作一定是关于这个点的。这个时候,假设已经确定了菊花中心,将其立为根,通过画图可以很容易发现:
- 儿子节点到该节点的边:这种边不用消掉。
- 孙子节点到儿子节点的边:以儿子节点为 (b),这种边是需要消掉的,消掉后原孙子变成新儿子。
- 曾孙节点到孙子节点的边:操作孙子节点的时候是会将这些点全部连到根的,因此这种边不用消掉。
- ......
如此看来,一个边需不需要消掉,跟其到根的距离有关系。
换根 (dp) 即可。
H [*hard]
瞅了眼题解,这个值域分块还挺妙的。
考虑值域分块,每一块的大小为 (T) 。对于每个值域块,处理出 (frac{T(T-1)}{2}) 个区间的集合。接着询问的时候,将整块逐次合并即可,容易发现这里的合并次数是 (frac{n}{T}),因此总共的区间数就是:
打表跑一跑发现 (T=362) 最优秀,这个看起来是 (sqrt{2q}) 的样子。
接下来考虑上面那个考虑 "(frac{T(T-1)}{2}) 个区间的集合" 怎么做。考虑按照值域分治,然后合并即可。计算这个需要的次数,容易发现:
容易发现后面那个东西一定小于 (2n),因此 (S(n)approx n^2) 。
然后又跑一下,发现最小值是 (T=256),这个就是 (sqrt{q}) 了。
Contest 1394 (Codeforces Round #664 (Div. 1)),[1800~3500]
C [*medium]
不难发现只需要使得 (s) 与 (t) 中的 B
、N
出现次数分别一样即可。
可以想到将 (s) 化成一个点(坐标用 B
、N
的出现次数来表示),画图将这些操作表示出来,不难发现形成的是一个凸的图形。考虑二分走的步数,用代数式表示这个图形,这样就变成了对于每一个 (s) ,图形是否有交。判断一下即可。
D [*medium]
首先如果一条边的两个端点优先级不相等,那么这条边就已经有方向了。令点 (u) 的入边有 (a) 条,出边有 (b) 条,那么其贡献为 (a_u imes max(a,b)) 。
对于没有方向的边,考虑 (dp) 。令 (dp_{u,0/1}) 表示对于 (u) 来说,父亲到其的边是入边/出边时,子树内的最小代价。转移的时候对于不能确定方向的边所连儿子全部都拿出来,然后钦定这些儿子全部选的入边,最后再逐次换成出边计算贡献即可,要求最优方案就将儿子按照 (dp) 状态的差排序即可。
Contest 1439 (Codeforces Round #684 (Div. 1)),[1500~3500]
B [*medium]
先将无用的度数小于 (k-1) 的点全部删去,这些点必然是没有贡献的。
然后就是删度数等于 (k-1) 的点,删的时候判断一下能不能团,显然团的边数是 (frac{k(k-1)}{2}),因此 (kleq sqrt{m}) 时才有这个必要,显然又因为这样的点不超过 (frac{m}{k}) 个,所以复杂度其实是 (O(msqrt{m}log m)),(log) 用来查边,实现优秀的话可以通过。
最后的话就是剩下的情况了。
C [*medium]
首先 (1) 操作不改变原序列的单调性。
接下来考虑 (2) 操作,容易发现一定是一段一段吃的,而且段数不超过 (log) 。因为后一项不比前一项大,所以对于每一段,怎么吃都会将 (y) 至少吃掉一半,那么找段的话直接在线段树上二分即可。