Loading

分块入门系列总结

数列分块入门 \(1\)

题目链接

区间加法、单点查值

显然,这是一道分块的入门模板。对于每一个整块,我们维护一个加法标记,表示这个块被整体加上的值。对于被操作的区间,我们将其拆分成若干整块和至多两个不完整块,对于不完整块中的元素直接进行操作,对于完整块则直接修改加法标记即可。每次查询时,直接返回原本数列中的元素与其所属块的加法标记即可。

#include <cstdio>
#include <cmath>
using namespace std;

const int maxn = 5e5 + 5;
const int maxm = 2e3 + 5;

int n;
int a[maxn], st[maxm], ed[maxm];
int bel[maxn], lazy[maxm];

inline int read() {
    int res = 0, flag = 1;
    char ch = getchar();

    while (ch < '0' || ch > '9') {
        if (ch == '-')
            flag = -1;

        ch = getchar();
    }

    while (ch >= '0' && ch <= '9') {
        res = res * 10 + ch - '0';
        ch = getchar();
    }

    return res * flag;
}

inline void update(int l, int r, int x) {
    if (bel[l] == bel[r])
        for (register int i = l; i <= r; i++)
            a[i] += x;
    else {
        for (register int i = l; i <= ed[bel[l]]; i++)
            a[i] += x;

        for (register int i = st[bel[r]]; i <= r; i++)
            a[i] += x;

        for (register int i = bel[l] + 1; i < bel[r]; i++)
            lazy[i] += x;
    }
}

signed main() {
    int opt, l, r, c;
    n = read();
    int block = sqrt(n);
    int size = ceil(n * 1.0 / block);

    for (register int i = 1; i <= size; i++) {
        st[i] = (i - 1) * block + 1;
        ed[i] = i * block;
    }

    ed[size] = n;

    for (register int i = 1; i <= n; i++) {
        a[i] = read();
        bel[i] = (i - 1) / block + 1;
    }

    for (register int i = 1; i <= n; i++) {
        opt = read();
        l = read();
        r = read();
        c = read();

        if (opt == 0)
            update(l, r, c);
        else
            printf("%d\n", a[r] + lazy[bel[r]]);
    }

    return 0;
}

数列分块入门 \(2\)

题目链接

区间加法、询问区间内小于某个值 \(x\) 的元素

这道题运用分块基本思想中的 二分 。假设现在有一个 升序序列 \(a\) ,要求出区间 \([l, r]\) 中小于值 \(x\) 的元素个数。显然,设第一个大于等于 \(x\) 的元素下标为 \(p\) ,第一个大于 \(x\) 的元素的下标为 \(q\) ,则 \(q - 1\) 就是最后一个等于 \(x\) 的元素的下标。所以等于 \(x\) 的元素个数为 \(q - 1 - p + 1\) ,也就是 \(q - p\)

由此,我们可以想到这道题的正解。同上一道题一样,对于每一个整块都维护一个加法标记。另外维护一个数组 \(b\) ,对于 \(b\) 来说,\(b\) 的元素与给定数组 \(a\) 的元素相同,每一个整块对应的区间都是有序的,但是整个数组不一定有序。

接着,区间加法操作按相同方法维护,区别在于在修改完序列 \(a\) 后,我们还需要把 \(a\) 赋值到 \(b\) ,同时维护序列 \(b\) 的有序性。每次查询时,如果左右两侧存在不完整的块,那么我们就暴力扫描一遍序列 \(a\) 中两侧不完整块的区间,统计小于 \(x\) 的元素个数。对于每一个完整的块,我们利用 lower_boundupper_bound 函数求出上段中提到的信息,然后求出小于 \(x\) 的元素个数。注意,此时还未考虑到加法标记。因此,我们在二分的时候需要查询的值为 $x\ - $ 加法标记。

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;

const int maxn = 5e4 + 5;
const int maxm = 2e3 + 5;

int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], b[maxn], lazy[maxm];

