倍增

基于倍增的状态设计

P1081 [NOIP 2012 提高组] 开车旅行

CF1516D Cut

对于每一个 \(i\) 去尺取其 \(\gcd\) 为一的最长区间的右端点。设 \(dp_{i, j}\) 表示从点 \(i\) 开始跳 \(2 ^ j\) 个区间到达的点。查询时从 \(l\) 开始倍增即可。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 5;
const int M = log2(N) + 5; 
int n, Q, l, r, pos, cnt, a[N], lg[N], buc[N], dp[N][M];

inline void add(int x) {
	for(int i = 2 ; i * i <= x ; ++ i)
		if(x % i == 0) {
			if(buc[i] == 1) ++ cnt;
			++ buc[i];
			
			if(i * i != x) {
				if(buc[x / i] == 1) ++ cnt;
				++ buc[x / i];
			}
		}
		
	if(x != 1) {
		if(buc[x] == 1) ++ cnt;
		++ buc[x];
	}
	
	return ;
}

inline void del(int x) {
	for(int i = 2 ; i * i <= x ; ++ i)
		if(x % i == 0) {
			-- buc[i];
			if(buc[i] == 1) -- cnt;
			
			if(i * i != x) {
				-- buc[x / i];
				if(buc[x / i] == 1) -- cnt;
			}
		}
	
	if(x != 1) {
		-- buc[x];
		if(buc[x] == 1) -- cnt;
	}
	
	return ;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	memset(dp, 0x3f, sizeof dp);
	
	cin >> n >> Q;
	for(int i = 1 ; i <= n ; ++ i)
		cin >> a[i];
	
	for(int i = 1 ; i <= n ; ++ i) {
		while(cnt == 0 && pos <= n) add(a[++ pos]);
		
//		cerr << "pos:" << pos - 1 << '\n';
		
		dp[i][0] = pos - 1;
		
		del(a[i]);
	}
	
	for(int i = 2 ; i < N ; ++ i)
		lg[i] = lg[i >> 1] + 1;
	
	for(int i = 1 ; (1 << i) <= n ; ++ i)
		for(int j = 1 ; j <= n ; ++ j)
			dp[j][i] = dp[min(dp[j][i - 1] + 1, n)][i - 1];
			
	while(Q --) {
		cin >> l >> r;
		
		int ans = 0;
		
		for(int i = lg[n] ; ~ i ; -- i)
			if(dp[l][i] < r) {
				ans += (1 << i);
				l = dp[l][i] + 1;
			}

		cout << ans + 1 << '\n';
	}

	return 0;
}

/*
10 3
1 1 1 1 12345 1 93461 1 86754 1
1 4
5 6
3 8
*/

ST 表

不用讲,浅显易懂,背背板子。

例题

Loj 10121 与众不同

一个需要瞪眼发现单调性的题目。

  • 分析
  1. 维护 \(last_{val}\) 表示元素 \(val\) 上一次出现的位置。

  2. 维护 \(s_i\) 表示以 \(i\) 为右端点的完美序列的起点,则:

\[s_i = \max \left \{ s_{i - 1}, last_{a_i} + 1 \right \} \]

如果这样去维护 \(s\) 的话就使得 \(s\) 一定是单调不减的。

  1. 对于询问的 \([l,r]\) 存在一个分界点 \(p\) 使得终点 \(p\)\([p,r]\) 间的完美序列的起点也在 \([l,r]\) 内,终点在 \([l,p - 1]\) 间的完美序列起点在 \(l\) 左边。

  2. \(p - 1\) 之前的起点都是 \(l\),则完美序列的最大长度为 \((p - 1) - l + 1 = p - l\),若起点在 \([p,r]\) 间则用 st 表维护 \(i - s_i + 1\) 的最大值,则每组询问的答案为:

\[\max \left \{ p - l, \max_{i \in [p,r]} \left \{ i - s_i + 1 \right \} \right \} \]

  1. 由于 \(s\) 单调不减,所以 \(p\) 可以二分查找得到。

  2. 注意 \(a_i\) 的值域可能为负,\(p\) 可能不存在。

时间复杂度 \(O(n \log n + m \log n)\)

树上倍增

