0x41 并查集
定义
在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。有一个联合-查找算法定义了两个用于此数据结构的操作:
- \(Find\):确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- \(Union\):将两个子集合并成同一个集合。
由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构或合并-查找集合。其他的重要方法,\(MakeSet\),用于创建单元素集合。有了这些方法,许多经典的划分问题可以被解决。
为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,\(Find(x)\) 返回 \(x\) 所属集合的代表,而 $Union $使用两个集合的代表作为参数。
路径压缩与按秩合并
这是两个并查集常用的优化
当我们在寻找祖先时,一旦元素多且来,并查集就会退化成单次\(O(n)\)的算法,为了解决这一问题我们可以在寻找祖先的过程中直接将子节点连在祖先上,这样可以大大降低复杂度,均摊复杂度是\(O(log(n))\)的
按秩合并也是常见的优化方法,“秩”的定义很广泛,举个例子,在不路径压缩的情况下,常见的情况是把子树的深度定义为秩
无论如何定义通常情况是把“秩”储存在根节点,合并的过程中把秩小的根节点插到根大的根节点上,这样可以减少操作的次数
特别的,如果把秩定义为集合的大小,那么采用了按秩合并的并查集又称“启发式并查集”
按秩合并的均摊复杂度是\(O(log(n))\)的,如果同时采用按秩合并和路径压缩均摊复杂度是\(O(\alpha(n) )\),\(\alpha(n)\)是反阿克曼函数
\[ \forall n \le 2^{10^{19729}},\alpha(n)\le 5 \]
可以视为均摊复杂度为\(O(1)\)
不过通常情况下我们仅采用路径压缩即可
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 ;
}
NOI2015 程序自动分析
虽然是\(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]\)
所以时间复杂度可能会略高,但没有具体证明,因为这个算法是一天中午我自己琢磨出来的,且没有在网上找严格的证明
NOI2002 银河英雄传说
这就是到带权并查集的模板,所以在没有在上面放代码
可以直接琢磨下这个代码
#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;
}
扩展域并查集
扩展域并查集就是将并查集的区域大小扩展成整数倍,用多个区域来同时维护多个传递关系
AcWing 239. 奇偶游戏
这是一道经典的扩展域并查集
首先我们用一个\(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;
}
NOI2001 食物链
经典的扩展域并查集,我们可以开三个域,同类,食物,天敌,来维护这样一个集合
同样为了方便表示,分别用\(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 奶酪
这道题是\(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;
}
0x42 树状数组
树状数组这里就不讲原理了,给张图自己理解即可
注意树状数组维护的数组下标必须是\(1\cdots n\),如果有\(0\)就会死循环
Loj 130. 树状数组 1 :单点修改,区间查询
模板题直接看代码即可
#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;
}
Loj 10116. 清点人数
模板题,没啥好解释的,之前看模板就能写出来
Loj 131. 树状数组 2 :区间修改,单点查询
区间修改也是树状数组的经典操作,简单来说就是维护一个差分序列
#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;
}
Loj 10117.简单题
还是到模板题,直接套板子吧
Loj 132. 树状数组 3 :区间修改,区间查询
联系上一道题,我们假设原数组是\(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;
}
Loj 10114.数星星 Stars
根据树状数组的性质我们可以快速的求出前\(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;
}
Loj 10115.校门外的树
我们把每次种树抽象成一个线段,同时开两个树状数组分别维护每条线段的两个端点
插入时在\(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;
}
Luogu P1908 逆序对
逆序对是树状数组的经典操作,其实相当于用树状数组维护了一个桶
#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;
}
Loj 3195. 「eJOI2019」异或橙子
首先先来两个引理
\[ 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;
}
Luogu P1168 中位数
考虑如何用树状数组做
我们可以依次插入每个数
比如我们要插入\(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;
}
0x43 线段树
线段树(英语:\(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 ;
}
Luogu SP1716 GSS3 - Can you answer these queries III
与基础的线段树操作很像,我们额外的维护三个值\(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;
}
Luogu P4939 Agent2
这道题用了树状数组的常用操作,说白了就是区间修改,单点查询
这道题需要实现的的功能线段树也可以,但是用过代码对比和实际测试,线段树过不了,并且代码很长
所以通过这道题可以得知,如果可以用树状数组的话就不要用线段树
树状数组
#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;
}
0x44 分块
本节部分内容选自 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})\) 。
区间和 2
上一个做法的复杂度是 \(\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})\) 。
Loj 6277. 数列分块入门 1
我们用以个类似懒惰标记的东西来维护,对于整块我们只修改标记,对于散块暴力修改即可
#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;
}
Loj 6278. 数列分块入门 2
开一个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;
}
通过这两道,我们可以以总结下怎么思考一个分块
- 不完整的块怎么处理
- 完整的块怎么处理
- 预处理什么信息
Loj 6279. 数列分块入门 3
这道题的做法和第二题比较类似
分块,块内排序,二分查找,块外暴力扫
对于这道题你会发现如果直接把分成\(\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;
}
P3372 【模板】线段树 1
没错,这是一道线段树模板题,但是不妨来用线段树做一下是可以的
#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\),可以注意到,分块不仅代码短而且跑得并不慢
不过这道题貌似没有构造数据