inline void update(int l, int r, long long x) {
    if (bel[l] == bel[r]) {
        for (register int i = l; i <= r; i++) {
            a[i] += x;
        }

        for (register int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
            b[i] = a[i];
        }

        sort(b + st[bel[l]], b + ed[bel[l]] + 1);
    } else {
        for (register int i = l; i <= ed[bel[l]]; i++) {
            a[i] += x;
        }

        for (register int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
            b[i] = a[i];
        }

        sort(b + st[bel[l]], b + ed[bel[l]] + 1);

        for (register int i = st[bel[r]]; i <= r; i++) {
            a[i] += x;
        }

        for (register int i = st[bel[r]]; i <= ed[bel[r]]; i++) {
            b[i] = a[i];
        }

        sort(b + st[bel[r]], b + ed[bel[r]] + 1);

        for (register int i = bel[l] + 1; i < bel[r]; i++) {
            lazy[i] += x;
        }
    }
}

inline int query(int l, int r, long long x) {
    int ans = 0, pos;

    if (bel[l] == bel[r]) {
        for (register int i = l; i <= r; i++) {
            if (a[i] + lazy[bel[i]] < x) {
                ans++;
            }
        }
    } else {
        for (register int i = l; i <= ed[bel[l]]; i++) {
            if (a[i] + lazy[bel[i]] < x) {
                ans++;
            }
        }

        for (register int i = st[bel[r]]; i <= r; i++) {
            if (a[i] + lazy[bel[i]] < x) {
                ans++;
            }
        }

        for (register int i = bel[l] + 1; i < bel[r]; i++) {
            pos = lower_bound(b + st[i], b + ed[i] + 1, x - lazy[i]) - b;
            ans += pos - st[i];
        }
    }

    return ans;
}

int main() {
    int opt, l, r;
    long long c;
    scanf("%d", &n);
    int block = sqrt(n);
    int cnt = ceil(n * 1.0 / block);

    for (register int i = 1; i <= cnt; i++) {
        st[i] = (i - 1) * block + 1;
        ed[i] = i * block;
    }

    ed[cnt] = n;

    for (register int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        b[i] = a[i];
        bel[i] = (i - 1) / block + 1;
    }

    for (register int i = 1; i <= cnt; i++) {
        sort(b + st[i], b + ed[i] + 1);
    }

    for (register int i = 1; i <= n; i++) {
        scanf("%d%d%d%lld", &opt, &l, &r, &c);

        if (opt == 0) {
            update(l, r, c);
        } else {
            printf("%d\n", query(l, r, c * c));
        }
    }

    return 0;
}

数列分块入门 \(3\)

题目链接

区间加法、查询前驱

同样利用二分思想。维护加法标记同上题相同,具体差异在于查询时维护的是前驱的值,而且二分时取的下标是 lower_bound 得到的下标 \(- 1\) ,并且无论二分还是暴力都要考虑 加法标记 ,具体思路见代码。

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;

int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], b[maxn], lazy[maxm];

void update(int l, int r, long long x) {
    if (bel[l] == bel[r]) {
        for (int i = l; i <= r; i++) {
            a[i] += x;
        }

        for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
            b[i] = a[i];
        }

        sort(b + st[bel[l]], b + ed[bel[l]] + 1);
    } else {
        for (int i = l; i <= ed[bel[l]]; i++) {
            a[i] += x;
        }

        for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
            b[i] = a[i];
        }

        sort(b + st[bel[l]], b + ed[bel[l]] + 1);

        for (int i = st[bel[r]]; i <= r; i++) {
            a[i] += x;
        }

        for (int i = st[bel[r]]; i <= ed[bel[r]]; i++) {
            b[i] = a[i];
        }

        sort(b + st[bel[r]], b + ed[bel[r]] + 1);

        for (int i = bel[l] + 1; i < bel[r]; i++) {
            lazy[i] += x;
        }
    }
}

long long query(int l, int r, long long x) {
    int pos;
    long long val = -1;

    if (bel[l] == bel[r]) {
        for (int i = l; i <= r; i++) {
            if (a[i] + lazy[bel[i]] < x) {
                val = max(val, a[i] + lazy[bel[i]]);
            }
        }
    } else {
        for (int i = l; i <= ed[bel[l]]; i++) {
            if (a[i] + lazy[bel[i]] < x) {
                val = max(val, a[i] + lazy[bel[i]]);
            }
        }

        for (int i = st[bel[r]]; i <= r; i++) {
            if (a[i] + lazy[bel[i]] < x) {
                val = max(val, a[i] + lazy[bel[i]]);
            }
        }

        for (int i = bel[l] + 1; i < bel[r]; i++) {
            if (b[st[i]] + lazy[i] >= x) {
                continue;
            }

            pos = lower_bound(b + st[i], b + ed[i] + 1, x - lazy[i]) - b;
            val = max(val, b[pos - 1] + lazy[i]);
        }
    }

    return val;
}

