倍增
基于倍增的状态设计
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 与众不同
一个需要瞪眼发现单调性的题目。
- 分析
-
维护 \(last_{val}\) 表示元素 \(val\) 上一次出现的位置。
-
维护 \(s_i\) 表示以 \(i\) 为右端点的完美序列的起点,则:
如果这样去维护 \(s\) 的话就使得 \(s\) 一定是单调不减的。
-
对于询问的 \([l,r]\) 存在一个分界点 \(p\) 使得终点 \(p\) 在 \([p,r]\) 间的完美序列的起点也在 \([l,r]\) 内,终点在 \([l,p - 1]\) 间的完美序列起点在 \(l\) 左边。
-
\(p - 1\) 之前的起点都是 \(l\),则完美序列的最大长度为 \((p - 1) - l + 1 = p - l\),若起点在 \([p,r]\) 间则用 st 表维护 \(i - s_i + 1\) 的最大值,则每组询问的答案为:
-
由于 \(s\) 单调不减,所以 \(p\) 可以二分查找得到。
-
注意 \(a_i\) 的值域可能为负,\(p\) 可能不存在。
时间复杂度 \(O(n \log n + m \log n)\)。
树上倍增
通常维护 \(dp_{i,j}\) 表示节点 \(i\) 沿着祖先跳 \(2^j\) 到达的节点编号、区间最值、区间和等。
LCA
倍增写法。
性质
-
LCA 与根是谁有关。
-
树上两点距离与根无关。
-
设 \(dis_i\) 表示根到 \(i\) 的距离,\(dist(i, j)\) 表示树上节点 \(i,j\) 间的距离,则:
容斥原理。
例题
P3398 仓鼠找 sugar
推 LCA 性质。
- 题意
给定一棵树,\(q\) 次询问,每次询问 \(a\) 到 \(b\) 和 \(c\) 到 \(d\) 的路径是否有交。
- 分析
用二元组 \((x, y)\) 表示 \(x \to y\) 这条路径。
可以大胆猜一个结论:两段路径有且仅有其端点的 LCA 与另一个路径有交。
证明:
若有两个交点,那么原图为一棵基环树,存在环,与原条件矛盾。
P4281 [AHOI2008] 紧急集合 / 聚会
- 分析
-
三点可以先取两点的 LCA 再与第三点求 LCA。
-
可以证明三点至多只会有两个不同的 LCA。
-
容斥算得三点间的距离:
CF379F New Year Tree
- 分析
-
每次操作只会加同一深度、同一子树内的两个节点,树的直径至多加一且如果直径增加,一定是新加的节点导致的,也就是说新加的节点一定是直径的端点,且两个新加的节点是等价的。
-
维护直径的端点,比较 \(dis(cnt,x), dis(cnt,y), dis(x, y)\),其中 \(x, y\) 为直径端点,\(cnt\) 为新加节点。
-
倘若用倍增去求 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
*/