26.1.21 根号分治 相向双指针

C. Subset Sums

根号分治的一个经典模型就是,多个集合,集合大小和是 O ( n ) O(n) O(n)的。那么大小大于 n \sqrt n n 的集合个数不会超过 B \sqrt B B ,个数多于 n \sqrt n n 的集合,大小不会超过 n \sqrt n n 。

根号分治的关键就是,划分问题,对于每一类问题,都要找到一个维度是不超过 n \sqrt n n 的,这里对这两类问题,显然第一类的不超过 n \sqrt n n 的维度是集合个数,第二类的是集合大小,分别对这两个维度进行暴力处理即可。

具体来说,我们把问题划分成大集合和小集合两类,这两类都分别有查询和更新,那么我们需要解决的是,一共四个问题,小更新对小查询,小更新对大查询,大更新对小查询,小更新对大查询。

  • 比较显然的是,对于小集合,更新直接暴力更新 a i a_i ai,查询直接对 a i a_i ai求和。这就解决了小更新对小查询。
  • 大集合更新,不能暴力,比较常见的处理是,用一个懒标记记录增加,查询时,由于大集合只有 n \sqrt n n 个,所以可以枚举所有大集合,累加每个大集合,对当前查询集合的影响。这个影响应该是,大集合和当前集合的交集元素个数,乘上大集合的懒标记。这需要我们预处理每个大集合和所有集合的交集大小。注意这只是集合交集,不是LCS,所以我们用一个临时数组,记录每个大集合包含的元素,然后再去枚举所有集合的元素,检查他们有多少在大集合中,这样对于每个大集合,都需要 ∑ s z i = O ( n ) \sum sz_i=O(n) ∑szi=O(n)的时间,整体预处理需要 O ( n n ) O(n\sqrt n) O(nn )。至此我们解决了大集合,对大小集合的贡献
  • 还剩小集合更新对大集合查询的影响,这个可以枚举大集合,直接把当前小集合加,对每个大集合的和的贡献加上,这不是懒标记,需要再开一个数组,新开的这个数组的初始值可以是每个集合的初始 ∑ a i \sum a_i ∑ai

最后是可以调节块长,虽然两部分的理论复杂度都是 O ( n ) O(n) O(n),但是常数不一样,根据均值不等式,这相当于 C 1 N B + C 2 N / B C_1NB+C_2N/B C1NB+C2N/B,那取到最小值的块长 B B B,也就会根据 C 1 , C 2 C_1,C_2 C1,C2这两个常数调整,而不是单纯的 B = n B=\sqrt n B=n

比如这题,注意到我们需要对大集合预处理他们和所有集合的交集大小,也就是大集合的工作量更大,那么为了平衡两类的速度,我们应该把 B B B调大,这样大集合会变少。

c 复制代码
const int B = 500 + 10;
ll a[N], add[N], sum[N];
int id[N], f[B][N], tmp[N];
void solve() {
	int n, m, q;
	cin >> n >> m >> q;
 
	int tot = 0;
	rep(i, 1, n) {
		cin >> a[i];
	}
 
	vvi s(m + 1);
	rep(i, 1, m) {
		int k;
		cin >> k;
		tot += k;
		rep(j, 1, k) {
			int x;
			cin >> x;
			s[i].push_back(x);
			sum[i] += a[x];
		}
	}
 
	int cnt = 0;
	rep(i, 1, m) {
		if (s[i].size() > B) {
			id[++cnt] = i;
			for (int pos : s[i]) {
				tmp[pos] = 1;
			}
			rep(j, 1, m) {
				int len = 0;
				for (int pos : s[j]) {
					len += tmp[pos];
				}
 
				f[cnt][j] = len;
			}
			for (int pos : s[i]) {
				tmp[pos] = 0;
			}
		}
	}
 
	rep(i, 1, q) {
		char op;
		cin >> op;
		ll k, x;
		if (op == '+') {
			cin >> k >> x;
			if (s[k].size() <= B) {
				for (int pos : s[k]) {
					a[pos] += x;
				}
				rep(j, 1, cnt) {
					sum[id[j]] += f[j][k] * x;
				}
			} else {
				add[k] += x;
			}
 
		} else {
			cin >> k;
			ll ans = 0;
			if (s[k].size() <= B) {
				for (int pos : s[k]) {
					ans += a[pos];
				}
				rep(j, 1, cnt) {
					ans += f[j][k] * add[id[j]];
				}
			} else {
				ans += sum[k];
				rep(j, 1, cnt) {
					ans += f[j][k] * add[id[j]];
				}
			}
			cout << ans << '\n';
		}
	}
}