int main() {
    int opt, l, r;
    long long c;
    scanf("%d", &n);
    int block = sqrt(n);
    int cnt = ceil(n * 1.0 / block);

    for (int i = 1; i <= cnt; i++) {
        st[i] = (i - 1) * block + 1;
        ed[i] = i * block;
    }

    ed[cnt] = n;

    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        bel[i] = (i - 1) / block + 1;
    }

    for (int i = 1; i <= cnt; i++) {
        for (int j = st[i]; j <= ed[i]; j++) {
            b[j] = a[j];
        }

        sort(b + st[i], b + ed[i] + 1);
    }

    for (int i = 1; i <= n; i++) {
        scanf("%d%d%d%lld", &opt, &l, &r, &c);

        if (opt == 0) {
            update(l, r, c);
        } else {
            printf("%lld\n", query(l, r, c));
        }
    }

    return 0;
}

数列分块入门 \(4\)

题目链接

这道题使用了分块基本思想中的 维护 。我们不仅维护整块的加法标记,同时维护整块的元素之和。这样,就可以在 \(O(1)\) 的时间内求出整块的和。

考虑区间加法对于区间和的影响。显然,如果操作的是不完整块,我们直接在操作单个元素时维护整块和即可。如果操作一个完整块,显然区间和会增加区间长度 \(\times\) 被操作数,直接维护即可,注意取模。

#include <cstdio>
#include <cmath>
using namespace std;

const int maxn = 5e4 + 5;
const int maxm = 1e3 + 5;

int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], sum[maxm], lazy[maxm];

void update(int l, int r, long long x) {
	if (bel[l] == bel[r]) {
		for (int i = l; i <= r; i++) {
			a[i] += x;
			sum[bel[i]] += x;
		}
	} else {
		for (int i = l; i <= ed[bel[l]]; i++) {
			a[i] += x;
			sum[bel[i]] += x;
		}
		for (int i = st[bel[r]]; i <= r; i++) {
			a[i] += x;
			sum[bel[i]] += x;
		}
		for (int i = bel[l] + 1; i < bel[r]; i++) {
			lazy[i] += x;
			sum[i] += (ed[i] - st[i] + 1) * x;
		}
	}
}

long long query(int l, int r, long long mod) {
	long long ans = 0;
	mod++;
	if (bel[l] == bel[r]) {
		for (int i = l; i <= r; i++) {
			ans = (ans + a[i] + lazy[bel[i]]) % mod;
		}
	} else {
		for (int i = l; i <= ed[bel[l]]; i++) {
			ans = (ans + a[i] + lazy[bel[i]]) % mod;
		}
		for (int i = st[bel[r]]; i <= r; i++) {
			ans = (ans + a[i] + lazy[bel[i]]) % mod;
		}
		for (int i = bel[l] + 1; i < bel[r]; i++) {
			ans = (ans + sum[i]) % mod;
		}
	}
	return ans;
}

int main() {
	int opt, l, r;
	long long c;
	scanf("%d", &n);
	int block = sqrt(n);
	int cnt = ceil(n * 1.0 / block);
	for (int i = 1; i <= cnt; i++) {
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
	}
	ed[cnt] = n;
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &a[i]);
		bel[i] = (i - 1) / block + 1;
		sum[bel[i]] += a[i];
	}
	for (int i = 1; i <= n; i++) {
		scanf("%d%d%d%lld", &opt, &l, &r, &c);
		if (opt == 0) {
			update(l, r, c);
		} else {
			printf("%lld\n", query(l, r, c));
		}
	}
	return 0;
}

数组分块入门 \(5\)

题目链接

区间开方、区间求和

这道题巧妙地运用了三大基本思想之一的 维护 。显然,假如待开方的数 \(\leq 1\) ,我们不需要对它进行开方。因此,我们可以维护一个块内最大值,假如块内最大元素 \(\leq 1\) ,我们不对这个块进行操作。否则,我们暴力地对整块中的每一个元素进行开方,同时重新维护整块最大值以及整块和。

严格意义上来说,这道题的时间复杂度十分玄学。但是,因为值域较小,相应的开方次数也会较小。因此,开了 \(O2\) 以后还是可以 卡进时限 \(AC\) 的。

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;

