整体二分 引入 在信息学竞赛中,有一部分题目可以使用二分的办法来解决。但是当这种题目有多次询问且我们每次查询都直接二分可能导致 TLE 时,就会用到整体二分。整体二分的主体思路就是把多个查询一起解决。(所以这是一个离线算法)
可以使用整体二分解决的题目需要满足以下性质:
询问的答案具有可二分性
修改对判定答案的贡献互相独立 ,修改之间互不影响效果
修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值
贡献满足交换律,结合律,具有可加性
题目允许使用离线算法
 ——许昊然《浅谈数据结构题几个非经典解法》
解释 记 [ 𝑙 , 𝑟 ] [ l , r ] [ 𝐿 , 𝑅 ] [ L , R ] [ 𝐿 , 𝑅 ] [ L , R ] [ 𝑙 , 𝑟 ] [ l , r ] 
我们首先把所有操作 按时间顺序  存入数组中,然后开始分治。 在每一层分治中,利用数据结构(常见的是树状数组)统计当前查询的答案和 𝑚 𝑖 𝑑 m i d  根据查询出来的答案和 𝑚 𝑖 𝑑 m i d 𝑚 𝑖 𝑑 m i d 𝑚 𝑖 𝑑 m i d 𝑞 1 q 1 𝑞 2 q 2  当 𝑙   = 𝑟 l = r  需要注意的是,在整体二分过程中,若当前处理的值域为 [ 𝑙 , 𝑟 ] [ l , r ] [ 𝑙 , 𝑟 ] [ l , r ] 
过程 注:
为可读性,文中代码或未采用实际竞赛中的常见写法。 若觉得某段代码有难以理解之处,请先参考之前题目的解释, 因为节省篇幅解释过的内容不再赘述。 从普通二分说起:
查询全局第 k 小 题 1  在一个数列中查询第 𝑘 k 
当然可以直接排序。如果用二分法呢?可以用数据结构记录每个大小范围内有多少个数,然后用二分法猜测,利用数据结构检验。
题 2  在一个数列中多次查询第 𝑘 k 
可以对于每个询问进行一次二分;但是,也可以把所有的询问放在一起二分。
先考虑二分的本质:假设要猜一个 [ 𝑙 , 𝑟 ] [ l , r ] 𝑙 l 𝑟 r 𝑚   = ⌊ 𝑙 + 𝑟 2 ⌋ m = ⌊ l + r 2 ⌋ 𝑚 m 𝑂 ( l o g  𝑛 ) O ( log  n ) 𝑞 q 𝑂 ( 𝑞 l o g  𝑛 ) O ( q log  n ) 
回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 𝑚 𝑖 𝑑 m i d 𝑚 𝑖 𝑑 m i d 𝑚 𝑖 𝑑 m i d 𝑚 𝑖 𝑑 m i d 𝑘 k 𝑚 𝑖 𝑑 m i d 𝑡 t 𝑘   − 𝑡 k − t 𝑙   = 𝑟 l = r [ 1 , 𝑛 ] [ 1 , n ] 𝑂 ( l o g  𝑛 ) O ( log  n ) 𝑂 ( 𝑇 ) O ( T ) 𝑂 ( 𝑇 l o g  𝑛 ) O ( T log  n ) 
参考代码如下:
实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 struct   Query   { 
   int   id ,   k ;    // 这个询问的编号, 这个询问的 k 
}; 
int   ans [ N ],   a [ N ];    // ans[i] 表示编号为i的询问的答案,a 为原数列 
int   val [ N ],   cnt [ N ];    // 离散化后,记录对应的值及其计数(假设已经处理好) 
// 返回原数列中值域在 [l,r] 中的数的个数 
int   check ( int   l ,   int   r )   { 
   int   res   =   0 ; 
   for   ( int   i   =   l ;   i   <=   r ;   i ++ )   { 
     res   +=   cnt [ i ]; 
   } 
   return   res ; 
} 
// 整体二分 
void   solve ( int   l ,   int   r ,   vector < Query >   q )   { 
   int   m   =   ( l   +   r )   /   2 ; 
   if   ( l   ==   r )   { 
     for   ( unsigned   i   =   0 ;   i   <   q . size ();   i ++ )   ans [ q [ i ]. id ]   =   val [ l ]; 
     return ; 
   } 
   vector < Query >   q1 ,   q2 ; 
   int   t   =   check ( l ,   m ); 
   for   ( unsigned   i   =   0 ;   i   <   q . size ();   i ++ )   { 
     if   ( q [ i ]. k   <=   t ) 
       q1 . push_back ( q [ i ]); 
     else 
       q [ i ]. k   -=   t ,   q2 . push_back ( q [ i ]); 
   } 
   solve ( l ,   m ,   q1 ),   solve ( m   +   1 ,   r ,   q2 ); 
   return ; 
} 
查询区间第 k 小 题 3  在一个数列中多次查询区间第 𝑘 k 
涉及到给定区间的查询,再按之前的方法进行二分就会导致 check 函数的时间复杂度爆炸。仍然考虑询问与值域中点 𝑚 m 𝑚 m 𝑡 t 𝑘 k 𝑘   ≤ 𝑡 k ≤ t 𝑚 m 𝑚 m [ 𝑙 , 𝑟 ] [ l , r ] 
参考代码(关键部分)
实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 struct   Num   { 
   int   p ,   x ; 
};    // 位于数列中第 p 项的数的值为 x 
struct   Query   { 
   int   l ,   r ,   k ,   id ; 
};    // 一个编号为 id, 询问 [l,r] 中第 k 小数的询问 
int   ans [ N ]; 
void   add ( int   p ,   int   x );    // 树状数组, 在 p 位置加上 x 
int   query ( int   p );          // 树状数组, 求 [1,p] 的和 
void   clear ();              // 树状数组, 清空 
void   solve ( int   l ,   int   r ,   vector < Num >   a ,   vector < Query >   q ) 
// a中为给定数列中值在值域区间 [l,r] 中的数 
{ 
   int   m   =   ( l   +   r )   /   2 ; 
   if   ( l   ==   r )   { 
     for   ( unsigned   i   =   0 ;   i   <   q . size ();   i ++ )   ans [ q [ i ]. id ]   =   l ; 
     return ; 
   } 
   vector < Num >   a1 ,   a2 ; 
   vector < Query >   q1 ,   q2 ; 
   for   ( unsigned   i   =   0 ;   i   <   a . size ();   i ++ ) 
     if   ( a [ i ]. x   <=   m ) 
       a1 . push_back ( a [ i ]),   add ( a [ i ]. p ,   1 ); 
     else 
       a2 . push_back ( a [ i ]); 
   for   ( unsigned   i   =   0 ;   i   <   q . size ();   i ++ )   { 
     int   t   =   query ( q [ i ]. r )   -   query ( q [ i ]. l   -   1 ); 
     if   ( q [ i ]. k   <=   t ) 
       q1 . push_back ( q [ i ]); 
     else 
       q [ i ]. k   -=   t ,   q2 . push_back ( q [ i ]); 
   } 
   clear (); 
   solve ( l ,   m ,   a1 ,   q1 ),   solve ( m   +   1 ,   r ,   a2 ,   q2 ); 
   return ; 
} 
下面提供 【模板】可持久化线段树 2  一题使用整体二分的,偏向竞赛风格的写法。
参考代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 
92 
93 
94 
95 
96 
97 
98 
99 #include   <algorithm> 
#include   <iostream> 
using   namespace   std ; 
constexpr   int   N   =   200020 ; 
int   n ,   m ; 
int   ans [ N ]; 
// BIT begin 
int   t [ N ]; 
int   a [ N ]; 
int   sum ( int   p )   { 
   int   ans   =   0 ; 
   while   ( p )   { 
     ans   +=   t [ p ]; 
     p   -=   p   &   ( - p ); 
   } 
   return   ans ; 
} 
void   add ( int   p ,   int   x )   { 
   while   ( p   <=   n )   { 
     t [ p ]   +=   x ; 
     p   +=   p   &   ( - p ); 
   } 
} 
// BIT end 
int   tot   =   0 ; 
struct   Query   { 
   int   l ,   r ,   k ,   id ,   type ;    // set values to -1 when they are not used! 
}   q [ N   *   2 ],   q1 [ N   *   2 ],   q2 [ N   *   2 ]; 
void   solve ( int   l ,   int   r ,   int   ql ,   int   qr )   { 
   if   ( ql   >   qr )   return ; 
   if   ( l   ==   r )   { 
     for   ( int   i   =   ql ;   i   <=   qr ;   i ++ ) 
       if   ( q [ i ]. type   ==   2 )   ans [ q [ i ]. id ]   =   l ; 
     return ; 
   } 
   int   mid   =   ( l   +   r )   /   2 ,   cnt1   =   0 ,   cnt2   =   0 ; 
   for   ( int   i   =   ql ;   i   <=   qr ;   i ++ )   { 
     if   ( q [ i ]. type   ==   1 )   { 
       if   ( q [ i ]. l   <=   mid )   { 
         add ( q [ i ]. id ,   1 ); 
         q1 [ ++ cnt1 ]   =   q [ i ]; 
       }   else 
         q2 [ ++ cnt2 ]   =   q [ i ]; 
     }   else   { 
       int   x   =   sum ( q [ i ]. r )   -   sum ( q [ i ]. l   -   1 ); 
       if   ( q [ i ]. k   <=   x ) 
         q1 [ ++ cnt1 ]   =   q [ i ]; 
       else   { 
         q [ i ]. k   -=   x ; 
         q2 [ ++ cnt2 ]   =   q [ i ]; 
       } 
     } 
   } 
   // rollback changes 
   for   ( int   i   =   1 ;   i   <=   cnt1 ;   i ++ ) 
     if   ( q1 [ i ]. type   ==   1 )   add ( q1 [ i ]. id ,   -1 ); 
   // move them to the main array 
   for   ( int   i   =   1 ;   i   <=   cnt1 ;   i ++ )   q [ i   +   ql   -   1 ]   =   q1 [ i ]; 
   for   ( int   i   =   1 ;   i   <=   cnt2 ;   i ++ )   q [ i   +   cnt1   +   ql   -   1 ]   =   q2 [ i ]; 
   solve ( l ,   mid ,   ql ,   cnt1   +   ql   -   1 ); 
   solve ( mid   +   1 ,   r ,   cnt1   +   ql ,   qr ); 
} 
pair < int ,   int >   b [ N ]; 
int   toRaw [ N ]; 
int   main ()   { 
   cin . tie ( nullptr ) -> sync_with_stdio ( false ); 
   cin   >>   n   >>   m ; 
   // read and discrete input data 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     int   x ; 
     cin   >>   x ; 
     b [ i ]. first   =   x ; 
     b [ i ]. second   =   i ; 
   } 
   sort ( b   +   1 ,   b   +   n   +   1 ); 
   int   cnt   =   0 ; 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     if   ( b [ i ]. first   !=   b [ i   -   1 ]. first )   cnt ++ ; 
     a [ b [ i ]. second ]   =   cnt ; 
     toRaw [ cnt ]   =   b [ i ]. first ; 
   } 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     q [ ++ tot ]   =   { a [ i ],   -1 ,   -1 ,   i ,   1 }; 
   } 
   for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   { 
     int   l ,   r ,   k ; 
     cin   >>   l   >>   r   >>   k ; 
     q [ ++ tot ]   =   { l ,   r ,   k ,   i ,   2 }; 
   } 
   solve ( 0 ,   cnt   +   1 ,   1 ,   tot ); 
   for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   cout   <<   toRaw [ ans [ i ]]   <<   '\n' ; 
} 
带修区间第 k 小 题 4  Dynamic Rankings  给定一个数列,要支持单点修改,区间查第 𝑘 k 
修改操作可以直接理解为从原数列中删去一个数再添加一个数,为方便起见,将询问和修改统称为「操作」。因后面的操作会依附于之前的操作,不能如题 3 一样将统计和处理询问分开,故可将所有操作存于一个数组,用标识区分类型,依次处理每个操作。为便于处理树状数组,修改操作可分拆为擦除操作和插入操作。
优化 
注意到每次对于操作进行分类时,只会更改操作顺序,故可直接在原数组上操作。具体实现,在二分时将记录操作的 𝑞 , 𝑎 q , a 𝐿 , 𝑅 L , R  树状数组每次清空会导致时间复杂度爆炸,可采用每次使用树状数组时记录当前修改位置(这已由 1 中提到的临时数组实现),本次操作结束后在原位置加 − 1 − 1  一开始对于数列的初始化操作可简化为插入操作。 参考代码(关键部分)
实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 struct   Opt   { 
   int   x ,   y ,   k ,   type ,   id ; 
   // 对于询问, type = 1, x, y 表示区间左右边界, k 表示询问第 k 小 
   // 对于修改, type = 0, x 表示修改位置, y 表示修改后的值, 
   // k 表示当前操作是插入(1)还是擦除(-1), 更新树状数组时使用. 
   // id 记录每个操作原先的编号, 因二分过程中操作顺序会被打散 
}; 
Opt   q [ N ],   q1 [ N ],   q2 [ N ]; 
// q 为所有操作, 
// 二分过程中, 分到左边的操作存到 q1 中, 分到右边的操作存到 q2 中. 
int   ans [ N ]; 
void   add ( int   p ,   int   x ); 
int   query ( int   p );    // 树状数组函数, 含义见题3 
void   solve ( int   l ,   int   r ,   int   L ,   int   R ) 
// 当前的值域范围为 [l,r], 处理的操作的区间为 [L,R] 
{ 
   if   ( l   >   r   ||   L   >   R )   return ; 
   int   cnt1   =   0 ,   cnt2   =   0 ,   m   =   ( l   +   r )   /   2 ; 
   // cnt1, cnt2 分别为分到左边, 分到右边的操作数 
   if   ( l   ==   r )   { 
     for   ( int   i   =   L ;   i   <=   R ;   i ++ ) 
       if   ( q [ i ]. type   ==   1 )   ans [ q [ i ]. id ]   =   l ; 
     return ; 
   } 
   for   ( int   i   =   L ;   i   <=   R ;   i ++ ) 
     if   ( q [ i ]. type   ==   1 )   {    // 是询问: 进行分类 
       int   t   =   query ( q [ i ]. y )   -   query ( q [ i ]. x   -   1 ); 
       if   ( q [ i ]. k   <=   t ) 
         q1 [ ++ cnt1 ]   =   q [ i ]; 
       else 
         q [ i ]. k   -=   t ,   q2 [ ++ cnt2 ]   =   q [ i ]; 
     }   else 
       // 是修改: 更新树状数组 & 分类 
       if   ( q [ i ]. y   <=   m ) 
         add ( q [ i ]. x ,   q [ i ]. k ),   q1 [ ++ cnt1 ]   =   q [ i ]; 
       else 
         q2 [ ++ cnt2 ]   =   q [ i ]; 
   for   ( int   i   =   1 ;   i   <=   cnt1 ;   i ++ ) 
     if   ( q1 [ i ]. type   ==   0 )   add ( q1 [ i ]. x ,   - q1 [ i ]. k );    // 清空树状数组 
   for   ( int   i   =   1 ;   i   <=   cnt1 ;   i ++ )   q [ L   +   i   -   1 ]   =   q1 [ i ]; 
   for   ( int   i   =   1 ;   i   <=   cnt2 ;   i ++ ) 
     q [ L   +   cnt1   +   i   -   1 ]   =   q2 [ i ];    // 将临时数组中的元素合并回原数组 
   solve ( l ,   m ,   L ,   L   +   cnt1   -   1 ),   solve ( m   +   1 ,   r ,   L   +   cnt1 ,   R ); 
   return ; 
} 
针对静态序列的优化 题 5  【模板】可持久化线段树 2  给定一个序列,区间查询第 𝑘 k 
树套树和整体二分实现带修区间第 𝑘 k 𝑂 ( 𝑛 l o g 2  𝑛 ) O ( n log 2  n ) 𝑘 k 𝑂 ( 𝑛 l o g  𝑛 ) O ( n log  n ) 𝑘 k 𝑂 ( 𝑛 l o g 2  𝑛 ) O ( n log 2  n ) 
优化 
对于每一轮划分,如果当前数列中小于等于 𝑚 𝑖 𝑑 m i d 𝑡 t 𝑘   − 𝑡 k − t [ 𝐿 , 𝑅 ] [ L , R ] [ 𝑙 , 𝑟 ] [ l , r ] [ 𝐿 , 𝑙 ) [ L , l )  由于需要使每轮划分都仅和当前答案值域 [ 𝑙 , 𝑟 ] [ l , r ]  如果划分不仅仅和当前答案值域有关呢?
由此可以得到一个与全局序列有关的优化方法:维护一个指针 𝑝 𝑜 𝑠 p o s 𝑚 𝑖 𝑑 m i d ≤ 𝑝 𝑜 𝑠 ≤ p o s 1 1 0 0 𝑝 𝑜 𝑠 p o s 𝑝 𝑜 𝑠 p o s 𝑛 l o g  𝑛 n log  n 不需要对询问做出修改 。
由于要追踪分治中心,需要让 𝑝 𝑜 𝑠 p o s 可以用整体二分解决并且不带修改的问题 ,都可以应用此种优化以大幅降低数据结构的使用次数。
由于减少了很多树状数组的载入和清空操作,应用这种优化通常情况下会明显提升整体二分的效率(即使只是常数优化),对于静态区间第 𝑘 k 𝑘 k 𝑂 ( 𝑛 l o g  𝑛 ) O ( n log  n ) 
参考代码(关键部分)
实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 struct   Query   { 
   int   i ,   l ,   r ,   k ; 
};    // 第 i 次询问查询区间 [l,r] 的第 k 小值 
Query   s [ 200005 ],   t1 [ 200005 ],   t2 [ 200005 ]; 
int   n ,   m ,   cnt ,   pos ,   p [ 200005 ],   ans [ 200005 ]; 
pair < int ,   int >   a [ 200005 ]; 
void   add ( int   x ,   int   y );    // 树状数组 位置 x 加 y 
int   sum ( int   x );            // 树状数组 [1,x] 前缀和 
// 当前处理的询问为 [l,r],答案值域为 [ql,qr] 
void   overall_binary ( int   l ,   int   r ,   int   ql ,   int   qr )   { 
   if   ( l   >   r )   return ; 
   if   ( ql   ==   qr )   { 
     for   ( int   i   =   l ;   i   <=   r ;   i ++ )   ans [ s [ i ]. i ]   =   ql ; 
     return ; 
   } 
   int   cnt1   =   0 ,   cnt2   =   0 ,   mid   =   ( ql   +   qr )   >>   1 ; 
   // 追踪分治中心,认为 [1,pos] 的值已经载入树状数组 
   while   ( pos   <=   n   -   1   &&   a [ pos   +   1 ]. first   <=   mid ) 
     add ( a [ pos   +   1 ]. second ,   1 ),   ++ pos ; 
   while   ( pos   >=   1   &&   a [ pos ]. first   >   mid )   add ( a [ pos ]. second ,   -1 ),   -- pos ; 
   for   ( int   i   =   l ;   i   <=   r ;   i ++ )   { 
     int   now   =   sum ( s [ i ]. r )   -   sum ( s [ i ]. l   -   1 ); 
     if   ( s [ i ]. k   <=   now ) 
       t1 [ ++ cnt1 ]   =   s [ i ]; 
     else 
       t2 [ ++ cnt2 ]   =   s [ i ];    // 注意 不应修改询问信息 
   } 
   for   ( int   i   =   1 ;   i   <=   cnt1 ;   i ++ )   s [ l   +   i   -   1 ]   =   t1 [ i ]; 
   for   ( int   i   =   1 ;   i   <=   cnt2 ;   i ++ )   s [ l   +   cnt1   +   i   -   1 ]   =   t2 [ i ]; 
   overall_binary ( l ,   l   +   cnt1   -   1 ,   ql ,   mid ); 
   overall_binary ( l   +   cnt1 ,   r ,   mid   +   1 ,   qr ); 
} 
int   main ()   { 
   scanf ( "%d%d" ,   & n ,   & m ); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     scanf ( "%d" ,   & a [ i ]. first ); 
     a [ i ]. second   =   i ; 
     p [ ++ cnt ]   =   a [ i ]. first ; 
   } 
   sort ( a   +   1 ,   a   +   n   +   1 );    // 对序列排序 离散化 
   sort ( p   +   1 ,   p   +   n   +   1 ); 
   cnt   =   unique ( p   +   1 ,   p   +   n   +   1 )   -   p   -   1 ; 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ ) 
     a [ i ]. first   =   lower_bound ( p   +   1 ,   p   +   cnt   +   1 ,   a [ i ]. first )   -   p ; 
   // 省略读入询问 
   overall_binary ( 1 ,   m ,   1 ,   cnt ); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   printf ( "%d \n " ,   p [ ans [ i ]]); 
   return   0 ; 
} 
区间前驱后继 题 6  在一个数列中多次查询 𝑘 k 𝑘 k 𝑘 k 
以前驱为例,使用数据结构解决此种问题的方法一般是先查询区间内有多少严格小于 𝑘 k 𝑥 x 𝑥 x 𝑘 k 𝑥 x 𝑥   + 1 x + 1 
考虑使用整体二分解决这个问题:整体二分是一种高效求解区间第 𝑘 k CDQ 分治  可以离线高效求解区间内的排名。先跑一遍 CDQ 分治求出排名就可以使用整体二分得到区间内部的前驱和后继了。
此问题还可以用 CDQ 分治套线段树离线一遍解决,但效率远低于跑两遍的 CDQ 分治 + 整体二分。
构造单调性序列 题 7  Sequence  给定一个序列,每次操作可以把某个数 + 1 + 1 − 1 − 1 
此类题目也可以使用动态规划或反悔贪心解决。
在满足操作次数最小化的前提下,一定存在一种方案使得最后序列中的每个数都是序列修改前存在的,这个结论可以使用数学归纳法证明。由于题目并不需要最终序列的信息,问题转化为求出最小操作次数。
由于要求最终的序列单调不降,可以使用整体二分。每轮整体二分判定最终序列区间 [ 𝑙 , 𝑟 ] [ l , r ] [ 𝑞 𝑙 , 𝑞 𝑟 ] [ q l , q r ] 𝑚 𝑖 𝑑   = ⌊ 𝑞 𝑙 + 𝑞 𝑟 2 ⌋ m i d = ⌊ q l + q r 2 ⌋ [ 𝑚 𝑖 𝑑   + 1 , 𝑞 𝑟 ] [ m i d + 1 , q r ] [ 𝑞 𝑙 , 𝑚 𝑖 𝑑 ] [ q l , m i d ] 0 0 [ 𝑙 , 𝑟 ] [ l , r ] 𝑚 𝑖 𝑑   + 1 m i d + 1 [ 𝑙 , 𝑟 ] [ l , r ] 𝑖 i [ 𝑙 , 𝑖 ] [ l , i ] 𝑚 𝑖 𝑑 m i d [ 𝑖   + 1 , 𝑟 ] [ i + 1 , r ] 𝑚 𝑖 𝑑   + 1 m i d + 1 [ 𝑞 𝑙 , 𝑚 𝑖 𝑑 ] [ q l , m i d ] 
划分时已经保证了最终序列的单调性不被破坏,同时因为每次都取最小操作次数,最终被划分至左区间的数取 𝑚 𝑖 𝑑 m i d 𝑚 𝑖 𝑑   + 1 m i d + 1 
参考代码(关键部分)
实现   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 int   a [ 500005 ],   ans [ 500005 ];    // a:原序列 ans:构造的序列 
void   overall_binary ( int   l ,   int   r ,   int   ql ,   int   qr )   { 
   if   ( l   >   r )   return ; 
   if   ( ql   ==   qr )   { 
     for   ( int   i   =   l ;   i   <=   r ;   i ++ )   ans [ i ]   =   ql ; 
     return ; 
   } 
   int   cnt   =   0 , 
       mid   =   ql   +   (( qr   -   ql )   >>   1 );    // 默认开始都填 mid+1 全部划分到右区间 
   long   long   res   =   0l l ,   sum   =   0l l ; 
   for   ( int   i   =   l ;   i   <=   r ;   i ++ )   sum   +=   abs ( a [ i ]   -   ( mid   +   1 )); 
   res   =   sum ; 
   for   ( int   i   =   l ;   i   <=   r ; 
        i ++ )   {    // 尝试把 [l,i] 从 mid+1 换成 mid 并且划分到左区间 
     sum   -=   abs ( a [ i ]   -   ( mid   +   1 )); 
     sum   +=   abs ( a [ i ]   -   mid ); 
     if   ( sum   <   res )   cnt   =   i   -   l   +   1 ,   res   =   sum ;    // 发现 [l,i] 取 mid 更优,更新 
   } 
   overall_binary ( l ,   l   +   cnt   -   1 ,   ql ,   mid ); 
   overall_binary ( l   +   cnt ,   r ,   mid   +   1 ,   qr ); 
} 
参考习题 「国家集训队」矩阵乘法 
「POI2011 R3 Day2」流星 Meteors 
二逼平衡树 
[BalticOI 2004] Sequence 数字序列 
参考资料 2025/8/3 04:51:50 ,更新历史 在 GitHub 上编辑此页! Ir1d , Henry-ZHR , partychicken , Prurite , ScaredQiu , Tiphereth-A , 2018-Danny , abc1763613206 , c-forrest , CCXXXI , dengxijian , Enter-tainer , GavinZhengOI , HeRaNO , hsfzLZH1 , iamtwz , kenlig , ksyx , LingeZ3z , Lynricsy , Marcythm , ouuan , ranwen , Sheng-Horizon , ShizuhaAki , Shyanko CC BY-SA 4.0  和 SATA