F. Sum of Progression

调和级数 根号分治

另一类根号分治,就是枚举倍数, n n n以内 x x x的倍数有 n / x n/x n/x个,所以对于需要枚举倍数计算的问题,如果 x x x较大,可以枚举倍数,如果 x x x较小,我们可以把这个较小的阈值设为小于 n \sqrt n n ,那么这样的 x x x个数,在 [ 1 , n ] [1,n] [1,n]梨又是很少的,个数很少,就可以有别的暴力方式。

对于这题,带权的枚举倍数求和。对于较大的 d d d显然可以直接枚举倍数,复杂度 O ( q n / B ) O(qn/B) O(qn/B),对于较小的 d d d,由于没有修改,考虑预处理然后直接查。

预处理什么呢?注意到每次查询的,实际上就是模 d d d等于 l m o d    d l\mod d lmodd的所有位置,如果我们把这些位置单拿出来作为一个序列,每次查询就是这个序列上的一个区间,可以维护前缀和。

那么我们需要对于所有模数,所有余数的组合,分别维护前缀和。比较朴素的想法是三维数组 f ( i , j , k ) f(i,j,k) f(i,j,k)表示模数 i i i余数 j j j的前 k k k个位置的元素和,但这样前两个维度的空间就 O ( n ) O(n) O(n)了,第三个维度不能全都开 O ( n ) O(n) O(n),而是需要开 O ( n / i ) O(n/i) O(n/i)。有点麻烦

更简单的方式是, f ( i , j ) f(i,j) f(i,j),模数为 i i i, j j j维度是 O ( n ) O(n) O(n)的我们让不同模数的前缀和都存在这一个 O ( n ) O(n) O(n)的数组上,上面所有形如 l , l + d , l + 2 d l,l+d,l+2d l,l+d,l+2d这些位置,就是一组前缀和,前缀和转移时, i i i从 i − d i-d i−d转移过来。

这里实现上,有一个优化,就是传统前缀和 l − 1 l-1 l−1,最坏也是 0 0 0,不会到负下标,但是这里查询时 l − d l-d l−d可能是负数,为了避免特判,我们可以把这个数组往右平移一点, a i a_i ai,会贡献在 i + d i+d i+d的位置而不是 i i i,那么查询 l , l + k d l,l+kd l,l+kd的查询就是 s [ l + k d ] − s [ l ] s[l+kd]-s[l] s[l+kd]−s[l],不会出现负数下标。

F. Remainder Problem

枚举倍数 根号分治

x x x较大,依然可以直接枚举所有倍数, x x x较小,注意到查询,一次就查所有模数 x x x余数 y y y的位置和,所以我们可以直接用一个数组 f ( i , j ) f(i,j) f(i,j)记录所有模 i i i余数 j j j的位置的元素和,查询时 O ( 1 ) O(1) O(1)查表。更新时,由于是单点更新,可以枚举所有不超过阈值 B B B的模数 m o d mod mod,把当前位置的增加值加上

C. Coloring Game

枚举 双指针 贪心

首先不考虑计数,先分析这个游戏的策略,如果先手确定了三个红色位置,后手应该怎么做,如果蓝色不覆盖一个红色,那么蓝色的应该越大越好,显然应该是 a n a_n an,那么假设的红色是 a i , a j , a k a_i,a_j,a_k ai,aj,ak,先手赢的条件是 a i + a j + a k > a n a_i+a_j+a_k>a_n ai+aj+ak>an,如果覆盖一个红色,比如 a i a_i ai,先手获胜条件是 a i + a j > a k a_i+a_j>a_k ai+aj>ak。

