数据结构 专题

P5065 [Ynoi Easy Round 2014] 不归之人与望眼欲穿的人们(分块)

有经典性质:固定区间端点,区间 or 和的变化量是 \(O(\log V)\) 的。考虑分块,修改直接暴力重构。查询:

  • 若区间跨块,枚举块左端点 \(a\) 并用双指针找最小的区间整块 or 和 \(\ge x\) 的位置 \(b\)。需要在 \(a\) 的后缀和 \(b\) 的前缀进行拼接,由上面的性质本质不同的前后缀只有 \(O(\log V)\) 个,预处理出来,枚举后缀并在前缀双指针即可。
  • 若区间在一块中,固定左端点之后有用的右端点只有 \(O(\log V)\) 个,那么支配区间只有 \(O(B\log V)\) 个,但是每次二分暴力跳是 2log 的,拆位找到每一位下一个 \(1\) 出现的位置,那么 or 和只会在这些位置的并集发生修改,拉下来排序就可以了,但是排序还有一个 \(\log \log\) 的常数,发现可以增量维护,于是就做完了,对每个块预处理长度 \(\le len\) 的最大 or 和,查询时二分即可。

取 \(B=\sqrt{n}\),时间复杂度 \(O((n+q)\sqrt{n}\log V)\)。

P9060 [Ynoi2002] Goedel Machine(数论,莫队)

:::info[直接上莫反!]

\[\prod_{S\subseteq [n]} \gcd(S)=\prod_{d=1}^{V} d^{\left(\sum\limits_{S\subseteq [n]}[\gcd(S)=d]\right)} \\ \sum_{S\subseteq [n]}[\gcd(S)=d]=\sum_{t=1}^{V/d}\mu(t)\cdot \left(2^{\sum\limits_{i=1}^{n} [td\mid a_i]}-1\right) \\ \prod_{d=1}^{V} d^{\left[\sum\limits_{t=1}^{V/d}\mu(t)\cdot (2^{\sum_{i=1}^{n} [td\mid a_i]}-1)\right]}=\prod_{d=1}^{V} \left[d^{\sum_{t=1}^{V/d}\mu(t)}\right]^{(2^{\sum\limits_{i=1}^{n} [td\mid a_i]}-1)} \\ \]

考虑提取一个 \(T=td\):

\[\prod_{T=1}^V \left(\prod_{d\mid T} d^{\mu(\frac{T}{d})}\right)^{\normalsize{2^{c(T)}-1}} \]

提取 \(f(T)=\prod_{d\mid T} d^{\mu(\frac{T}{d})}\),其实 \(f(T)\) 不为 \(1\) 当且仅当 \(T=p^q,p\in \mathbb{P}\),并且此时 \(f(T)=p\),但是需要打表等手段辅助观察,因此不适合拆这类乘积式。

:::

莫反太吃操作了,考虑 P7360 的 Trick,乘积式的特性在于每个质因子是独立的。考虑每个质因子 \(p\) 在最终答案里的幂次,也就是子集 gcd 中 \(p\) 的次数之和。令 \(c_i(p)\) 表示 \(a_i\) 分解质因数后 \(p\) 的次数。

\[\prod_{S\subseteq [n]} \gcd(S)=\prod_{p\in \mathbb{P}} p^{\small{\sum\limits_{S\subseteq [n]}\min\limits_{i\in S}c_i(p)}} \\ \sum\limits_{S\subseteq [n]}\min\limits_{i\in S}c_i(p)=\sum_{t=1}^{+\infty}\sum_{S\subseteq [n]} [\min_{i\in S} c_i(p)\ge t]=\sum_{t=1}^{+\infty}(2^{\small\sum_{i=1}^n [c_i(p)\ge t]}-1) \\ \mathrm{ans}=\prod_{p^t\le V} p^{\normalsize 2^{\sum_{i=1}^n [p^t\mid a_i]}-1} \]

设 \(\sum_{i=1}^n [p^t\mid a_i]=c_{p,t}\),考虑增量维护,向集合中加入 \(a_i\) 时枚举所有 \(p^k\mid a_i\),造成的影响就是 \(c_{p,k}\) 加 \(1\),贡献变化为 \(p^{2^x}\to p^{2^{x+1}}\),也就是乘上 \(p^{2^{c_{p,k}}}\),那么我们预处理出 \(p^{2^x},p^{-1}\) 即可增量维护答案,枚举的 \((p,k)\) 个数是 \(\log V\) 的,如果直接硬上莫队只能做到 \(O(n\sqrt{m}\log V)\),无法接受。

