在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。有一个联合-查找算法定义了两个用于此数据结构的操做:node
因为支持这两种操做,一个不相交集也常被称为联合-查找数据结构或合并-查找集合。其余的重要方法,\(MakeSet\),用于建立单元素集合。有了这些方法,许多经典的划分问题能够被解决。c++
为了更加精确的定义这些方法,须要定义如何表示集合。一种经常使用的策略是为每一个集合选定一个固定的元素,称为表明,以表示整个集合。接着,\(Find(x)\) 返回 \(x\) 所属集合的表明,而 $Union $使用两个集合的表明做为参数。算法
这是两个并查集经常使用的优化数组
当咱们在寻找祖先时,一旦元素多且来,并查集就会退化成单次\(O(n)\)的算法,为了解决这一问题咱们能够在寻找祖先的过程当中直接将子节点连在祖先上,这样能够大大下降复杂度,均摊复杂度是\(O(log(n))\)的数据结构
按秩合并也是常见的优化方法,“秩”的定义很普遍,举个例子,在不路径压缩的状况下,常见的状况是把子树的深度定义为秩app
不管如何定义一般状况是把“秩”储存在根节点,合并的过程当中把秩小的根节点插到根大的根节点上,这样能够减小操做的次数函数
特别的,若是把秩定义为集合的大小,那么采用了按秩合并的并查集又称“启发式并查集”测试
按秩合并的均摊复杂度是\(O(log(n))\)的,若是同时采用按秩合并和路径压缩均摊复杂度是\(O(\alpha(n) )\),\(\alpha(n)\)是反阿克曼函数
\[ \forall n \le 2^{10^{19729}},\alpha(n)\le 5 \]
能够视为均摊复杂度为\(O(1)\)优化
不过一般状况下咱们仅采用路径压缩便可ui
int father[N]; int getfather( int x )//查询 { if( father[x] == x ) return x; return father[x] = getfather( father[x] ); } inline void union( int x , int y )//合并 { register int fx = getfather( x ) , fy = getfather( y ); father[ fx ] = fy; return ; } inline bool same( int x , int y ) { return getfather( x ) == getfather( y ) ;} //判读是否在同一结合 //把深度看成秩的 按秩合并 memset( rank , 0 , sizeof( rank ) ); inline void rank_union( int x , int y ) { fx = getfather( x ) , fy = getfather( y ); if( rank[ fx ] < rank[ fy ] ) ) father[ fx ] = fy; else { father[ fy ] = fx; if( rank[ fx ] == rank[ fy ] ) rank[ fx ] ++; } return ; }
虽然是\(noi\)的题但整体仍是很简单的,本质就是维护一个并查集
根据操纵把相同的所有合并,在把逐一判断不相同的
为何不能反过来作呢?举个例子\(a\ne b,b\ne c\)可否推出\(a\ne c\)呢?
显然不能
为何?由于不等关系没有传递性
那为何相同能够呢?由于相同是有传递性的
因此从本题也可知并查集维护的必定要具备传递性
那么剩下的就数据比较大,但\(n\le 1e6\)因此离散化便可
#include <bits/stdc++.h> #define PII pair< int , int > #define S second #define F first using namespace std; const int N = 1e6+5; int n , cur[ N * 2 ] , fa[ N * 2 ] , ta , tb , cnt; PII a[N] , b[N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline int getfa( int x ) { if( x == fa[x] ) return x; return fa[x] = getfa( fa[x] ); } inline void work() { cnt = ta = tb = 0; n = read(); for( register int i = 1 , u , v , w ; i <= n ; i ++ ) { u = read() , v = read() , w = read(); cur[ ++ cnt ] = u , cur[ ++ cnt ] = v; if( w ) a[ ++ ta ] = { u , v }; else b[ ++ tb ] = { u , v }; } sort( cur + 1 , cur + 1 + cnt ); cnt = unique( cur + 1 , cur + 1 + cnt ) - cur - 1; for( register int i = 1 ; i <= cnt ; i ++ ) fa[i] = i; register int fx , fy; for( register int i = 1 ; i <= ta ; i ++ ) { a[ i ].F = lower_bound( cur + 1 , cur + 1 + cnt , a[i].F ) - cur; a[ i ].S = lower_bound( cur + 1 , cur + 1 + cnt , a[i].S ) - cur; fx = getfa( a[i].F ) , fy = getfa( a[i].S ); fa[ fx ] = fy; } for( register int i = 1 ; i <= tb ; i ++ ) { b[ i ].F = lower_bound( cur + 1 , cur + 1 + cnt , b[i].F ) - cur; b[ i ].S = lower_bound( cur + 1 , cur + 1 + cnt , b[i].S ) - cur; fx = getfa( b[i].F ) , fy = getfa( b[i].S ); if( fx != fy ) continue; puts("NO"); return ; } puts("YES"); return ; } int main() { for( register int T = read() ; T ; T -- ) work(); return 0; }
咱们在维护并查集实际的过程当中额外的在维护一个\(dis\)表明从当前的点到根节点的距离。因为路径压缩致使每次访问后都会将因此的点指向根节点。因此咱们要在每次维护的过程当中更新\(dis\)数组,此时须要咱们在维护一个\(size\)数组,表明在每一个根节点的子树的大小,怎样咱们合并的过程当中就能够把根节点\(x\)插到根节点\(y\)的后面,而且让\(dis[x]+=size[y]\)。这样咱们就能够在压缩路径的过程当中,不断的更新每一个节点到根节点的距离
注意这里的状况说的是,每一个树都是一条链,且每次都是将一条连接在另外一条链的后面
那么若是是将任意一颗子树插到另外一颗子树的任意一个节点怎么办办呢?
而且我还要压缩路径,实际上是能够的
咱们而且只用两个数组\(father\)和\(dis\)就能够实现
int father[N] , dis[N] inline int getfather( int x ) { if( father[x] == x ) return x; register int root =getfather( father[x] ); dis[x] = dis[ father[x] ] + 1; return father[x] = root; } inline void merge( int x , int y )//把 根节点x 插到 结点y 上 { register int fx = getfather( x ) , fy = getfather( y ); fa[x] = fy; dis[fx] += dis[y] + 1 ; return ; } inline void init()//初始化 { for( register int i = 1 ; i <= n ; i++ ) father[i] = i; }
注意这里的\(x\)必须是根节点
假设咱们要把\(x\)插到\(y\)上,咱们直接用\(dis[y]+1\)来更新\(dis[x]\),对于\(x\)的子节点咱们能够在递归的时候修改
注意,若是须要使用\(dis[x]\)在用以前必需要先调用一次\(getfather(x)\),来更新一下\(dis[x]\)和\(fahter[x]\)
因此时间复杂度可能会略高,但没有具体证实,由于这个算法是一天中午我本身琢磨出来的,且没有在网上找严格的证实
这就是到带权并查集的模板,因此在没有在上面放代码
能够直接琢磨下这个代码
#include <bits/stdc++.h> using namespace std; const int N = 30005; int fa[N] , dis[N] ,size[N] , n ; char opt; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline int getfa( int x ) { if( fa[x] == x ) return x; register int root = getfa( fa[x] ); dis[ x ] += dis[ fa[x] ]; return fa[x] = root; } inline void merge( int x , int y ) { x = getfa( x ) , y = getfa( y ); fa[ x ] = y, dis[ x ] += size[ y ] , size[y] += size[x] , size[ x ] = 0; return ; } inline int query( int x , int y ) { register int fx = getfa( x ) , fy = getfa( y ); if( fx != fy ) return - 1; return abs( dis[x] - dis[y] ) - 1; } int main() { n = read(); for( register int i = 1 ; i < N ; i ++ ) fa[i] = i , size[i] = 1 ; for( register int u , v ; n ; n -- ) { do{ opt = getchar() ; }while( opt != 'C' && opt != 'M' ); u = read() , v = read() ; if(opt == 'M') merge( u , v ); else printf( "%d\n" , query( u , v ) ); } return 0; }
扩展域并查集就是将并查集的区域大小扩展成整数倍,用多个区域来同时维护多个传递关系
这是一道经典的扩展域并查集
首先咱们用一个\(sum[x]\)数组,表明从\(1\)到\(x\)的\(1\)的个数
若是当前询问的答案是\(even\)也就是偶数,那么\(sum[l-1]\)与\(sum[r]\)的寄偶性应该相同
若是当前询问的答案是\(odd\)也就是寄数,那么\(sum[l-1]\)与\(sum[r]\)的寄偶性应该不相同
因此咱们能够建一个大小为\(2n\)的并查集,其中\(1\cdots n\)表示偶数的关系、\(n+1\cdots 2n\)表示奇数
为了表示方便咱们定义两个变量\(x\_even=x,x\_odd=x+n\)
若是奇数的话咱们就把\(x\_even\)和\(y\_odd\)合并,\(x\_odd\)和\(y\_even\)合并
若是偶数的话咱们就把\(x\_odd\)和\(y\_odd\)合并,\(x\_even\)和\(y\_even\)合并
另外在每次合并前都要判断一下时候正确
#include <bits/stdc++.h> #define PII pair< int , int > #define PIIB pair < PII , bool > #define F first #define S second #define hash( x ) ( lower_bound( a + 1 , a + 1 + n , x ) - a ) using namespace std; const int N = 10010; int a[ N << 1 ] , n , m , fa[ N << 2 ]; PIIB opt[N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline int getfa( int x ) { if( fa[x] == x ) return x; return fa[x] = getfa( fa[x] ); } int main() { n = read() , m = read() , n = 0; register string str; for( register int i = 1 ; i <= m ; i ++ ) { opt[i].F.F = read() , opt[i].F.S = read(); a[ ++ n ] = opt[i].F.F - 1 , a[ ++ n ] = opt[i].F.S; cin >> str; if( str == "even" ) opt[i].S = 1; else opt[i].S = 0; } //离散化 sort( a + 1 , a + 1 + n ); n = unique( a + 1 , a + 1 + n ) - a - 1 ; for( register int i = 1 ; i <= m ; i ++ ) opt[i].F.F = hash( opt[i].F.F - 1 ) , opt[i].F.S = hash( opt[i].F.S ); for( register int i = 1 ; i <= 2 * n ; i ++ ) fa[i] = i; for( register int i = 1 , x_even , x_odd , y_even , y_odd ; i <= m ; i ++ ) { x_even = getfa( opt[i].F.F + n ) , x_odd = getfa( opt[i].F.F ) , y_even = getfa( opt[i].F.S + n ) , y_odd = getfa( opt[i].F.S ); if( opt[i].S ) // 不一样 { if( x_odd == y_even ) printf( "%d\n" , i - 1) , exit(0); fa[ x_even ] = y_even , fa[ x_odd ] = y_odd; } else// 相同 { if( x_even == y_even ) printf( "%d\n" , i - 1 ) , exit(0); fa[ x_even ] = y_odd , fa[ x_odd ] = y_even; } } printf( "%d\n" , m ); return 0; }
经典的扩展域并查集,咱们能够开三个域,同类,食物,天敌,来维护这样一个集合
一样为了方便表示,分别用\(x_a,x_b,x_c\)表示\(x\)的同类,\(x\)的食物,\(x\)的天敌
若是\(x\)和\(y\)是同类,就把\(x_a\)和\(y_a\)合并、\(x_b\)和\(y_b\)合并、\(x_c\)和\(y_c\)合并
若是\(x\)吃\(y\),就把\(x_a\)和\(y_c\)合并、\(x_b\)和\(y_a\)合并、\(x_c\)和\(y_b\)合并
因此针对每次操做前想判断是否出现冲突,在进行合并便可
#include <bits/stdc++.h> using namespace std; const int N = 5e4 + 5; int n , m , cnt , fa[ N * 3 ]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 1 ) + ( x << 3 ) + ch - '0'; ch = getchar(); } return x; } inline int getfa( int x ) { if( fa[x] == x ) return x; return fa[x] = getfa( fa[x] ); } int main() { n = read() , m = read(); for( register int i = 1 ; i <= n * 3 ; i ++ ) fa[i] = i; for( register int opt , x , y , x_a , x_b , x_c , y_a , y_b , y_c ; m >= 1 ; m -- ) { opt = read() , x = read() , y = read(); x_a = getfa( x ) , x_b = getfa( x + n ) , x_c = getfa( x + 2 * n ) , y_a = getfa( y ) , y_b = getfa( y + n ) , y_c = getfa( y + 2 * n ); // x_a x的同类 x_b x的食物 x_c x的天敌 if( x > n || y > n || ( opt == 2 && x == y ) ) { cnt ++ ; continue; } if( opt == 1 ) { if( x_b == y_a || x_c == y_a ) { cnt ++ ; continue ; } fa[ x_a ] = y_a , fa[ x_b ] = y_b , fa[ x_c ] = y_c; } else { if( x_a == y_a || x_c == y_a ) { cnt ++ ; continue ; } fa[ x_a ] = y_c , fa[ x_b ] = y_a , fa[ x_c ] = y_b; } } cout << cnt << endl; return 0; }
这道题是\(NOIP2017\)来的一道题,这道题也是我第一次参加联赛遇到题,考场上我并无看出这是道并查集
这道题其实很暴力由于\(n\)的范围比较小,我么能够直接\(O(N^2)\)暴力枚举,而后用并查集判断便可,在随后枚举与上下底面相交或相切的圆判断是否在一个集合里便可
#include <bits/stdc++.h> #define LL long long #define PII pair< LL , LL > #define PIII pair < PII , LL > #define F first #define S second #define pb( x ) push_back( x ) #define square( x ) ( x * x ) using namespace std; const int N = 1005; LL n , h , r , fa[N] ; vector < LL > Floor , Roof; vector < PIII > node; inline LL read() { register LL x = 0 , f = 1; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f; } inline LL getfa( LL x ) { if( x == fa[x] ) return x; return fa[x] = getfa( fa[x] ); } inline void merge( LL x , LL y ) { x = getfa( x ) , y = getfa( y ); fa[x] = y; return ; } inline bool pending( LL x , LL y ) { return ( square( ( node[x].F.F - node[y].F.F ) ) + square( ( node[x].F.S - node[y].F.S ) ) + square( ( node[x].S - node[y].S ) ) ) <= r * r * 4 ; } inline void work() { n = read() , h = read() , r = read() , node.clear() , Floor.clear() , Roof.clear(); for( register int i = 0 , x , y , z ; i < n ; i ++ ) { fa[i] = i , x = read() , y = read() , z = read(); node.push_back( { { x , y } , z } ); if( z - r <= 0 ) Floor.push_back( i ); if( z + r >= h ) Roof.push_back( i ); } for( register int i = 0 ; i < n ; i ++ ) { for( register int j = i + 1 ; j < n ; j ++ ) { if( pending( i , j ) ) merge( i , j ); } } for( auto i : Floor ) { for( auto j : Roof ) { if( getfa( i ) != getfa( j ) ) continue; puts("Yes"); return ; } } puts("No"); return ; } int main() { for( register int T = read() ; T >= 1 ; T -- ) work(); return 0; }
树状数组这里就不讲原理了,给张图本身理解便可
注意树状数组维护的数组下标必须是\(1\cdots n\),若是有\(0\)就会死循环
模板题直接看代码便可
#include <bits/stdc++.h> #define lowbit( x ) ( x & -x ) #define LL long long using namespace std; const int N = 1e6 + 5; LL n , m , bit[N]; inline LL read() { register LL x = 0 , f = 1 ; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f ; } inline void add( LL x , LL w ) { for( register int i = x ; i <= n ; i += lowbit( i ) ) bit[i] += w; } inline LL find( LL x ) { register LL sum = 0; for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) sum += bit[i]; return sum; } int main() { n = read() , m = read(); for( register int i = 1 , x ; i <= n ; i ++ ) x = read() , add( i , x ); for( register int i = 1 , opt , x , y ; i <= m ; i ++ ) { opt = read() , x = read() , y = read(); if( opt == 1 ) add( x , y ); else printf( "%lld\n" , find( y ) - find( x - 1 ) ); } return 0; }
模板题,没啥好解释的,以前看模板就能写出来
区间修改也是树状数组的经典操做,简单来讲就是维护一个差分序列
#include <bits/stdc++.h> #define LL long long #define lowbit( x ) ( x & - x ) using namespace std; const int N = 1e6 + 1000; LL n , m , bit[N]; inline LL read() { register LL x = 0 , f = 1; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f; } inline LL add( LL x , LL w ) { for( register int i = x ; i <= n ; i += lowbit( i ) ) bit[i] += w ; } inline LL find( LL x ) { register LL sum = 0; for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) sum += bit[i]; return sum; } int main() { n = read() , m = read(); for( register LL i = 1 , last = 0 , x ; i <= n ; i ++ ) x =read() ,add( i , x - last ) , last = x ; for( register LL i = 1 , opt , l , r , w ; i <= m ; i ++ ) { opt = read(); if( opt == 1 ) { l = read() , r = read() , w = read(); add( l , w ) , add( r + 1 , -w ); } else printf("%lld\n" , find( read() ) ); } return 0; }
仍是到模板题,直接套板子吧
联系上一道题,咱们假设原数组是\(a[i]\)维护一个差分数组\(d[i]\)天然能够获得
\[ \sum_{i=1}^{n}a[i]=\sum_{i=1}^{n}\sum_{j=1}^{i}d[i] \]
而后咱们发现\(d[1]\)用了\(p\)次,\(d[2]\)用了\(p-1\)次,\(d[i]\)用了\(p-i+1\)次,因此能够获得
\[ \sum_{i=1}^{n}\sum_{j=1}^{i}d[i]=\sum_{i=1}^{n}d[i]\times(n-i+1)=(n+1)\times\sum_{i=1}^{n}d[i]\times\sum_{i=1}^{n}(d[i]\times i) \]
因此咱们能够同时维护两个数组\(sum1[i]=\sum d[i],sum2[i]=\sum (d[i]\times i)\)
查询
查询位置\(p\),\((p+1)\)乘以\(sum1\)种\(p\)的前缀减去\(sum2\)中\(p\)的前缀
查询\([l,r]\)的区间和,\(r\)的前缀减\(l-1\)的前缀
修改
对于\(sum1\)中的修改相似与上一个问题的修改
对于\(sum2\)中的修改,给\(sum2[l]+=l\times x , sum2[r+1]-=(r+1)\times x\)
#include <bits/stdc++.h> #define LL long long #define lowbit( x ) ( x & - x ) using namespace std; const int N = 1e6 +5; int n , m ; LL sum1[N] , sum2[N]; inline int read() { register int x = 0 , f = 1; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f; } inline void add( int x , LL w ) { for( register int i = x ; i <= n ; i += lowbit( i ) ) sum1[i] += w , sum2[i] += w * x; } inline LL find( int x ) { register LL s = 0 , t = 0 ; for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) s += sum1[i] , t += sum2[i]; return ( x + 1 ) * s - t; } int main() { n = read() , m = read(); for( register int i = 1 , x , last = 0 ; i <= n ; i ++ ) x = read() , add( i , x - last ) , last = x ; for( register int i = 1 , opt , l , r , w ; i <= m ; i ++ ) { opt = read(); if( opt == 1 ) l = read() , r = read() , w = read() , add( l , w ) , add( r + 1 , - w ); else l = read() , r = read() , printf( "%lld\n" , find( r ) - find( l - 1 ) ); } return 0; }
根据树状数组的性质咱们能够快速的求出前\(k\)个数的和,加上坐标是递增给的,咱们能够在每次插入前统计在当前星星以前有多少个星星便可,显然纵坐标是没有用的
注意坐标是从\((0,0)\)开始的,但树状数组的下标是从\(1\)开始因此给坐标总体加\(1\)便可p
#include <bits/stdc++.h> #define lowbit( x ) ( x & - x ) using namespace std; const int N = 15e3 + 5 , M = 32010; int n , bit[M] , level[N]; inline int read() { register int x = 0 , f = 1 ; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f ; } inline void add( int x , int w ) { for( register int i = x ; i <= 32001 ; bit[i] += w , i += lowbit( i ) ); } inline int find( int x ) { register int sum = 0; for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) sum += bit[i]; return sum; } int main() { n = read(); for( register int i = 1 , x ; i <= n ; x = read() + 1 , read() , level[ find(x) ] ++ , add( x , 1 ) , i ++ ); for( register int i = 0 ; i < n ; printf( "%d\n" , level[i] ) , i ++ ); return 0; }
咱们把每次种树抽象成一个线段,同时开两个树状数组分别维护每条线段的两个端点
插入时在\(l\)处增长一个左端点,\(r\)处增长一个右端点
查询时查询$1\cdots r $的右端点个数,\(1\cdots l\)的左端点个数,作差就是\(l\cdots r\)中线段的个数
#include <bits/stdc++.h> #define lowbit( x ) ( x & - x ) using namespace std; const int N = 5e4 + 5; int n , m , bit[2][N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while ( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int w , int k ) { for( register int i = x ; i <= n ; i += lowbit( i ) ) bit[k][i] += w ; } inline int find( int x , int k ) { register int res = 0 ; for( register int i = x ; i >= 1 ; i -= lowbit( i ) ) res += bit[k][i]; return res; } int main() { n = read() , m = read(); for( register int i = 1 , op , l , r ; i <= m ; i ++ ) { op = read() , l = read() , r = read(); if( op == 1 ) add( l , 1 , 0 ) , add( r , 1 , 1 ); else printf( "%d\n" , find( r , 0 ) - find( l - 1 , 1 ) ); } return 0; }
逆序对是树状数组的经典操做,其实至关于用树状数组维护了一个桶
#include <bits/stdc++.h> #define lowbit( x ) ( x & - x ) #define LL long long using namespace std; const int N = 5e5 + 5 ; int n , m , a[N] , b[N] , bit[N]; LL cnt ; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int w ) { for( register int i = x ; i <= n ; bit[i] += w , i += lowbit( i ) ); } inline LL find( int x ) { register LL sum = 0; for( register int i = x ; i >= 1 ; sum += bit[i] , i -= lowbit( i ) ); return sum; } int main() { n = read(); for( register int i = 1 ; i <= n ; i ++ ) a[i] = b[i] = read(); sort( b + 1 , b + 1 + n ); m = unique( b + 1 , b + 1 + n ) - b - 1; for( register int i = 1 ; i <= n ; i ++ ) a[i] = lower_bound( b + 1 , b + 1 + m , a[i] ) - b; for( register int i = n ; i >= 1 ; i -- ) { add( a[i] , 1 ); cnt += find( a[i] - 1 ); } cout << cnt << endl; return 0; }
首先先来两个引理
\[ a \oplus a = 0\\0\oplus a = a \]
知道这个引理后,咱们就能够看下样例
\[ a_2 \oplus a_3 \oplus a_4 \oplus (a_2 \oplus a_3) \oplus (a_3 \oplus a_4) \oplus (a_2 \oplus a_3 \oplus a_4)=a_2 \oplus a_2 \oplus a_2 \oplus a_3 \oplus a_3 \oplus a_3\oplus a_3 \oplus a_4 \oplus a_4 \oplus a_4\\=a_2\oplus0 \oplus a_4= 2 \]
也就是说咱们能够根据异或的一些性质,来优化一下
若是\(a\)的个数是奇数个其贡献就是\(a\),若是\(a\)的个数是偶数个其贡献就是\(0\)
手推几个数据就能发现
若是\(l,r\)奇偶性不一样全部的数都是偶数个,结果天然是\(0\)
若是\(l,r\)奇偶性相同,那么只有和\(l,r\)奇偶性的位置上的数才会有贡献
咱们能够开两个树状数组来维护下,一个维护奇数位上的异或前缀和,另外一个维护偶数位上的异或前缀和
对于操做1,注意不是把第\(i\)位异或\(j\)是把\(i\)为修改成\(j\),根据异或和的性质咱们要先异或\(a[i]\)在异或\(j\),因此能够异或$a[i]\oplus j $
对于操做2首先特判奇偶性不一样的,对于奇偶性相同的,咱们在对应的树状数组里求出\(r,l-1\)的异或前缀和,在异或一下便可
#include <bits/stdc++.h> #define lowbit( x ) ( x & - x ) using namespace std; const int N = 2e5 + 10; int n , m , bit[2][N] , a[N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int val , int pos , int k ) { for( register int i = pos ; i <= n ; bit[k][i] ^= val , i += lowbit( i ) ); } inline int find( int pos , int k ) { register int res = 0; for( register int i = pos ; i >= 1 ; res ^= bit[k][i] , i -= lowbit( i ) ); return res; } int main() { n = read() , m = read(); for( register int i = 1 ; i <= n ; a[i] = read() , add( a[i] , i , i & 1 ) , i ++ ); for( register int i = 1 , op , l , r ; i <= m ; i ++ ) { op = read() , l = read() , r = read(); if( op == 1 ) add( a[l] ^ r , l , l & 1 ) , a[l] = r ; else printf( "%d\n" , ( ( l + r ) & 1 ) ? 0 : ( find( r , r & 1 ) ^ find( l - 1 , r & 1 ) ) ); } return 0; }
考虑如何用树状数组作
咱们能够依次插入每一个数
好比咱们要插入\(x\),就个\(x\)个这个位置加\(1\),而后\(find(x)\)求前缀和,就知道小于等于\(x\)的数有多少个
而后\(A_i\)的范围很大,\(n\)的范围比较小,且咱们不须要知道每一个数的具体大小,只需知道相对大小便可,天然选择离散化
而后就是求第\(k\)个数有多大,如过二分的话是\(O(log^2(n))\)的,比较慢
根据树状数组的特性,考虑倍增,具体过程至关把lowbit
的过程倒过来,具体能够看代码理解
#include <bits/stdc++.h> #define lowbit( x ) ( x & -x ) using namespace std; const int N = 1e5 + 5; int n , m , a[N] , b[N] , bit[N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 1 ) + ( x << 3 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int p ) { for( register int i = x ; i <= m ; i += lowbit( i ) ) bit[i] += p; } inline int find( int k ) { register int ans = 0, cnt = 0;// ans 是答案 , cnt 是小于等于 ans 的数有多少个 for( register int i = 20 ; i >= 0 ; i -- ) { ans += ( 1 << i ); if( ans > m || cnt + bit[ ans ] >= k ) ans -= ( 1 << i ); else cnt += bit[ ans ]; } return ans + 1; } int main() { m = n = read(); for( register int i = 1 ; i <= n ; i ++ ) a[i] = b[i] = read(); sort( a + 1 , a + 1 + n ); m = unique( a + 1 , a + 1 + m) - a - 1;//去重 for( register int i = 1 ; i <= n ; i ++ ) b[i] = lower_bound( a + 1 , a + 1 + m , b[i] ) - a; //离散化 for( register int i = 1 ; i <= n ; i ++ ) { add( b[i] , 1 ); if( i & 1 ) printf( "%d\n" , a[find( ( i + 1 ) >> 1 )] ); } return 0; }
线段树(英语:\(Segment\ tree\))是一种二叉树形数据结构,\(1977\)年由\(Jon Louis Bentley\)发明,用以存储区间或线段,而且容许快速查询结构内包含某一点的全部区间。
一个包含 \({\displaystyle n}\)个区间的线段树,空间复杂度为 \({\displaystyle O(n)}\),查询的时间复杂度则为 ${\displaystyle O(\log n+k)} $,其中 \({\displaystyle k}\)是匹配条件的区间数量。
此数据结构亦可推广到高维度
struct Node { int l , r , value , tag; Node * left , * right; // 左右子树 Node (int s , int t , int v , Node * a , Node * b) { l = s; r = t; value = v; tag = 0; left = a; right = b; } } * root; //根节点 Node * build(int l,int r) { if(l == r) return new Node( l , r , a[l] , 0 , 0 );//叶子节点 register int mid = ( l + r ) >> 1; Node * left = build( l , mid), * right = build( mid+1 , r ); // 递归构建左右子树 return new Node( l , r , left -> value + right -> value , left , right); // 创建当前节点 }
int find( int l , int r , Node * cur) { if(l == cur -> l && r == cur -> r) return cur -> value; // 彻底包含 int mid = ( cur -> l + cur -> r ) >> 1; if( l > mid ) return find( l , r, cur -> right); // 所有在右子树 if(mid >= r) return find( l , r , cur -> left); // 所有在左子树 return find( l , mid , cur -> left) + find( mid + 1 , r , cur -> right ); // 区间跨越mid } void modify( int x , int v , Node * cur) { if(cur -> l == cur -> r ) cur -> value += v; // 叶子节点 else { int mid = (cur -> l + cur -> r ) >> 1; modify(x , v , x > mid ? cur -> right : cur -> left); cur -> value = cur -> left -> value + cur -> right -> value; } }
单点修改只要将\(l == r\)便可,因此很少作介绍
区间快速修改有两种方法
考虑在每一个节点维护一个标记tag,并执行如下操做
inline void mark(int v,Node * cur) { cur -> tag += v; cur -> value += (cur -> r - cur -> l + 1) * v; return ; } inline void pushdown( Node * cur) { if(cur -> tag == 0) return ; if(cur -> left) { mark(cur -> tag,cur -> left); mark(cur -> tag,cur -> right); } else cur -> value += cur -> tag; cur -> tag = 0; return ; } inline int query( int l , int r , Node * cur) { if(l <= cur -> l && cur -> r <= r ) return cur -> value; register int mid = (cur -> l + cur -> r) >> 1 , res = 0; pushdown( cur ); if( l <= mid ) res += query( l , r , cur -> left ); if( mid + 1 <= r) res += query( l , r , cur -> right); return res; } void extent_modify( int l , int r , int v , Node * cur) // [l,r] + v { if(cur -> l > r || cur -> r < l) return ; if(l <= cur -> l && cur -> r <= r) { mark(v,cur); return ; } pushdown( cur ); register int mid = (cur -> l + cur -> r) >> 1; if(l <= mid) extent_modify( l , r , v , cur -> left); if(mid + 1 <= r) extent_modify( l , r , v , cur -> right); cur -> value = cur -> left -> value + cur -> right -> value; return ; }
与基础的线段树操做很像,咱们额外的维护三个值\(dat,ldat,rdat\)分别表明整个区间的最大子段和、当前区间从作左端点开始的最大子段和,从右端点开始的最大子段和
考虑如何更新当前结点
inline void update( Node * cur ) { cur -> sum = cur -> left -> sum + cur -> right -> sum; //更新sum cur -> ldat = max( cur -> left -> ldat , cur -> left -> sum + cur -> right -> ldat ); //从左起的最大子段多是 左区间的最大子段 或 左区间的和加右区间的最大子段 cur -> rdat = max( cur -> right -> rdat , cur -> right -> sum + cur -> left -> rdat ); //相似上面 cur -> dat = max( max( cur -> left -> dat , cur -> right -> dat ) , max( max( cur -> ldat , cur -> rdat ) , cur -> left -> rdat + cur -> right -> ldat ) ) ; //当前区间的的最大子段和要么是 左右区间的的最大子段和,要么是中间的最大子段和,要么是左右端点开始的最大子段和 }
也就是说线段是不止能维护区间和,实际上只要是知足结合律的均可以用线段树来维护,区间和,区间最值,区间异或和等
#include <bits/stdc++.h> using namespace std; const int N = 500005 , INF = 0x7f7f7f7f; int n , a[N] ; struct Node { int l , r , sum , dat , ldat , rdat ; Node * right , * left; Node( int a , int b , int c , int d , int e , int f , Node * g , Node * h ) { l = a , r = b , sum = c , dat = d , ldat = e , rdat = r , left = g , right = h ; } } * root ; inline int read() { register int x = 0 , f = 1 ; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = - 1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f; } inline void update( Node * cur ) { cur -> sum = cur -> left -> sum + cur -> right -> sum; cur -> ldat = max( cur -> left -> ldat , cur -> left -> sum + cur -> right -> ldat ); cur -> rdat = max( cur -> right -> rdat , cur -> right -> sum + cur -> left -> rdat ); cur -> dat = max( max( cur -> left -> dat , cur -> right -> dat ) , max( max( cur -> ldat , cur -> rdat ) , cur -> left -> rdat + cur -> right -> ldat ) ) ; } inline Node * build( int l , int r ) { Node * cur = new Node( l , r , 0 , 0 , 0 , 0 , NULL , NULL ); if( l == r ) { cur -> ldat = cur -> rdat = cur -> sum = cur -> dat = a[l]; return cur ; } register int mid = ( l + r ) >> 1; cur -> left = build( l , mid ); cur -> right = build( mid + 1 , r); update( cur ); return cur; } inline Node * query( int l , int r , Node * cur ) { if( l <= cur -> l && cur -> r <= r ) return cur; register int mid = ( cur-> l + cur -> r ) >> 1; if( r <= mid ) return query( l , r , cur -> left ); if( l > mid ) return query( l , r , cur -> right ); Node *res = new Node( l , r , 0 , 0 , 0 , 0 , 0 , 0 ); Node * L = query( l , r , cur -> left ) , * R = query( l , r , cur -> right ); res -> sum = L -> sum + R -> sum; res -> ldat = max( L -> ldat , L -> sum + R -> ldat ); res -> rdat = max( R-> rdat , R -> sum + L -> rdat ); res -> dat = max( max( L -> dat , R -> dat ) , max( max( res -> ldat , res -> rdat ) , L -> rdat + R -> ldat ) ); return res; } inline void change( int x , int w , Node * cur ) { if( cur -> r == x && x == cur -> l ) { cur -> sum = cur -> dat = cur ->ldat = cur -> rdat = w; return ; } register int mid = ( cur -> r + cur -> l ) >> 1; if( x <= mid ) change( x , w , cur -> left ); if( x > mid ) change( x , w , cur -> right ); update( cur ); return ; } int main() { n = read() ; for( register int i = 1 ; i <= n ; i ++ ) a[i] = read(); root = build( 1 , n ); for( register int m = read() , op , x , y ; m >= 1 ; m -- ) { op = read() , x = read() , y = read(); if( op ) printf( "%d\n" , query( x , y , root ) -> dat ); else change( x , y ,root ); } return 0; }
这道题用了树状数组的经常使用操做,说白了就是区间修改,单点查询
这道题须要实现的的功能线段树也能够,可是用过代码对比和实际测试,线段树过不了,而且代码很长
因此经过这道题能够得知,若是能够用树状数组的话就不要用线段树
树状数组
#include <bits/stdc++.h> #define lowbit( x ) ( x & -x ) using namespace std; const int N = 1e7 + 5; int n , m , l , r , op , bit[N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int v ) { for( register int i = x ; i <= n ; i += lowbit(i) ) bit[i] += v; return ; } inline int find( int x ) { register int sum = 0; for( register int i = x ; i ; i -= lowbit(i) ) sum += bit[i]; return sum; } int main() { n = read() , m = read(); for( register int i = 1 ; i <= m ; i ++ ) { op = read(); if( op ) printf( "%d\n" , find( read() ) ); else add( read() , 1 ) , add( read() + 1 , -1 ); } return 0; }
线段树
#include <bits/stdc++.h> using namespace std; int n , m ; struct Node { int l , r , value , tag; Node * left , * right; Node( int s , int t , int w , Node * a , Node * b ) { l = s , r = t , value = w , tag = 0; left = a , right = b; } } *root; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline Node * build( int l , int r ) { if( l == r ) return new Node( l , r , 0 , 0 , 0 ); register int mid = ( l + r ) >> 1; Node * left = build( l , mid ) , * right = build( mid + 1 , r ); return new Node( l , r , 0 , left , right ); } inline void mark( int w , Node * cur ) { cur -> tag += w , cur -> value += ( cur -> r - cur -> l + 1 ) * w; } inline void pushdown( Node * cur ) { if( cur -> tag == 0 ) return ; if( cur -> left ) mark( cur -> tag , cur -> left ) , mark( cur -> tag , cur -> right ); else cur -> value += cur -> tag; cur -> tag = 0; return ; } inline int query( int l , int r , Node * cur ) { if( l <= cur -> l && cur -> r <= r ) return cur -> value; register int mid = ( cur -> l + cur -> r ) >> 1 , res = 0; pushdown( cur ); if( l <= mid ) res += query( l , r , cur -> left ); if( mid + 1 <= r ) res += query( l , r , cur -> right ); return res; } inline void modify( int l , int r , int w , Node * cur ) { if( cur -> l > r || cur -> r < l) return ; if( l <= cur-> l && cur -> r <= r ) { mark( w , cur ); return ; } register int mid = ( cur -> l + cur -> r ) >> 1; if( l <= mid ) modify( l , r , w , cur -> left ); if( mid + 1 <= r ) modify( l , r , w , cur -> right); cur -> value = cur -> left -> value + cur -> right -> value; return ; } int main() { n = read() , m = read(); root = build( 1 , n ); for( register int i = 1 ; i <= m ; i ++ ) { register int op = read(); if( !op ) { register int x = read() , y = read(); modify( x , y , 1 , root ); } else { register int x = read(); printf( "%d\n" , query( x , x , root ) ); } } return 0; }
本节部份内容选自 oi-wiki
其实,分块是一种思想,而不是一种数据结构。
从 NOIP 到 NOI 到 IOI,各类难度的分块思想都有出现。
一般的分块算法的复杂度带根号,或者其余奇怪的复杂度,而不是 \(\log\) 。
分块是一种很灵活的思想,几乎什么都能分块,而且不难实现。
你想写出什么数据结构就有什么,缺点是渐进意义的复杂度不够好。
固然,在 \(n=10^5\) 时,因为常数小,跟线段树可能差很少。
这不是建议大家用分块的意思,在 OI 中,能够做为一个备用方案,首选确定是线段树等高级的数据结构。
如下经过几个例子来介绍~
动机:线段树太难写?
将序列分段,每段长度 \(T\) ,那么一共有 \(\frac{n}{T}\) 段。
维护每一段的区间和。
单点修改:显然。
区间询问:会涉及一些完整的段,和最多两个段的一部分。
完整段使用维护的信息,一部分暴力求。
复杂度 \(O(\frac{n}{T}+T)\) 。
区间修改:一样涉及这些东西,使用打标记和暴力修改,一样的复杂度。
当 \(T=\sqrt{n}\) 时,复杂度 \(O(\sqrt{n})\) 。
上一个作法的复杂度是 \(\Omega(1) , O(\sqrt{n})\) 。
咱们在这里介绍一种 \(O(\sqrt{n}) - O(1)\) 的算法。
为了 \(O(1)\) 询问,咱们能够维护各类前缀和。
然而在有修改的状况下,不方便维护,只能维护单个块内的前缀和。
以及整块做为一个单位的前缀和。
每次修改 \(O(T+\frac{n}{T})\) 。
询问:涉及三部分,每部分均可以直接经过前缀和获得,时间复杂度 \(O(1)\) 。
一样的问题,如今序列长度为 \(n\) ,有 \(m\) 个操做。
若是操做数量比较少,咱们能够把操做记下来,在询问的时候加上这些操做的影响。
假设最多记录 \(T\) 个操做,则修改 \(O(1)\) ,询问 \(O(T)\) 。
\(T\) 个操做以后,从新计算前缀和, \(O(n)\) 。
总复杂度: \(O(mT+n\frac{m}{T})\) 。
\(T=\sqrt{n}\) 时,总复杂度 \(O(m \sqrt{n})\) 。
咱们用以个相似懒惰标记的东西来维护,对于整块咱们只修改标记,对于散块暴力修改便可
#include <bits/stdc++.h> using namespace std; const int N = 50005 , M = 250; int n , len , tot , a[N] , tag[M] , pos[N] , lef[M] , rig[M]; inline int read() { register int x = 0 , f = 1; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x * f; } inline void add( int l , int r , int val ) { if( pos[l] == pos[r] ) { for( register int i = l ; i <= r ; a[i] += val , i ++ ); return ; } for( register int i = l ; i <= rig[ pos[l] ] ; a[i] += val , i ++ ); for( register int i = r ; i >= lef[ pos[r] ] ; a[i] += val , i -- ); for( register int i = pos[l] + 1 ; i <= pos[r] - 1 ; tag[i] += val , i ++ ); return ; } int main() { n = read() , len = sqrt( 1.0 * n ) , tot = n / len + ( n % len ? 1 : 0 ); for( register int i = 1 ; i <= n ; a[i] = read() , pos[i] = ( i - 1 ) / len + 1 , i ++ ); for( register int i = 1 ; i <= tot ; lef[i] = ( i - 1 ) * len + 1 , rig[i] = i * len , i ++ ); for( register int i = 1 , opt , l , r , val ; i <= n ; i ++ ) { opt = read() , l = read() , r = read() , val = read(); if( opt ) printf( "%d\n" , a[r] + tag[ pos[r] ] ); else add( l , r , val ); } return 0; }
开一个vector
储存每个块内的元素,而后排序
若是修改包含当前整个块在不会改变块内元素的相对大小,只修改标记
若是没有可以包含整个块,就暴力修改,而后从新排序便可
查询时,对于散块直接暴力扫一遍,整块的话二分查找
#include <bits/stdc++.h> #define L( x ) ( ( x - 1 ) * len + 1 ) #define R( x ) ( x * len ) #define pb( x ) push_back( x ) using namespace std; const int N = 50005 , M = 250; int n , a[N] , len , tag[M] , pos[N]; vector< int > group[M]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void reset( int x ) { group[x].clear(); for( register int i = L( x ) ; i <= R( x ) ; i ++ ) group[x].pb( a[i] ); sort( group[x].begin() , group[x].end() ); return ; } int query(int l,int r,int val) { register int res = 0 , p = pos[l] , q = pos[r]; if( p == q ) { for( register int i = l ; i <= r ; i ++ ) { if( a[i] + tag[p] < val ) res ++; } return res; } for( register int i = l ; i <= R( p ) ; i ++ ) { if( a[i] + tag[p] < val ) res ++; } for( register int i = r ; i >= L( q ) ; i -- ) { if( a[i] + tag[q] < val ) res ++; } for( register int i = p + 1 ; i <= q - 1 ; i ++ ) res += lower_bound( group[i].begin() , group[i].end() , val - tag[i] ) - group[i].begin(); return res; } inline void modify( int l , int r , int val ) { register int p = pos[l] , q = pos[r]; if( p == q ) { for( register int i = l ; i <= r ; a[i] += val , i ++ ); reset( p ); return ; } for( register int i = l ; i <= R( p ) ; a[i] += val , i ++ ); for( register int i = r ; i >= L( q ) ; a[i] += val , i -- ); reset( p ) , reset( q ); for( register int i = p + 1 ; i <= q - 1 ; tag[i] += val , i ++ ); return ; } int main() { n = read() , len = sqrt( 1.0 * n ); for( register int i = 1 ; i <= n ; a[i] = read() , group[ pos[i] = ( i - 1 ) / len + 1 ].pb( a[i] ) , i ++ ); for( register int i = 1 ; i <= pos[n] ; sort( group[i].begin() , group[i].end() ) , i ++ ); for( register int i = 1 , opt , l , r , val ; i <= n ; i ++ ) { opt = read() , l = read() , r = read() , val = read(); if( opt ) printf( "%d\n" , query( l , r , val * val ) ); else modify( l , r , val ); } return 0; }
经过这两道,咱们能够以总结下怎么思考一个分块
这道题的作法和第二题比较相似
分块,块内排序,二分查找,块外暴力扫
对于这道题你会发现若是直接把分红\(\sqrt{n}\)块的话会\(T\)掉一部分点
因此咱们这这里引入均值不等式\(\sqrt{xy} \le \frac{1}{2}(x+y)\),当且仅当\(x=y\)时,\(\sqrt{xy} = \frac{1}{2}(x+y)\)
假设序列长度为\(n\),块的大小为\(x\),天然有\(y=\frac{n}{x}\)块,假设每次操做的复杂度为\(Ax+By\)
则根据均值不等式能够获得$Ax+By \ge 2\sqrt{ABn} $
因此可知
\[ Ax=By\Rightarrow x=\frac{By}{A}=\frac{Bn}{Ax}\Rightarrow x^2=\frac{B}{A}n\Rightarrow x=\sqrt{\frac{B}{A}n} \]
根据上面的推到可知当\(x=\sqrt{\frac{B}{A}n}\)时复杂度最低,最低为\(O(2\sqrt{ABn})\)
对于本题每次操做是\(O(x+log_ny)\)的,天然可得\(x=\sqrt{nlog_n}\approx 700\)
可是因为咱们在计算复杂度时只考虑数量级,不免会有偏差
因此咱们能够用两个仅仅时块的大小不同的程序对拍,比较时间找出比较快的分块大小
#include <bits/stdc++.h> #define L( x ) ( ( x - 1 ) * len + 1 ) #define R( x ) ( x * len ) using namespace std; const int N = 1e5+5 , M = 1e3 + 5 ,INF = - 1 << 31; int n , len , a[N] , pos[N] , tag[M]; set< int > group[M]; inline int read() { register int x = 0 , f = 1; register char ch = getchar(); while( ch < '0' || ch > '9' ) { if( ch == '-' ) f = -1; ch = getchar(); } while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline int query( int l , int r , int val ) { register int res = -1 , p = pos[l] , q = pos[r]; if( p == q ) { for( register int i = l ; i <= r ; i ++ ) { if( a[i] + tag[ q ] < val ) res = max( res , a[i] + tag[ q ] ); } return ( res != INF ? res : - 1 ); } for( register int i = l ; i <= R( p ) ; i ++ ) { if( a[i] + tag[ p ] < val ) res = max( res , a[i] + tag[p] ); } for( register int i = r ; i >= L( q ) ; i -- ) { if( a[i] + tag[ q ] < val ) res = max( res , a[i] + tag[q] ); } for( register int i = p + 1 ; i <= q - 1 ; i ++ ) { auto t = group[i].lower_bound( val - tag[i] ); if( t == group[i].begin() ) continue; t -- ; res = max( res , *t + tag[i] ); } return res; } inline void modify( int l , int r , int val ) { register int p = pos[l] , q = pos[r]; if( p == q ) { for( register int i = l ; i <= r ; group[p].erase(a[i]) , a[i] += val , group[p].insert(a[i]) , i ++ ) ; return ; } for( register int i = l ; i <= R( p ) ; group[p].erase(a[i]) , a[i] += val , group[p].insert(a[i]) , i ++ ); for( register int i = r ; i >= L( q ) ; group[q].erase(a[i]) , a[i] += val , group[q].insert(a[i]) , i -- ); for( register int i = p + 1 ; i <= q - 1 ; tag[i] += val , i ++ ); return ; } int main() { n = read() , len = 1000; for( register int i = 1 ; i <= n ; a[i] = read() , pos[i] = ( i - 1 ) / len + 1 , group[ pos[i] ].insert( a[i] ) , i ++ ); for( register int i = 1 , opt , l , r , val ; i <= n ; i ++ ) { opt = read() , l = read() , r = read() , val = read(); if( opt )printf("%d\n" , query( l , r , val ) ); else modify( l , r , val ); } return 0; }
没错,这是一道线段树模板题,可是不妨来用线段树作一下是能够的
#include <bits/stdc++.h> #define LL long long #define L( x ) ( ( x - 1 ) * len + 1 ) #define R( x ) ( x * len ) using namespace std; const int N = 1e5 + 5 , M = 1e3 + 5 ; int n , m , len , tot , pos[N] , lef[M] , rig[M]; LL a[N] , sum[N] , tag[M]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline LL query( int l , int r ) { register LL res = 0; if( pos[l] == pos[r] )//若是在一个块中 { for( register int i = l ; i <= r ; res += a[i] + tag[ pos[i] ] , i ++ ); return res; } for( register int i = l ; i <= rig[ pos[l] ] ; res += a[i] + tag[ pos[i] ] , i ++ );//散块 for( register int i = r ; i >= lef[ pos[r] ] ; res += a[i] + tag[ pos[i] ] , i -- ); for( register int i = pos[l] + 1 ; i <= pos[r] - 1 ; res += sum[i] + tag[i] * ( rig[i] - lef[i] + 1 ) , i ++ );//整块 return res; } inline void modify( int l , int r , LL val ) { for( register int i = l ; i <= min( r , rig[ pos[l] ] ) ; a[i] += val , sum[ pos[i] ] += val , i ++ );//散块 for( register int i = r ; i >= max( l , lef[ pos[r] ] ) ; a[i] += val , sum[ pos[r] ] += val , i -- ); for( register int i = pos[l] + 1 ; i <= pos[r] - 1 ; tag[i] += val , i ++ );//整块 } int main() { n = read() , m = read(); len = sqrt( 1.0 * n ) , tot = n / len; if( n % len ) tot ++; for( register int i = 1 ; i <= n ; a[i] = read() , pos[i] = ( i - 1 ) / len + 1 , sum[ pos[i] ] += a[i] , i ++) for( register int i = 1 ; i <= tot ; lef[i] = ( i - 1 ) * len + 1 , rig[i] = i * len , i ++ ); for( register int opt , l , r , val ; m >= 1 ; m -- ) { opt = read() , l = read() , r = read(); if( opt & 1 ) val = read() , modify( l , r , val ); else printf( "%lld\n" , query( l , r ) ); } return 0; }
请忽视我分块开了\(O2\),能够注意到,分块不只代码短并且跑得并不慢
不过这道题貌似没有构造数据