const int maxn = 5e4 + 5;
const int maxm = 1e3 + 5;
const long long inf = 1e16 + 5;

int n;
int st[maxm], ed[maxm], bel[maxn];
long long a[maxn], sum[maxm], val[maxm];

void update(int l, int r) {
	if (bel[l] == bel[r]) {
		if (val[bel[l]] <= 1) {
			return;
		}
		for (int i = l; i <= r; i++) {
			sum[bel[i]] = sum[bel[i]] - a[i] + sqrt(a[i]);
			a[i] = sqrt(a[i]);
		}
		val[bel[l]] = -inf;
		for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
			val[bel[l]] = max(val[bel[l]], a[i]);
		}
	} else {
		if (val[bel[l]] > 1) {
			for (int i = l; i <= ed[bel[l]]; i++) {
				sum[bel[i]] = sum[bel[i]] - a[i] + sqrt(a[i]);
				a[i] = sqrt(a[i]);
			}
			val[bel[l]] = -inf;
			for (int i = st[bel[l]]; i <= ed[bel[l]]; i++) {
				val[bel[l]] = max(val[bel[l]], a[i]);
			}
		}
		if (val[bel[r]] > 1) {
			for (int i = st[bel[r]]; i <= r; i++) {
				sum[bel[i]] = sum[bel[i]] - a[i] + sqrt(a[i]);
				a[i] = sqrt(a[i]);
			}
			val[bel[r]] = -inf;
			for (int i = st[bel[r]]; i <= ed[bel[r]]; i++) {
				val[bel[i]] = max(val[bel[i]], a[i]);
			}
		}
		for (int i = bel[l] + 1; i < bel[r]; i++) {
			if (val[i] > 1) {
				for (int j = st[i]; j <= ed[i]; j++) {
					sum[i] = sum[i] - a[j] + sqrt(a[j]);
					a[j] = sqrt(a[j]);
				}
				val[i] = sqrt(val[i]);
			}
		}
	}
}

long long query(int l, int r) {
	long long ans = 0;
	if (bel[l] == bel[r]) {
		for (int i = l; i <= r; i++) {
			ans += a[i];
		}
	} else {
		for (int i = l; i <= ed[bel[l]]; i++) {
			ans += a[i];
		}
		for (int i = st[bel[r]]; i <= r; i++) {
			ans += a[i];
		}
		for (int i = bel[l] + 1; i < bel[r]; i++) {
			ans += sum[i];
		}
	}
	return ans;
}

int main() {
	int opt, l, r, c;
	scanf("%d", &n);
	int block = sqrt(n);
	int cnt = ceil(n * 1.0 / block);
	for (int i = 1; i <= cnt; i++) {
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
		val[i] = -inf;
	}
	ed[cnt] = n;
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &a[i]);
		bel[i] = (i - 1) / block + 1;
		sum[bel[i]] += a[i];
		val[bel[i]] = max(val[bel[i]], a[i]);
	}
	for (int i = 1; i <= n; i++) {
		scanf("%d%d%d%d", &opt, &l, &r, &c);
		if (opt == 0) {
			update(l, r);
		} else {
			printf("%lld\n", query(l, r));
		}
	}
	return 0;
}

数组分块入门 \(6\)

题目链接

单点插入、单点查询

因为数组的长度会变动,所以显然不可以用数组和传统分块来维护。自然想到用 vector 来维护每一个块,每次插入时直接暴力地使用 vectorinsert 函数来插入。但是,如果任由数据插入的话,块长显然会失衡,分块也就没有了意义。因此,当块长失衡,也就是单块过长时,我们直接暴力重新分块。至于块长失衡的条件,这里设定成单块的块长超过了 \(20 \times \sqrt{n}\) ,亲测不会导致超时。

#include <cstdio>
#include <cmath>
#include <vector> 
using namespace std;

const int maxn = 2e5 + 5;
const int maxm = 1e3 + 5;

int n, block, size;
int a[maxn], bel[maxn];
vector<int> v[maxm];

void rebuild()
{
	int cnt = 0, sz;
	for (int i = 1; i <= size; i++)
	{
		sz = v[i].size();
		for (int j = 0; j < sz; j++)
			a[++cnt] = v[i][j];
		v[i].clear();
	}
	if (!cnt)
		cnt = n;
	block = sqrt(cnt);
	size = ceil(cnt * 1.0 / block);
	for (int i = 1; i <= cnt; i++)
		bel[i] = (i - 1) / block + 1;
	for (int i = 1; i <= cnt; i++)
		v[bel[i]].push_back(a[i]);
}