经典到不能再经典的套路:按照 \(p\le \sqrt{V}, p>\sqrt{V}\) 分组:

  • \(p\le \sqrt{V}\),此时质数个数只有 \(O(\frac{\sqrt{V}}{\ln V})\) 个,因此 \((p,k)\) 二元组的个数也只有 \(O(\sqrt{V})\) 个,可以直接处理出 \(s_{p,k,r}\) 表示 \(p^k\) 次数的前缀和,查询时枚举所有 \((p,k)\) 计算贡献,这样时间复杂度是 \(O(n\sqrt{V})\) 的。
  • \(p>\sqrt{V}\),每个 \(a_i\) 至多包含一个这种质因子,更新的复杂度变成 \(O(1)\),直接上莫队。

因此总时间复杂度 \(O(n\sqrt{V})+n\sqrt{m})\)。

莫队二次离线

下文令 \(n\) 表示序列长度,\(m\) 表示询问个数。

:::info[普通莫队复习]

莫队的核心思想就是要减小相邻两个询问之间指针的移动距离和。也就是要把 \(\sum\limits_{i=2}^{m} |l_i-l_{i-1}|+|r_i-r_{i-1}|\) 降到可接受的范围。

取块长为 \(B\) 将序列分块,把询问按照 \(l\) 所在的块编号为第一关键字、\(r\) 为第二关键字升序排序,这样对同一块内部:\(l\) 每次移动的距离不会超过 \(O(B)\),同一块中 \(r\) 已升序排序体现为一直右移,移动距离也不会超过 \(O(n)\)。于是总移动次数就是 \(Bm+\frac{n^2}{B}\),取 \(B=\frac{n}{\sqrt{m}}\) 时移动次数不超过 \(n\sqrt{m}\)。设指针移动单次的代价为 \(f(n)\),那么普通莫队算法的时间复杂度为 \(O(n\sqrt{m}\cdot f(n))\)。

:::

我们熟知,当指针单次移动的时间开销为 \(f(n)\) 时,普通莫队算法的时间复杂度为 \(O(n\sqrt{m}\cdot f(n))\)。在 \(f(n)=O(\log n)\) 时有时还能接受,但是以 P4887 【模板】莫队二次离线 为例,单次插入时需要知道 \(a_j\in S,\mathrm{popcount}(a_j\oplus a_x)=K\) 的数量,这个东西很难处理。

令 \(f([l,r],x)\) 表示当前区间为 \([l,r]\),加入 \(a_x\) 时对答案的贡献。使用莫队二离有前提:\(f([l,r],x)\) 要具有可差分性。如果满足这个条件,可以用莫队二离做到 \(O(nf(n)+n\sqrt{m})\) 的优秀复杂度。

以莫队右端点 \(R\to r\) 为例,答案的增量就是 \(\sum\limits_{i=R+1}^{r} f([L,i-1],i)\),显然可以差分转化为 \(\sum\limits_{i=R+1}^r f([1,i-1],i)-f([1,L-1],i)\)。发现 \(f([1,i-1],i)\) 只与 \(i\) 有关可以简单处理,考虑 \(f([1,L-1],i)\) 怎么处理,离线下来,所有询问都是形如 \(x,l,r\) 表示询问 \(\sum\limits_{i=l}^r f([1,x],i)\) 的值,这个直接对着 \(x\) 扫描线,对 \(\mathrm{popc}(a_j\oplus a_i)=K\) 换一个统计方式,开个桶 \(cnt_y\) 表示加入 \(y\) 的贡献,枚举所有 \(\mathrm{popc}(t)=K\) 的值构成的集合 \(T_K\) 中的元素 \(u\in T_K\),那么加入 \(a_x\) 时会对 \(cnt_{u\oplus a_x}\) 造成贡献,查询可以直接暴力遍历 \([l,r]\) 区间统计 \(cnt_{a_i}\) 之和作为答案。分析一下时间复杂度,扫描线时更新桶需要 \(n\binom{14}{K}\) 的复杂度,由于莫队指针变化量总和只有 \(n\sqrt{m}\),\(\sum r-l\) 也是可以接受的 \(O(n\sqrt{m})\),因此总时间复杂度为 \(O(n\binom{14}{K}+n\sqrt{m})\)。