都移项使得左侧只有 i , j i,j i,j,这两个策略分别是 a n − a k , a k a_n-a_k,a_k an−ak,ak,这两个那个大是不一定的,所以后手应该选较大的那个,也就是 max ⁡ ( a n − a k , a k ) \max(a_n-a_k,a_k) max(an−ak,ak)

回到计数,总的方案数是 O ( n 3 ) O(n^3) O(n3)级别的,肯定不是全枚举,而是枚举一些维度后,剩下的可以快速计算。注意到数据量允许 O ( n 2 ) O(n^2) O(n2)的枚举,那么我们可以枚举先手选的 k k k, k k k值域 [ 1 , n ] [1,n] [1,n],再枚举 j j j,值域 [ 1 , k − 1 ] [1,k-1] [1,k−1]。此时只要确定合法的 i i i个数即可。

注意到这个式子 a i + a j > max ⁡ ( a k , a n − a k ) a_i+a_j>\max(a_k,a_n-a_k) ai+aj>max(ak,an−ak),我们枚举 k k k了,先把右侧当成定值,接下来从大到小枚举 j j j的话, a j a_j aj单增,那么对于一个 j j j满足条件的 i i i在一个区间内,并且 a j a_j aj越大,这个区间的下界越小,也就是有单调性,这显然可以双指针,只不过是相向双指针。

接下来就是经典的相向双指针求 a l + a r > C a_l+a_r>C al+ar>C的 ( l , r ) (l,r) (l,r)个数

两种实现,比较经典的实现是,用一个 w h i l e while while,里面用分支决定应该移动哪个指针。对于一个 r r r,如果 a l + a r > C a_l+a_r>C al+ar>C,那么 [ l , r − 1 ] [l,r-1] [l,r−1]内的所有下标都是合法的。这相当于每次移动一次 r r r,然后调整 l l l直到合法

c 复制代码
void solve() {
	int n;
	cin >> n;
 
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}
 
	int ans = 0;
	rep(i, 3, n) {
		int l = 1, r = i - 1;
		int t = max(a[i], a[n] - a[i]);
		while (l < r) {
			if (a[l] + a[r] > t) {
				ans += r - l;
				r--;
			} else {
				l++;
			}
		}
	}
	cout << ans << '\n';
}

另一种做法是枚举 l l l,调整 r r r直到合法。对于一个 l l l,如果 a l + a r > C a_l+a_r>C al+ar>C,那么 [ r , k − 1 ] [r,k-1] [r,k−1]内的下标都合法。但这样的话, l > r l>r l>r了,仍然有方案是合法的,只不过这些合法下标还需要满足大于 l l l,最后写起来边界有点麻烦。不如前一种写法

c 复制代码
void solve() {
	int n;
	cin >> n;
 
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}
 
	int ans = 0;
	rep(i, 3, n) {
		for (int j = 1, k = i - 1; j < i - 1; j++) {
			while (j < k && a[j] + a[k] > max(a[i], a[n] - a[i])) {
				k--;
			}
			ans += min(i - k - 1, i - j - 1);
		}
	}
	cout << ans << '\n';
}
相关推荐
迦蓝叶2 小时前
JDBC元数据深度实战:企业级数据资源目录系统构建指南
java·jdbc·企业级·数据资源·数据血缘·数据元管理·构建指南
chilavert3182 小时前
技术演进中的开发沉思-327 JVM:内存区域与溢出异常(下)
java·jvm
冲刺逆向2 小时前
【js逆向案例六】创宇盾(加速乐)通杀模版
java·前端·javascript
洛阳纸贵2 小时前
JAVA高级工程师-消息中间件RabbitMQ工作模式(二)
java·rabbitmq·java-rabbitmq
涅小槃2 小时前
Carla仿真学习笔记(版本0.9.16)
开发语言·python·ros·carla
沛沛老爹2 小时前
Web开发者转型AI安全核心:Agent Skills沙盒环境与威胁缓解实战
java·前端·人工智能·安全·rag·web转型升级
像少年啦飞驰点、2 小时前
Java大厂面试真题:Spring Boot + Kafka + Redis 在电商场景下的实战应用
java·spring boot·redis·分布式·kafka·面试题·电商秒杀
wujialaoer2 小时前
常用软件阿里源地址
开发语言·python