void update(int pos, int val)
{
	int x = 1;
	while (x <= size && pos > v[x].size())
	{
		pos -= v[x].size();
		x++;
	}
	v[x].insert(v[x].begin() + pos - 1, val);
	if (v[x].size() > 5 * block)
		rebuild();
}

int query(int pos)
{
	int x = 1;
	while (x <= size && pos > v[x].size())
	{
		pos -= v[x].size();
		x++;
	}
	return v[x][pos - 1];
}

int main()
{
//	freopen("a8.in", "r", stdin);
//	freopen("a8.ans", "w", stdout);
	int opt, l, r, c;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	rebuild();
	for (int i = 1; i <= n; i++)
	{
		scanf("%d%d%d%d", &opt, &l, &r, &c);
		if (opt == 0)
			update(l, r);
		else
			printf("%d\n", query(r));
	}
	return 0;
}

数组分块入门 \(7\)

题目链接

区间乘法、区间加法、单点询问

显然,像线段树的模板一样,我们需要维护加法标记和乘法标记。相应的,两个标记的修改优先级需要讨论。当我们修改加法标记,原本的乘法标记不会有任何改变。但是,当我们修改乘法标记的时候,原本的序列值和加法标记都要相应地改变。

我们可以这样大致理解,设加法标记为 \(a\) ,乘法标记为 \(m\) ,原本的值为 \(x\) ,新乘的值为 \(n\) 。则修改前,这个单点的值为 \(x \times m + a\) ,修改后,这个值变成了 \(n \times (x \times m + a)\) ,乘法分配律后得到 \(n \times x \times m + n \times a\)

另外,每次对不完整块进行操作时,我们都需要先把原本的标记叠加到原序列上。这样做的原因是假如不先把乘法标记叠加在加法标记上,先加后乘的操作就会出错,也就是我们询问时就会访问到乘法之前的乘法标记。

#include <cstdio>
#include <cmath>
using namespace std;

const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;
const int mod = 10007;

int n;
int bel[maxn], st[maxm], ed[maxm];
long long a[maxn], add[maxm], mul[maxm];

void clear(int x)
{
	for (int i = st[x]; i <= ed[x]; i++)
		a[i] = ((a[i] * mul[x]) % mod + add[x]) % mod;
	mul[x] = 1;
	add[x] = 0;
}

void add_num(int l, int r, long long c)
{
	if (bel[l] == bel[r])
	{
		clear(bel[l]);
		for (int i = l; i <= r; i++)
			a[i] = (a[i] + c) % mod;
	}
	else
	{
		clear(bel[l]);
		for (int i = l; i <= ed[bel[l]]; i++)
			a[i] = (a[i] + c) % mod;
		clear(bel[r]);
		for (int i = st[bel[r]]; i <= r; i++)
			a[i] = (a[i] + c) % mod;
		for (int i = bel[l] + 1; i < bel[r]; i++)
			add[i] = (add[i] + c) % mod;
	}
}

void mul_num(int l, int r, long long c)
{
	if (bel[l] == bel[r])
	{
		clear(bel[l]);
		for (int i = l; i <= r; i++)
			a[i] = (a[i] * c) % mod;
	}
	else
	{
		clear(bel[l]);
		for (int i = l; i <= ed[bel[l]]; i++)
			a[i] = (a[i] * c) % mod;
		clear(bel[r]);
		for (int i = st[bel[r]]; i <= r; i++)
			a[i] = (a[i] * c) % mod;
		for (int i = bel[l] + 1; i < bel[r]; i++)
		{
			mul[i] = (mul[i] * c) % mod;
			add[i] = (add[i] * c) % mod;
		}
	}
}

long long query(int pos)
{
	return ((a[pos] * mul[bel[pos]]) % mod + add[bel[pos]]) % mod;
}