:::success[代码]

#include <bits/stdc++.h>
#define psb push_back

using namespace std;

typedef long long ll;
const int N=100005;
int n,Q,K,B; int a[N]; int msk[1<<14],tot; 
struct Query
{
	int l,r,qid;
	bool operator < (const Query &oth) const
	{
		int p=l/B, q=oth.l/B;
		if (p^q) return p<q;
		else return (p&1) ? r<oth.r : r>oth.r;
	}
} q[N]; ll ans[N], res[N]; ll f0[N], f1[N];
struct query { int l,r,k,v; };
vector<query> scan[N]; void apply(int x,int l,int r,int k,int v) { scan[x].psb({l,r,k,v}); }
int c[1<<14]; void ins(int x) { for (int i=1;i<=tot;i++) c[msk[i]^x]++; }
 
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0); 
	
	cin >> n >> Q >> K; B=ceil(1.0*n/sqrt(Q));
	for (int i=1;i<=n;i++) cin >> a[i];
	for (int S=0;S<(1<<14);S++) if (__builtin_popcount(S)==K) msk[++tot]=S;
	for (int i=1;i<=n;i++) f0[i]=f0[i-1]+c[a[i]], ins(a[i]), f1[i]=f1[i-1]+c[a[i]];
	for (int i=1;i<=Q;i++) { int l,r; cin >> l >> r; q[i]={l,r,i}; }
	sort(q+1,q+Q+1); int L=1, R=0;
	for (int k=1;k<=Q;k++)
	{
		auto [l,r,qid]=q[k];
		if (R<r) res[k]+=f0[r]-f0[R], apply(L-1,R+1,r,k,-1), R=r;
		if (l<L) res[k]-=f1[L-1]-f1[l-1], apply(R,l,L-1,k,1), L=l;
		if (r<R) res[k]-=f0[R]-f0[r], apply(L-1,r+1,R,k,1), R=r;
		if (L<l) res[k]+=f1[l-1]-f1[L-1], apply(R,L,l-1,k,-1), L=l;
		
	}
	memset(c,0,sizeof(c));
	for (int x=1;x<=n;x++)
	{
		ins(a[x]);
		for (auto [l,r,k,v] : scan[x])
		{
			ll sum=0; for (int i=l;i<=r;i++) sum+=c[a[i]];
			res[k]+=sum*v;
		}
	}
	for (int i=1;i<=Q;i++) res[i]+=res[i-1];
	for (int i=1;i<=Q;i++) ans[q[i].qid]=res[i];
	for (int i=1;i<=Q;i++) cout << ans[i] << "\n";
	
	return 0;
}

:::

P7906 [Ynoi2005] rpxleqxq(莫队二次离线,高低位分治)

发现就是莫队二离板子。需要一个数据结构维护集合支持:插入一个数,查询集合中 \((a_j\oplus x)\le X\) 的个数,要做到查询 \(O(1)\)。直接高低位分治,分成高位就大于 \(X\),以及高位与 \(X\) 相等低位 \(\le X\),这样修查是 \(O(\sqrt{V})-O(1)\) 的,时间复杂度 \(O(n\sqrt{V}+n\sqrt{m})\)。

P5397 [Ynoi2018] 天降之物

会 \(O(n\sqrt{n}\log n)\) 被卡掉了/kx/kx/kx。

考虑根号分治,取 \(B=\sqrt{N}\),按照集合大小 \(\mathrm{siz}\) 与 \(B\) 的关系分为小块和大块,对大块使用一个可以 \(O(\sqrt{n})\) 插入 \(O(1)\) 查前驱后继的分块,并维护大块-大块之间的答案 \(mn_{x,y}\):

  • \(siz_x,siz_y\le B\),直接暴力合并,如果合并之后 \(siz_x+siz_y>B\) 那么新建一个大块,这样的新建最多进行 \(O(\sqrt{n})\) 次,可以暴力遍历其他大块计算 \(mn_{x,y}\)。
  • \(siz_x\le B,siz_y>B\)。遍历 \(x\) 内部元素并计算与其他大块的 \(mn\),注意这种情况下插入元素的总和均摊是 \(O(n)\) 的,时间复杂度是 \(O(n\sqrt{n})\)。
  • \(siz_x,siz_y>B\)。仍然暴力合并,这种情况的出现次数只会有 \(O(\sqrt{n})\) 次,但是由启发式合并 \(siz_x\) 之和可能是 \(O(n\log n)\) 量级的,因此插入的时间复杂度退化到了 \(O(n\sqrt{n}\log n)\)。