通常维护 \(dp_{i,j}\) 表示节点 \(i\) 沿着祖先跳 \(2^j\) 到达的节点编号、区间最值、区间和等。

LCA

倍增写法。

性质

  1. LCA 与根是谁有关。

  2. 树上两点距离与根无关。

  3. \(dis_i\) 表示根到 \(i\) 的距离,\(dist(i, j)\) 表示树上节点 \(i,j\) 间的距离,则:

\[dist(x, y) = dis_x + dis_y + 2dis_{LCA(x, y)} \]

容斥原理。

例题

P3398 仓鼠找 sugar

推 LCA 性质。

  • 题意

给定一棵树,\(q\) 次询问,每次询问 \(a\)\(b\)\(c\)\(d\) 的路径是否有交。

  • 分析

用二元组 \((x, y)\) 表示 \(x \to y\) 这条路径。

可以大胆猜一个结论:两段路径有且仅有其端点的 LCA 与另一个路径有交。

证明:

若有两个交点,那么原图为一棵基环树,存在环,与原条件矛盾。

P4281 [AHOI2008] 紧急集合 / 聚会

  • 分析
  1. 三点可以先取两点的 LCA 再与第三点求 LCA。

  2. 可以证明三点至多只会有两个不同的 LCA。

  3. 容斥算得三点间的距离:

\[dis_x + dis_y + dis_z - dis_{LCA_{x, y}} - dis_{LCA_{y, z}} - dis_{LCA_{x, z}} \]

CF379F New Year Tree

  • 分析
  1. 每次操作只会加同一深度、同一子树内的两个节点,树的直径至多加一且如果直径增加,一定是新加的节点导致的,也就是说新加的节点一定是直径的端点,且两个新加的节点是等价的。

  2. 维护直径的端点,比较 \(dis(cnt,x), dis(cnt,y), dis(x, y)\),其中 \(x, y\) 为直径端点,\(cnt\) 为新加节点。

  3. 倘若用倍增去求 LCA 每次单独更新 \(fa_{cnt,i}\) 即可。

时间复杂度是小常数 \(O(q \log q)\)

图上倍增

例题

CF702E Analysis of Pathes in Functional Graph

无需使用基环树知识,分别设 \(dp_{i, j}, mn_{i, j}, sum_{i, j}\) 表示从点 \(i\) 出发走 \(2 ^ j\) 步到达的点,到该点路径上的点权最小值,到该点路径上的点权之和。则直接倍增询问即可。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 5;
const int M = log2(1e10) + 5;
int n, k, lg, to[N][M], mn[N][M], sum[N][M];

inline void solve(int x) {
	int m = k, ans1 = 0, ans2 = 1e18;
	
	for(int i = lg ; ~ i ; -- i)
		if(m - (1ll << i) >= 0) {
			ans1 += sum[x][i];
			ans2 = min(ans2, mn[x][i]);
			
			m -= (1ll << i);
			x = to[x][i];
		}
		
	cout << ans1 << ' ' << ans2 << '\n';
	
	return ;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	memset(mn, 0x3f, sizeof mn);
	
	cin >> n >> k;
	for(int i = 1 ; i <= n ; ++ i)
		cin >> to[i][0], ++ to[i][0];
	for(int i = 1 ; i <= n ; ++ i) {
		cin >> mn[i][0];
		sum[i][0] = mn[i][0];
	}
	
	for(int i = 0 ; ; ++ i) {
		if((1ll << i) > k) break ;
		
		lg = i;
	}
	
	for(int i = 1 ; i <= lg ; ++ i)
		for(int j = 1 ; j <= n ; ++ j) {
			to[j][i] = to[to[j][i - 1]][i - 1];
			sum[j][i] = sum[j][i - 1] + sum[to[j][i - 1]][i - 1];
			mn[j][i] = min(mn[j][i - 1], mn[to[j][i - 1]][i - 1]);
		}

	cerr << lg << '\n';

	for(int i = 1 ; i <= n ; ++ i)
		solve(i); 
	
	return 0;
}

/*
1 2
0
10000
*/
posted @ 2024-08-23 11:52  endswitch  阅读(16)  评论(0)    收藏  举报