int main()
{
	int opt, l, r;
	long long c;
	scanf("%d", &n);
	int block = sqrt(n);
	int size = ceil(n * 1.0 / block);
	for (int i = 1; i <= size; i++)
	{
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
		mul[i] = 1;
	}
	ed[size] = n;
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld", &a[i]);
		bel[i] = (i - 1) / block + 1; 
	}
	for (int i = 1; i <= n; i++)
	{
		scanf("%d%d%d%lld", &opt, &l, &r, &c);
		if (opt == 0)
			add_num(l, r, c);
		else if (opt == 1)
			mul_num(l, r, c);
		else
			printf("%lld\n", query(r));
	}
	return 0;
}

数列分块入门 \(8\)

题目链接

区间赋值、查询与 \(x\) 相等的元素个数

这道题与分块入门 \(2\) 相似,但是使用相同的方法却会超时。这道题将询问和修改融合在一起,因此,我们可以将代码稍微进行修改,整合成一个整体的函数。

我们维护一个标记,含义是整块被赋值成的值,初始值为无穷大,代表没有被赋值过。对于每次查询,若左右两侧存在不完整块,则我们暴力扫描一遍,若当前元素与 \(x\) 相等,则相等元素个数 \(+ \ 1\) 。否则,我们把当前元素修改成 \(x\)

对于整块的查询类似。若当前块存在赋值标记,那么我们直接判断标记是否与 \(x\) 相等,若相等,说明整块都与 \(x\) 相等,计数器加上块长;否则,直接把当前块的标记修改成 \(x\) 。如果当前块没有赋值标记,我们再次暴力查询、修改,最后别忘了修改标记。

与上一道题相同,这道题也需要在每次操作前还原标记。hao

这道题的算法复杂度十分玄学,但是仔细一想,假如原本序列是有序的,每次操作至多令开头和结尾两个块失去有序性。也就是说,我们至少用 \(\sqrt{n}\) 个操作才能令序列失去有序性。并且 \(\sqrt{n}\) 个操作之后,我们的操作要么直接扫描的时间复杂度优于 \(O(\sqrt{n})\) ,要么就会无形中维护了序列的有序性。假如序列原本无序,那么我们的操作反而有可能帮助维护序列的有序性。这样一想,玄学的复杂度似乎也可以接受了。具体的证明,请前往开头提到的 \(hzwer\) 学长的博客。

#include <cstdio>
#include <cmath>
using namespace std;
#define int long long

const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;
const int inf = 1e18;

int n;
int a[maxn], st[maxm], ed[maxm];
int bel[maxn], lazy[maxm];

void clear(int x)
{
	if (lazy[x] == inf)
		return;
	for (int i = st[x]; i <= ed[x]; i++)
		a[i] = lazy[x];
	lazy[x] = inf;
}

int solve(int l, int r, int c)
{
	int cnt = 0;
	if (bel[l] == bel[r])
	{
		clear(bel[l]);
		for (int i = l; i <= r; i++)
		{
			if (a[i] == c)
				cnt++;
			else
				a[i] = c;
		}
	}
	else
	{
		clear(bel[l]);
		for (int i = l; i <= ed[bel[l]]; i++)
		{
			if (a[i] == c)
				cnt++;
			else
				a[i] = c;
		}
		clear(bel[r]);
		for (int i = st[bel[r]]; i <= r; i++)
		{
			if (a[i] == c)
				cnt++;
			else
				a[i] = c;
		}
		for (int i = bel[l] + 1; i < bel[r]; i++)
		{
			if (lazy[i] != inf)
			{
				if (lazy[i] == c)
					cnt += (ed[i] - st[i] + 1);
				else
					lazy[i] = c;
			}
			else
			{
				for (int j = st[i]; j <= ed[i]; j++)
				{
					if (a[j] == c)
						cnt++;
					else
						a[j] = c;
				}
				lazy[i] = c;
			}
		}
	}
	return cnt;
}

signed main()
{
	int l, r, c;
	scanf("%lld", &n);
	int block = sqrt(n);
	int size = ceil(n * 1.0 / block);
	for (int i = 1; i <= size; i++)
	{
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
		lazy[i] = inf;
	}
	ed[size] = n;
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld", &a[i]);
		bel[i] = (i - 1) / block + 1;
	}
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld%lld%lld", &l, &r, &c);
		printf("%lld\n", solve(l, r, c));
	}
	return 0;
}

数列分块入门 \(9\)

题目链接

静态查询区间最小众数

这道题是分块入门系列的压轴题,难度直接上天。在说明思路之前,先解释一个显然的现象:区间 \([l, r]\) 的最小众数,只有可能是左右两侧不完整块的最小众数和中间完整块的最小众数。所以,我们可以考虑暴力枚举这些众数,再一个个加以判断。

