数列分块:
数列分块的思想就是通过把数列分成块状,整体上通过块来对数列进行操作,同时在做有些操作的时候有一点lazy标记的思想,而且一般都是多定义数组来解决不同的功能。这个和线段树解决的问题差不多,但是不得不承认在有些问题的解决上线段树的代码两确实太大了。
ST表:
算法的主题是倍增。常用来处理RMQ问题。一般开 a[ i ][ j ]表示从下标 i 开始,长度为 2^j的子数组最值是多少。步骤一般是 dp预处理 查询操作。ST表的复杂度为:预处理 O( NlogN); O( 1 ).具体见下面的模板题代码。
A - 数列分块入门 1
题目描述:给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,单点查值。第一行输入一个数字 n。第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。接下来输入 n 行询问,每行输入四个数字 opt、l、r、c,以空格隔开。若 opt=0,表示将位于 [l,r]的之间的数字都加 c。若 opt=1,表示询问 ar 的值(l 和 c 忽略)。
思路:就是个数列分块的板题目。
#include<iostream>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 5e4 + 10;
ll n, a[N], lazy[N], id[N], op, l, r, c;
void update( ll l, ll r, ll val ){
ll L = id[l], R = id[r];
if( L == R ){//如果是在一个块内,就直接暴力更新,因为5e4开方后也只有两百多,一般不会被T
for( ll i = l; i < r + 1; i++ ) a[i] += val;
return ;
}
// 分开来处理,分为零碎的左边,右边,和完整的中间块
for( ll i = l; id[i] == L; i++ ) a[i] += val;// 零碎的部分直接暴力
for( ll i = L + 1; i < R; i++ ) lazy[i] += val;// 对这整个区间进行lazy标记
for( ll i = r; id[i] == R; i-- ) a[i] += val;
}
ll query( ll r ){// 单点查询
return a[r] + lazy[id[r]];//下发lazy标记
}
int main(){
ios :: sync_with_stdio( false );
cin.tie( NULL );
cin >> n;
ll len = sqrt( n );
for( ll i = 1; i < n + 1; i++ ){
cin >> a[i];
id[i] = ( i - 1 ) / len + 1;// 这相当于是模板来定义块的下标
}
for( ll i = 0; i < n; i++ ){
cin >> op >> l >> r >> c;
if( op ) cout << query( r ) << "\n";
else update( l, r, c );
}
return 0;
}
C - 数列分块入门 4
给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,区间求和。第一行输入一个数字 n。第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。接下来输入 n 行询问,每行输入四个数字 opt、l、r、c,以空格隔开。若 opt=0,表示将位于 [l,r]的之间的数字都加 c。若 opt=1,表示询问 l到r 的值 mod( c + 1 )。
思路:本质就是 在上面的题的基础上通过 增加一个数组 来 新增一个功能。
// 当时错那么多还是因为对于数列分块没有真的了解意义,没有了解块lazy标记的含义
#include<iostream>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 5e4 + 10;
ll n, a[N], lazy[N], id[N], op, l, r, c, sum[N], len;
void update( ll l, ll r, ll val ){
ll L = id[l], R = id[r];
if( L == R ){
for( ll i = l; i < r + 1; i++ ){
a[i] += val;
sum[L] += val;// L块需要总和更新
}
return ;//注意要及时return,以及return的位置
}
for( ll i = l; id[i] == L; i++ ){
a[i] += val;
sum[L] += val;
}
for( ll i = L + 1; i < R; i++ ){
lazy[i] += val;// 注意只能对整块进行lazy标记。
}
for( ll i = r; id[i] == R; i-- ){
a[i] += val;
sum[R] += val;
}
}
ll query( ll l, ll r, ll MOD ){
ll ans = 0;
ll L = id[l], R = id[r];
if( L == R ){
for( ll i = l; i < r + 1; i++ ){
ans += ( a[i] + lazy[L] ) % MOD;
// 注意要下发lazy标记,因为我在做块更新操作的时候实际上没有对a数组进行更新操作,
//只是对lazy标记进行了更新,而对a数组没有影响。可能这次询问的就是前面的整块
ans %= MOD;
}
return ans % MOD;
}
for( ll i = l; id[i] == L; i++ ){
ans += ( a[i] + lazy[L] ) % MOD;
ans %= MOD;
}// 同理
for( ll i = L + 1; i < R; i++ ){
ans += ( sum[i] + lazy[i] * len ) % MOD;// 注意这里是sum
ans %= MOD;
}
for( ll i = r; id[i] == R; i-- ){
ans += ( a[i] + lazy[R] ) % MOD;
ans %= MOD;
} // 同理
return ans % MOD;
}
int main(){
ios :: sync_with_stdio( false );
cin.tie( NULL );
cin >> n;
len = sqrt( n );
for( ll i = 1; i < n + 1; i++ ){
cin >> a[i];
id[i] = ( i - 1 ) / len + 1;
sum[id[i]] += a[i];// 代表每个块的总和
}
for( ll i = 0; i < n; i++ ){
cin >> op >> l >> r >> c;
if( op ) cout << query( l, r, c + 1 ) << "\n"; //注意是mod( c + 1 );
else update( l, r, c );
}
return 0;
}
贴上错误代码
#include<iostream>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 5e4 + 10;
ll n, a[N], lazy[N], id[N], op, l, r, c, sum[N], len;
void update( ll l, ll r, ll val ){
ll L = id[l], R = id[r];
if( L == R ){
for( ll i = l; i < r + 1; i++ ){
a[i] += val;
sum[L] += val;
}
return ;
}
for( ll i = l; id[i] == L; i++ ){
a[i] += val;
sum[L] += val;
}
for( ll i = L + 1; i < R; i++ ){
lazy[i] += val;
sum[i] += val * len;
}
for( ll i = r; id[i] == R; i-- ){
a[i] += val;
sum[R] += val;
}
}
ll query( ll l, ll r, ll MOD ){
ll ans = 0;
ll L = id[l], R = id[r];
if( L == R ){
for( ll i = l; i < r + 1; i++ ){
ans += a[i];
}
return ans % MOD;
}
for( ll i = l; id[i] == L; i++ ){
ans += a[i];
}
for( ll i = L + 1; i < R; i++ ){
ans += sum[i];
}
for( ll i = r; id[i] == R; i-- ){
ans += a[i];
}
return ans % MOD;
}
int main(){
ios :: sync_with_stdio( false );
cin.tie( NULL );
cin >> n;
len = sqrt( n );
for( ll i = 1; i < n + 1; i++ ){
cin >> a[i];
id[i] = ( i - 1 ) / len + 1;
sum[id[i]] += a[i];
}
for( ll i = 0; i < n; i++ ){
cin >> op >> l >> r >> c;
if( op ) cout << query( l, r, c + 1 ) << "\n";
else update( l, r, c );
}
return 0;
}
B - 数列分块入门 2
题目描述:给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x的元素个数。第一行输入一个数字 n。第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。接下来输入 n 行询问,每行输入四个数字 opt、l、r、c,以空格隔开。若 opt=0,表示将位于 [l,r]的之间的数字都加 c。若 opt=1,表示询问 l到r 的值小于c^2数字的个数。
思路:这道题的难点在于操作1,计算满足条件的个数,暴力铁超时,这个时候我们采用 二分查找,但是要二分必须保证数组是有序的,我们不能在原数组上进行操作,所以要重新开一个数组,但是这个数组维护的是块区间内有序。
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 5e5 + 10;
ll n, a[N], lazy[N], id[N], op, l, r, c, len, b[N];
void reset( ll l, ll r ){
for( ll i = l; i < r + 1; i++ ){
b[i] = a[i];
}
sort( b + l, b + r + 1 );// 注意sort排序的特点是前闭后开,维护单个区间有序
}
void update( ll l, ll r, ll val ){
ll L = id[l], R = id[r];
if( L == R ){
for( ll i = l; i < r + 1; i++ ){
a[i] += val;
}
reset( ( L - 1 ) * len + 1, L * len );
// 注意放的位置,是加完再整体sort,不然会提高复杂度导致超时
return ;
}
for( ll i = l; id[i] == L; i++ ){
a[i] += val;
}
reset( ( L - 1 ) * len + 1, L * len );//同理
for( ll i = L + 1; i < R; i++ ){
lazy[i] += val;
}// 注意这是没必要的,因为都加上一个数,顺序不会改变
for( ll i = r; id[i] == R; i-- ){
a[i] += val;
}
reset( ( R - 1 ) * len + 1, R * len );//同理
}
ll query( ll l, ll r, ll val ){
ll cnt = 0;
ll L = id[l], R = id[r];
if( L == R ){
for( ll i = l; i < r + 1; i++ ){
if( ( a[i] + lazy[L] ) < val ) ++cnt;// 记得下发lazy
}
return cnt;
}
for( ll i = l; id[i] == L; i++ ){
if( ( a[i] + lazy[L] ) < val ) ++cnt;
}
for( ll i = L + 1; i < R; i++ ){
ll bl = ( i - 1 ) * len + 1, br = i * len;
cnt += ( lower_bound( b + bl, b + br + 1, val - lazy[i] ) - b - bl );// !!!
}
for( ll i = r; id[i] == R; i-- ){
if( ( a[i] + lazy[R] ) < val ) ++cnt;
}
return cnt;
}
int main(){
ios :: sync_with_stdio( false );
cin.tie( NULL );
cin >> n;
len = sqrt( n );
for( ll i = 1; i < n + 1; i++ ){
cin >> a[i];
id[i] = ( i - 1 ) / len + 1;
}
for( ll i = 1; i < id[n] + 1; i++ ){
reset( ( i - 1) * len + 1, i * len );
// 这里有计算块的第一个数下标,和最后一个数下标的方法,让块内有序
if( i == id[n] ){
reset( ( i - 1) * len + 1, n );
}//注意最后一个块可能长度和前面的不同意,所以,我们需要对最后一个块单独处理,
//虽然有一点点浪费前面的最后一次排序
}
for( ll i = 0; i < n; i++ ){
cin >> op >> l >> r >> c;
if( op ) cout << query( l, r, c * c) << "\n";
else update( l, r, c );
}
return 0;
}
D - Balanced Lineup
题目描述:FJ 的 N 头牛总是按同一序列排队。有一天,FJ 决定让一些牛玩一场飞盘比赛。他准备找一群在对列中为置连续的牛来进行比赛,但是为了避免水平悬殊,牛的身高不应该相差太大。FJ 准备了 Q 个可能的牛的选择和所有牛的身高。他想知道每一组里面最高和最低的牛的身高差别。第一行:N 和 Q; 第二至第 N+1 行,第 i+1 行是第 i 头牛的身高 hi; 第 N+2 至第 N+Q+1 行,每行两个整数 A 和 B,表示从 A 到 B 的所有牛。
思路:就是个模板题,但是要注意的是这个要同时求最大最小,所以我开了一个结构体,来存储最大最小值。
// 具体证明可以看看网上的其他博客
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 5e4 + 10;// 我也不知道为什么要开这么大???
struct node{
ll minn, maxx;
}a[N][20];
ll n, q, A, B;
void ini( ){
for( ll j = 1; j <= 20; j++ ){
// 放在前面是因为从转移方程式可知j要在i前面被推出来,长度由N定
for( ll i = 1; i + ( 1 << j ) - 1 < n + 1; i++ ){
a[i][j].minn = min( a[i][j - 1].minn, a[i + ( 1 << ( j - 1 ) )][j - 1].minn );
a[i][j].maxx = max( a[i][j - 1].maxx, a[i + ( 1 << ( j - 1 ) )][j - 1].maxx );
}
}
}
ll query( ll l, ll r ){
ll k = log2( r - l + 1 );
return max( a[l][k].maxx, a[r - ( 1 << k ) + 1][k].maxx ) - min( a[l][k].minn, a[r - ( 1 << k ) + 1][k].minn );
}
int main(){
ios :: sync_with_stdio( false );
cin.tie( NULL );
cin >> n >> q;
for( ll i = 1; i < n + 1; i++ ){
cin >> a[i][0].minn;
a[i][0].maxx = a[i][0].minn;// 初始化
}
ini( );
while( q-- ){
cin >> A >> B;
cout << query( A, B ) << "\n";
}
return 0;
}
总结:
其实线段是,ST表,数列分块,都是围绕区间开展,只是线段树书写起来很麻烦,在某些情况下后两者可以起到书写方便,理解简单的作用。特别是ST表,对于区间最值问题很方便,复杂度也比较好。