根号

简单的根号相关问题
(根号分治 定期重构 分块 莫队)

一. 根号分治

精髓就是拼接两个暴力

1. 余数根号分治:Remainder Problem

首先直接单点更新O(1),查询暴力跳O(n)。这个暴力的有点在于当查询的x比较大的时候,那么跳的次数就比较少。
另一种暴力思路就是维护c[i][j]为%i = j的下标的数的和,那么更新就是O(n)的,查询则变成O(1)的了。
我们考虑分治 设阈值为B。
当x<=B时用暴力2 O(B) O(1)
x > B时用暴力1 O(1) O(n/B)
B = sqrt(n)时 B = n / B

if(op == 1)
{
	rep(i, 1, B) c[i][x % i] += y;
	a[x] += y;
}
else
{
	if(x <= B) cout << c[x][y] << '\n';
	else
	{
		int res = 0;
		for(int i = y; i <= 500000; i += x) res += a[i];
		cout << res << '\n';
	}
}

小变化

给定一个长度为 ( n ) 的序列 ( x )(元素编号从 ( 1 ) 开始),所有元素初始值为 ( 0 )。接下来进行 ( q ) 次操作,每次操作分为以下两种类型(操作含参数 ( a, b )):

设有长度为 n 的序列 \(x = (x_1, x_2, \dots, x_n)\),初始时满足全0

接下来进行 q 次操作,每次操作分为以下两类之一:

  1. 更新操作:给定 a, b,对所有满足 \(a \cdot i \le n\) 的正整数 \(i\),执行
    \(x_{a \cdot i} \leftarrow x_{a \cdot i} + b\).

  2. 查询操作:给定 \(a, b \; (a \le b)\),输出
    \(\sum_{i=a}^{b} x_i\).
    暴力1,直接跳着修改然后BIT查询,复杂度\(O(log(n) \times \sqrt{n})\)
    暴力2,对于每个修改直接记录tag[a]加了多少,查询时在区间内查看a的倍数有几个在区间内即为贡献 \(O(B)\)
    然后就可以对于a<=B用2,a>B用

分块

比线段树能解决的问题更多但是复杂度更。
[0, B) [B, 2B) [2B, 3B) ... [iB, (i+1)B)
区间tag + 自身信息 = 真实信息

inline int get(int i)
{
    return i / len;
}
void modify(int l, int r, int d)
{
    int x = get(l), y = get(r);
    if (x == y)for (int i = l; i <= r; i++)a[i] += d, sum[get(i)] += d; // 同一块内
    else // 跨块
    {
        for (int i = l; i < (x + 1) * len; i++)a[i] += d, sum[get(i)] += d;
        for (int i = x + 1; i < y; i++)add[i] += d, sum[i] += 1ll * d * len;
        for (int i = y * len; i <= r; i++)a[i] += d, sum[get(i)] += d;
    }
}
ll query(int l, int r)
{
    ll ans = 0;
    int x = get(l), y = get(r);
    if (x == y)for (int i = l; i <= r; i++)ans += a[i] + add[get(i)];
    else
    {
        for (int i = l; i < (x + 1) * len; i++) ans += a[i] + add[get(i)];
        for (int i = x + 1; i < y; i++) ans += sum[i];
        for (int i = y * len; i <= r; i++) ans += a[i] + add[get(i)];
    }
    return ans;
}

分块最正经的写法

线段树不太能做的 教主的魔法
[1, B] [B+1, 2B] [2B+1, 3B] ... [kB+1, n]
注意最后是n
对于每一块如果他们同时加上k,那么他们的相对大小不变。
对于块内部分元素的加,则会打乱相对大小所以需要重新排序。

#include <bits/stdc++.h>
#define ls cur << 1
#define rs cur << 1 | 1
#define repf(i, a, b) for(long long i = (a); i >= (b); i -- )
#define rep(i, a, b) for(long long i = (a); i <= (b); i ++ )
typedef long long ll;
using namespace std;
const int N = 1e6 + 10;
const int M = 2e6 + 10;
int a[N], tag[1010];
int b[N]; // sort
int B;
int L[1010], R[1010], cnt;
int get(int x)
{
	return (x - 1) / B + 1;
}
void xg(int l, int r, int val)
{
	rep(i, l, r) a[i] += val;
	int c = get(l);
	memcpy(b + L[c], a + L[c], (R[c] - L[c] + 1) * sizeof(int));
	sort(b + L[c], b + R[c] + 1);
}
void modify(int l, int r, int val)
{
	int x = get(l), y = get(r);
	if(x == y) xg(l, r, val);
	else
	{
		xg(l, R[x], val);
		xg(L[y], r, val);
		rep(i, x + 1, y - 1) tag[i] += val;
	}
}
int qs(int l, int r, int val)
{
	int k = val - tag[get(l)];
	int res = 0;
	rep(i, l, r) if(a[i] >= k) res ++;
	return res;
}
int query(int l, int r, int val)
{
	int x = get(l), y = get(r);
	if(x == y) return qs(l, r, val);
	int ans = 0;
	ans += qs(l, R[x], val);
	ans += qs(L[y], r, val);
	rep(i, x + 1, y - 1)
	{
		int LL = L[i], RR = R[i], best = -1;
		int k = val - tag[i];
		while(LL <= RR)
		{
			int mid = LL + RR >> 1;
			if(b[mid] >= k) best = mid, RR = mid - 1;
			else LL = mid + 1;
		}
		if(best == -1) continue;
		ans += R[i] - best + 1;
	}
	return ans;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int n, q; cin >> n >> q;
	rep(i, 1, n) cin >> a[i], b[i] = a[i];
	B = sqrt(n);
	cnt = get(n);
	rep(i, 1, cnt)
	{
		L[i] = (i - 1) * B + 1;
		R[i] = i * B;
		if(i == cnt) R[i] = n;
		sort(b + L[i], b + R[i] + 1);
	}
	while( q -- )
	{
		char op; cin >> op;
		int l, r, val; cin >> l >> r >> val;
		if(op == 'M') modify(l, r, val);
		else cout << query(l, r, val) << '\n';
	}
	return 0;
}

分块 (序列分块 操作分块(对时间轴分块技巧))

数列分块入门 1 太简单不写了

数列分块入门 2 与教主的魔法完全相同

数列分块入门 3 区间加法,询问区间内某个值 x 的前驱(比其小的最大元素)

查询可以拆到整块和散块上 (对于每个块查一个前驱,散块也是再求最大值即可)
查排名和前驱后继是等难的问题 处理方式几乎相同 处理方法也类似2

数列分块入门 4 区间加 区间求和 简单

数列分块入门 5 区间开方 区间求和

类似势能线段树,每个数被开方的次数<= 6次。 V < 2^32。 所以当区间最大值 > 1时暴力开方即可。
一个较为精确的上界是 loglogV,因为每一次开根都是在指数上 ÷ 2。

数列分块入门 6 定期重构

先想一个暴力,插入分块后O(块的个数)的代价定位,然后再O(块长)插入。
这样有一个问题就是如果往一个块内一直插入,那么O(块长)插入就变得很慢 退化至平方。
所以考虑插入到一定数量(2*B)后 重新分块(代价就是O(n))。重构的次数不会很多,都往一个里面插\(O(\frac{q}{B} \times n)\)

定期重构

数列分块6
与块状链表相似

posted @ 2025-09-26 20:07  闫柏军  阅读(11)  评论(0)    收藏  举报