但是这样做的时间复杂度还是太坏了。我们考虑优化这个算法。注意到查询是 静态 的,这意味着我们可以 预处理 。设 \(f_{i, j}\) 为从块 \(i\) 到 块 \(j\) 的最小众数。我们在查询的时候可以直接调用预处理好的 \(f\) 来快速求出中间完整块的最小众数。

但是这样做的时间复杂度还是太坏了。我们在判断是否更新最小众数的时候需要扫一遍整个块,时间复杂度自然爆炸。期望在 \(O(logn)\) 的复杂度内更新最小众数,我们考虑一种乱搞做法。先将原序列 离散化 ,然后对于每一个排名,我们都用一个 vector 保存它出现过的下标。这样,我们就可以通过与分块入门 \(2\) 类似的思想来求出区间 \([l, r]\) 中排名 \(x\) 出现过的次数。

值得注意的是,我们需要维护两个映射:值到离散化排名、离散化排名到值。这里使用 map 实在没有必要,乱搞即可。以及这道题的块长十分玄学,使用 \(\sqrt{n}\) 会原地爆炸,亲测 \([150, 300]\) 是可以通过的区间。其他块长不保证可以通过。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5;
const int maxm = 1e3 + 5;

int n;
int st[maxm], ed[maxm], f[maxm][maxm];
int a[maxn], val[maxn], cnt[maxn], bel[maxn];
map<int, int> mp;
vector<int> v[maxn];

int read()
{
	int res = 0, flag = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')
			flag = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		res = res * 10 + ch - '0';
		ch = getchar();
	}
	return res * flag;
}

int get_num(int l, int r, int x)
{
	return upper_bound(v[x].begin(), v[x].end(), r) - lower_bound(v[x].begin(), v[x].end(), l);
}

void pre_work(int x)
{
	int ans = 0, res = 0;
	memset(cnt, 0, sizeof(cnt));
	for (int i = st[x]; i <= n; i++)
	{
		cnt[a[i]]++;
		if (cnt[a[i]] > res)
		{
			res = cnt[a[i]];
			ans = a[i];
		}
		else if (cnt[a[i]] == res && val[a[i]] < val[ans])
			ans = a[i];
		f[x][bel[i]] = ans;
	}
}

int query(int l, int r)
{
	int ans = 0, res, max_res = 0;
	if (bel[l] == bel[r])
	{
		for (int i = l; i <= r; i++)
		{
			res = get_num(l, r, a[i]);
			if (res > max_res)
			{
				max_res = res;
				ans = a[i];
			}
			else if (res == max_res && val[a[i]] < val[ans])
				ans = a[i];
		}
	}
	else
	{
		ans = f[bel[l] + 1][bel[r] - 1];
		max_res = get_num(l, r, ans);
		for (int i = l; i <= ed[bel[l]]; i++)
		{
			res = get_num(l, r, a[i]);
			if (res > max_res)
			{
				max_res = res;
				ans = a[i];
			}
			else if (res == max_res && val[a[i]] < val[ans])
				ans = a[i];
		}
		for (int i = st[bel[r]]; i <= r; i++)
		{
			res = get_num(l, r, a[i]);
			if (res > max_res)
			{
				max_res = res;
				ans = a[i];
			}
			else if (res == max_res && val[a[i]] < val[ans])
				ans = a[i];
		}
	}
	return val[ans];
}

int main()
{
	int l, r, tot = 0;
	n = read();
	int block = 150;
	int size = ceil(n * 1.0 / block);
	for (int i = 1; i <= size; i++)
	{
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
	}
	ed[size] = n;
	for (int i = 1; i <= n; i++)
	{
		a[i] = read();
		bel[i] = (i - 1) / block + 1;
		if (!mp.count(a[i]))
		{
			mp[a[i]] = ++tot;
			val[tot] = a[i];
		}
		a[i] = mp[a[i]];
		v[a[i]].push_back(i);
	}
	for (int i = 1; i <= size; i++)
		pre_work(i);
	for (int i = 1; i <= n; i++)
	{
		l = read();
		r = read();
		printf("%d\n", query(l, r));
	}
	return 0;
}
posted @ 2021-07-24 23:34  kymru  阅读(80)  评论(0编辑  收藏  举报