取 \(B=\sqrt{n}\),时间复杂度是 \(O(n\sqrt{n}\log n)\),但是被卡掉了。考虑上述黑体字的性质,进行优化。

这个听说是叫第四分块。考虑上述做法哪里没处理好,我们使用了一个结构维护了大块的所有信息,导致在合并时不可避免的需要遍历元素进行修改。

重要观察: 可以通过一次 \(O(n)\) 的暴力重构求出某个块与其他所有颜色的关系,这个只需要在合并时动态维护颜色,最后在序列上扫一遍就行了。

那么我们抛弃掉原来的分块维护,记 \(mn_{b,i}\) 表示大块 \(b\) 与颜色 \(i\) 的答案。

  • 在大块-大块合并时每次暴力重构,由于大块合并的出现次数只会有 \(O(\sqrt{n})\) 次,这样做的时间复杂度是 \(O(n\sqrt{n})\)。

  • 在小块-小块合并成大块时暴力重构,由于这样的新建最多进行 \(O(\sqrt{n})\) 次,时间复杂度也是 \(O(n\sqrt{n})\)。

查询时可以直接查表。但是还需要处理 \(siz_x\le B,siz_y>B\) 的情况,如果此时也重构复杂度又假掉了。不过插入元素的总和均摊是 \(O(n)\) 的,我们引入一个缓冲池的概念,对每个大块开一个缓冲池 \(\mathrm{buf}_b\) 表示大块 \(b\) 中要加入但是还没有进行重构过的元素,并时刻保证这个缓冲池的大小 \(\le B\),查询就可以把缓冲池当作小块单独处理。在缓冲池大小 \(>B\) 时将其并入大块进行暴力重构,由于总和均摊是 \(O(n)\),这样的重构只会进行 \(O(\sqrt{n})\) 次,复杂度就是 \(O(n\sqrt{n})\) 了,非常的厉害。

P5064 [Ynoi Easy Round 2014] 等这场战争结束之后(操作树,值域分块)

考虑回退操作怎么做。令 \(G_q\) 表示第 \(q\) 次操作时图的状态,那么操作可以看作是状态的转移,加边等价于 \(G_i\) 是由 \(G_{i-1}\) 加入一条边得到的,而回退相当于 \(G_i\) 是由 \(G_x\) 加入滚木得到的,那么我们不妨将这个转移关系建成一棵操作树,加边时 \((i-1,i)\) 连边,回退时 \((x,i)\) 连边。这样每个位置的状态就是其根链上所有操作的叠加,因此支持加边和删边即可通过一遍 dfs 得出每个操作时图的状态。

加删边使用可撤销并查集即可维护。对于询问操作 \((x,k)\),一个想法是用动态开点权值线段树维护每个连通块内的权值集合,但是由于删边操作的存在这样做是错误的。考虑值域分块,每 \(B\) 个数分一段按值域从小到大处理,对于 \([(i-1)B,iB]\) 这一块,令 \(cnt_u\) 表示连通块 \(u\) 在当前块内的元素个数,如果 \(k>cnt_{\mathrm{find}(x)}\) 说明第 \(k\) 大在后面的块,令 \(k\) 减去 \(cnt_{\mathrm{find}(x)}\)。否则第 \(k\) 大就在这一块中,直接暴力遍历权值在这一块中的点并判断与 \(x\) 的连通关系。这样做的时间复杂度为 \(O(n^2/B+Bn\log n)\),取 \(B=\sqrt{n/\log n}\) 得到最优复杂度 \(O(n\sqrt{n\log n})\)。其实值域分块的本质就是按块的整体二分。

posted @ 2026-10-19 11:02  STDJCY  阅读(12)  评论(0)    收藏  举报