倍增
当我们在进行递推时,如果状态空间很大,线性递推会超时,那么就选择成倍的增长方式,以递推状态空间中 2 的整数幂次作为代表值。根据任何整数都能表示为若干个 2 的幂次的和这一性质,使用之前求出的代表值拼出所需的值,这就是倍增。所以倍增要求递推的问题的状态空间关于 2 的幂次具有可划分性。
题目分析
树上倍增(LCA)
解题思路
设数组 f [ i ] [ j ] f[i][j] f[i][j] 代表节点 i i i 的 2 j 2^j 2j 辈祖先,若超出根节点范围用 0 代替。显然对于 j j j , f [ i ] [ j ] = f [ f [ i ] [ j − 1 ] ] [ j − 1 ] f[i][j] = f[f[i][j - 1]][j - 1] f[i][j]=f[f[i][j−1]][j−1],预处理后的大致流程为:
- 设 d [ u ] d[u] d[u] 表示 u u u 的深度,设 d [ u ] ≤ d [ v ] d[u] \leq d[v] d[u]≤d[v],即 v v v 的深度更大;否则交换二者。
- 利用二进制拆分的思想,把 v v v 向上调整到和 u u u 同一深度,设需要向上跳 x x x 次,对 x x x 二进制拆分,只需要正确的按二的次幂跳,因为已知 u , v u, v u,v 的深度,所以可以 w h i l e ( d [ u ] < d [ v ] ) v = f [ v ] [ l o g 2 ( d [ v ] − d [ u ] ) ] while(d[u] < d[v]) ~~v = f[v][log_2(d[v] - d[u])] while(d[u]<d[v]) v=f[v][log2(d[v]−d[u])]。
- 若此时 u = v u = v u=v,则 u u u 就是二者的最近公共祖先。
- 否则一定存在 x x x 使得二者向上操作 x x x 次后第一次相会,这时的节点编号就是最近公共祖先。所以用类似二进制差分的思想,按二进制高位到低位,若不会使二者相会,则两节点均向上跳,否则无需再跳。
- 这时 u , v u,v u,v 一定只差一步就能相会,输出 f [ u ] [ 0 ] f[u][0] f[u][0] 即为答案。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef pair<ll, ll> pii;
const double eps = 1e-8;
const int Mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 5e5 + 10;
int n;
int Log[maxn], d[maxn], f[maxn][20];
vector<int> G[maxn];
void dfs(int u, int fa) {
d[u] = d[fa] + 1;
f[u][0] = fa;
for (auto v : G[u])
if (v != fa) dfs(v, u);
}
void init() {
for (int i = 2; i <= n; i++) Log[i] = Log[i / 2] + 1;
for (int j = 1; j < 20; j++)
for (int i = 1; i <= n; i++) {
f[i][j] = f[f[i][j - 1]][j - 1];
}
}
int lca(int u, int v) {
if (d[u] > d[v]) swap(u, v);
while (d[u] < d[v]) v = f[v][Log[d[v] - d[u]]];
if (u == v) return u;
for (int j = 19; j >= 0; j--)
if (f[u][j] != f[v][j]) {
u = f[u][j], v = f[v][j];
}
return f[u][0];
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int m, root;
cin >> n >> m >> root;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
dfs(root, 0);
init();
int u, v;
while (m--) {
cin >> u >> v;
cout << lca(u, v) << ENDL;
}
return 0;
}
洛谷 P1081 [NOIP2012 提高组] 开车旅行
题目大意
给定 n ( 1 ≤ n ≤ 1 e 5 ) n(1 \leq n \leq 1e5) n(1≤n≤1e5) 个位置,每个位置的海拔是 h i ( − 1 e 9 ≤ h i ≤ 1 e 9 ) h_i(-1e9 \leq h_i \leq 1e9) hi(−1e9≤hi≤1e9),两个位置之间的距离是二者之间的差值绝对值,任意两个城市的海拔不会相同。两个人小 A 和小 B 交替驾车,且只会向右行驶。小 A 只会向右边第二近的位置行驶,小 B 只会向右边最近的位置行驶(相同的距离,海拔越低的城市优先级越高),车能行驶的距离会给出。若车无法行驶到下一个城市或者后面没有了最近(次近)的城市,则旅途停止。需要解决两个问题:
- 给定距离 x 0 x_0 x0 问从哪个城市出发,使得小 A、小 B行驶距离比值越小。(分母为 0 视为无穷大)
- 有 m ( 1 ≤ m ≤ 1 e 5 ) m(1 \leq m \leq 1e5) m(1≤m≤1e5) 次询问,每次给出一个城市编号 s i s_i si 和 车的最大行驶距离 x i x_i xi,分别输出小 A、小 B行驶的距离。
解题思路
首先确定本题的思路,如果知道车的行驶距离,只需要知道从每个城市出发最远能走到哪个城市。但两人交替开车,第一个子问题肯定是如何确定每个城市后面的最近和次近城市?
子问题一
抛出一个问题:给出一个序列,如何求出每个数的前缀中有多少个数大于它。
思路:对于每个前缀,我们排序然后二分即可,但是如果操作 n n n 个前缀肯定超时,这时我们可以利用 s e t / m u l t i s e t set/multiset set/multiset 逐个插入,每次二分即可。
但是回归本题,需要的是限定条件下的最近和次近,因为城市海拔互不相同,所以我们二分上下界都行,设这时二分的迭代器的位置是 p o s pos pos,那么它的前一个迭代器为 p r e pre pre,最近的城市一定在这两个城市之中;取出最近后,设 p r e pre pre 的前一个迭代器为 P r e Pre Pre, p o s pos pos 的后一个迭代器为 n x t nxt nxt,那么次近的城市一定在三者之中(这两个加上除去 p o s pos pos 和 p r e pre pre 的其中之一)。
上述过程的直接维护十分繁琐,因为迭代器的物理地址是离散的,所以要考虑很多种情况。我的第一次写法调了几个小时都不对,心态血崩。到了第二天突然想到,这实际上和给定不多于四个数,按限定条件取出前两个是一样的,我们无需写很多 i f − e l s e if-else if−else,只需要用动态数组存起来,然后排序取前两个即可。
然后提一下,迭代器的前一个和后一个可以用如下两个函数:
pre = prev(pos), nxt = next(pos)
子问题二
知道了每个城市右边的最近和次近,如果考虑一个城市作为起点,向右一直走直到不能走,这中间的城市构成了一个链表,链表的每条边都有一个权值,给定了车的行驶距离可以依靠这个权值判断停止。肯定不能暴力维护链表,于是我们可以利用倍增,预处理出每个位置向右跳 2 j 2^j 2j 次后二人行驶的距离,然后给定了城市和车的距离我们就能 l o g ( n ) log(n) log(n)的复杂度内求出两人行驶距离了。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef pair<ll, ll> pii;
const double eps = 1e-8;
const int Mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 1e5 + 10;
set<int> st;
map<int, int> idx;
int cmp_num;
int h[maxn], suf[maxn][2], city[maxn][20];
ll d1[maxn][20], d2[maxn][20];
bool cmp(int p, int q) {
return abs(p - cmp_num) == abs(q - cmp_num) ? p < q : abs(p - cmp_num) < abs(q - cmp_num);
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m, s, x;
cin >> n;
for (int i = 1; i <= n; i++) cin >> h[i], idx[h[i]] = i;
st.insert(h[n]);
suf[n][0] = suf[n][1] = 0;
vector<int> res;
for (int i = n - 1; i >= 1; i--) {
auto pos = st.upper_bound(h[i]);
auto pre = prev(pos), nxt = next(pos);
auto Pre = prev(pre);
res.clear();
if (pos == st.end()) {
if (pre == st.begin())
res.push_back(*pre);
else
res.push_back(*pre), res.push_back(*Pre);
} else if (pos == st.begin()) {
if (nxt == st.end())
res.push_back(*pos);
else
res.push_back(*pos), res.push_back(*nxt);
} else {
res.push_back(*pos), res.push_back(*pre);
if (pre != st.begin()) res.push_back(*Pre);
if (nxt != st.end()) res.push_back(*nxt);
}
cmp_num = h[i];
sort(res.begin(), res.end(), cmp);
suf[i][0] = idx[res[0]];
suf[i][1] = res.size() > 1 ? idx[res[1]] : 0;
st.insert(h[i]);
}
for (int i = 1; i <= n; i++) {
city[i][0] = suf[i][1];
d1[i][0] = suf[i][1] ? abs(h[i] - h[suf[i][1]]) : 0;
d2[i][0] = 0;
}
for (int i = 1; i <= n; i++) {
int nxt = city[i][0];
city[i][1] = suf[nxt][0];
d1[i][1] = d1[i][0];
d2[i][1] = suf[nxt][0] ? abs(h[nxt] - h[suf[nxt][0]]) : 0;
}
for (int j = 2; j < 20; j++) {
for (int i = 1; i <= n; i++) {
int nxt = city[i][j - 1];
city[i][j] = city[nxt][j - 1];
d1[i][j] = d1[i][j - 1] + d1[nxt][j - 1];
d2[i][j] = d2[i][j - 1] + d2[nxt][j - 1];
}
}
//从哪个城市出发最多走x能使得S_A : S_B最小,第二关键字为海拔最高
cin >> x;
ll p1 = 1e12, q1 = 0, city_id = 0;
for (int i = 1; i <= n; i++) {
int pos = i;
ll sum = x;
ll p2 = 0, q2 = 0;
for (int j = 19; j >= 0; j--) {
if (city[pos][j] && sum >= d1[pos][j] + d2[pos][j]) {
p2 += d1[pos][j], q2 += d2[pos][j];
sum -= d1[pos][j] + d2[pos][j];
pos = city[pos][j];
}
}
if (q1 == 0 && q2 == 0 && h[city_id] < h[i])
city_id = i;
else if (q2 != 0 && p1 * q2 > p2 * q1) {
p1 = p2, q1 = q2;
city_id = i;
}
}
cout << city_id << ENDL;
cin >> m;
while (m--) {
cin >> s >> x;
ll ans1 = 0, ans2 = 0;
for (int j = 19; j >= 0; j--) {
if (city[s][j] && x >= d1[s][j] + d2[s][j]) {
ans1 += d1[s][j], ans2 += d2[s][j];
x -= d1[s][j] + d2[s][j];
s = city[s][j];
}
}
cout << ans1 << " " << ans2 << ENDL;
}
return 0;
}
Codeforces 1516D. Cut
题目大意
给出一个长度为 n ( 1 ≤ n ≤ 1 e 5 ) n(1 \leq n \leq 1e5) n(1≤n≤1e5) 的序列 a i ( 1 ≤ a i ≤ 1 e 5 ) a_i (1 \leq a_i \leq 1e5) ai(1≤ai≤1e5)。给出 q ( 1 ≤ q ≤ 1 e 5 ) q(1 \leq q \leq 1e5) q(1≤q≤1e5) 次询问,每次询问给出区间的左右端点 l , r l,r l,r,问这个区间最少被分为多少个子数组,使得每个子数组中每个数的乘积都恰好等于他们的 l c m lcm lcm。
解题思路
第一个需要解决的问题是,给定一个区间如何去分?不难想到肯定是贪心的向后选,直到选到一个数和上一段数的乘积不等于公倍数,然后另起一段区间。暴力贪心肯定不行,可以用类似链表的思想,记录每个数右边第一个不互质数的位置 N e x t [ i ] Next[i] Next[i],每次跳到这个位置代表需要重新一段子数组;但是最终需要的是一段区间能向右延伸的最小位置,从右向左更新一遍这个位置就可以了,即每次和右边相邻的数能跳的最远位置取最小 N e x t [ i ] = m i n ( N e x t [ i ] , N e x t [ i + 1 ] ) Next[i] = min(Next[i], Next[i + 1]) Next[i]=min(Next[i],Next[i+1])。
维护好这个东西之后,只需要从左端点向右跳,优化了跳跃的复杂度。但是最坏的情况下,若所有数都是偶数,那么每次只能跳一个位置,这样仍是暴力的复杂度!
考虑使用类似ST表那样的倍增优化:设 d [ i ] [ j ] ( 1 ≤ i ≤ n , 2 j ≤ n ) d[i][j]( 1 \leq i \leq n, 2^j \leq n) d[i][j](1≤i≤n,2j≤n) 表示从位置 i i i 向后跳 2 j 2^j 2j 次能到达的位置,初始化 d [ i ] [ 0 ] = N e x t [ i ] d[i][0] = Next[i] d[i][0]=Next[i];状态转移方程为 d [ i ] [ j ] = d [ d [ i ] [ j − 1 ] ] [ j − 1 ] d[i][j] = d[d[i][j-1]][j - 1] d[i][j]=d[d[i][j−1]][j−1] 。预处理之后就能 O ( l o g n ) O(logn) O(logn) 查询了,每次从当前起点 l l l 跳跃最大的 2 k 2^k 2k,更新 l = d [ l ] [ k ] l = d[l][k] l=d[l][k],直到跳出 r r r。
因为若最远位置已经超过 n n n,要设置为 n + 1 n + 1 n+1,于是DP时位置要考虑到 [ 1 , n + 1 ] [1,n+1] [1,n+1]。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
typedef long long ll;
const int Mod = 1e9 + 7;
const int maxn = 1e5 + 10;
int n;
int d[maxn][20];
int a[maxn], Next[maxn];
vector<int> p[maxn];
void init() {
for (int i = 2; i < maxn; i++) {
if (!p[i].size()) {
Next[i] = n + 1;
for (int j = i; j < maxn; j += i) p[j].push_back(i);
}
}
d[n + 1][0] = n + 1;
for (int i = n; i >= 1; i--) {
d[i][0] = d[i + 1][0];
for (auto j : p[a[i]]) {
d[i][0] = min(d[i][0], Next[j]);
Next[j] = i;
}
}
for (int j = 1; j < 20; j++) {
for (int i = 1; i <= n + 1; i++) {
d[i][j] = d[d[i][j - 1]][j - 1];
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int q;
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
init();
int l, r;
while (q--) {
cin >> l >> r;
int ans = 0;
for (int j = 19; j >= 0; j--) {
if (d[l][j] <= r) {
ans += (1 << j);
l = d[l][j];
}
}
cout << ans + 1 << ENDL;
}
return 0;
}
AcWing109. 天才ACM
题目大意
给出一个整数 M M M ,对于任意一个集合 S S S,定义校验值如下:从集合中取出 M M M 对数,不能重复使用集合中的数,使得每对数的差的平方和最大,这个最大值就称为集合 S S S 的校验值。若不够 M M M 对取到不能取为止,若超过 M M M 对则只取 M M M 对。
现在给定一个长度为 N ( 1 ≤ N ≤ 5 e 5 ) N( 1\leq N \leq 5e5) N(1≤N≤5e5) 的数列 A ( 0 ≤ A i ≤ 2 20 A(0 \leq A_i \leq 2^{20} A(0≤Ai≤220 以及一个整数 T ( 0 ≤ T ≤ 1 0 18 ) T(0 \leq T \leq 10^{18}) T(0≤T≤1018),问最少把 N N N 分为多少段,使得每一段的校验值都不超过 T T T。
解题思路
根据贪心,从第一个数开始能向右找的数作为第一段越多越好,然后以此类推。
对于一个子区间 a a a,如何求出校验值?每对数差的平方和最大,展开平方和公式,得到若 a i ∗ a j a_i*a_j ai∗aj 最小则差的平方和最大,联系排序不等式,每次取出最大值和最小值,累加平方和即可。
求出一段区间的校验值最少需要 O ( n l o g n ) O(nlogn) O(nlogn) 的复杂度。显然我们可以对每个起点的位置,在后缀中二分答案每次二分 O ( m l o g m ) O(mlogm) O(mlogm) (m为起点到二分的 m i d mid mid 的距离)检查答案。看似可以通过本题,但是只是平均的情况下可以通过。考虑最坏的情况 T = 0 T = 0 T=0,那么每次都要检查 l o g log log 次,这样的时间复杂度是接近 O ( n 2 l o g n ) O(n^2logn) O(n2logn) 的。
采用如下的方式倍增:
- 初始化 p = 1 , R = L p = 1, R= L p=1,R=L
- 求出 [ L , R + p ] [L, R + p] [L,R+p] 这一段的校验值,若小于等于 T T T,则 R + = p , p = p ∗ 2 R += p, p = p * 2 R+=p,p=p∗2;否则 p = p / 2 p = p/ 2 p=p/2。
- 重复上一步,直到 p p p 的值变为 0,此时的 R R R 即为所求。
使用倍增可以达到我们期望用二分达到的时间复杂度 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)。
但是上述算法还可以优化,假设我们上次已经对区间 [ L , R ] [L, R] [L,R] 排序,接下来要检测的新的一段是 [ R + 1 , R + p ] [R + 1, R + p] [R+1,R+p] ,那么可以只对新增部分排序然后归并,这样可以使总体时间复杂度降低到 O ( n l o g n ) O(nlogn) O(nlogn),可以完美通过本题。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef pair<ll, ll> pii;
const double eps = 1e-8;
const int Mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 5e5 + 10;
int T, n, m;
int a[maxn], b[maxn], c[maxn];
ll val;
void merge(int l1, int r1, int l2, int r2) {
int i = l1, j = l2, k = l1;
while (i <= r1 && j <= r2) {
if (a[i] <= b[j])
c[k++] = a[i++];
else
c[k++] = b[j++];
}
while (i <= r1) c[k++] = a[i++];
while (j <= r2) c[k++] = b[j++];
}
bool check(int l1, int r1, int l2, int r2) {
int len = r2 - l2 + 1;
for (int i = l2; i <= r2; i++) b[i] = a[i];
sort(b + 1 + l2, b + 1 + r2);
merge(l1, r1, l2, r2);
int i = l1, j = r2, k = 0;
ll ans = 0;
while (i < j && k < m) {
ans += 1LL * (c[j] - c[i]) * (c[j] - c[i]);
i++, j--, k++;
}
return ans <= val;
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> T;
while (T--) {
cin >> n >> m >> val;
for (int i = 1; i <= n; i++) cin >> a[i];
int ans = 0;
for (int i = 1; i <= n; i++) {
int p = 1, j = i;
while (p) {
if (j + p <= n && check(i, j, j + 1, j + p)) {
j += p;
for (int k = i; k <= j; k++) a[k] = c[k];
p <<= 1;
} else
p >>= 1;
}
i = j;
ans++;
}
cout << ans << endl;
}
return 0;
}
洛谷 P1613 跑路
题目大意
给定一个 n ( n ≤ 50 ) n(n \leq 50) n(n≤50) 个节点 m ( 1 ≤ m ≤ 10000 ) m(1 \leq m \leq 10000) m(1≤m≤10000) 条边的有向图,只要走 2 k 2^k 2k 米花费的时间就是一秒,问从 1 到 n 最少需要几秒(保证一定有路径)
解题思路
首先发现数据范围很小。本题的思路还是很巧的,起码本蒟蒻得看题解否则想不到。问题肯定是从 1 到 n 走最少的二的幂次,为了确保答案可以在若干个环上一直转圈,似乎正常的思路很难下手。
对于两个点,走一个 2 的幂次肯定是最优的,于是我们可以预处理出两两之间,能否通过走 2 的某个幂次到达,无论哪个幂次都是最优的。这个是稍微变动的弗洛伊德。
然后发现从 1 到 n 肯定是走若干个 2 的幂次,于是我们根据上面的更新跑一次最短路即可。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef pair<ll, ll> pii;
const double eps = 1e-8;
const int Mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 5e4 + 10;
int d[55][55][65], G[55][55];
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
memset(G, 0x3f, sizeof G);
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
d[u][v][0] = G[u][v] = 1;
}
for (int p = 0; p < 65; p++)
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (d[i][k][p] && d[k][j][p]) {
d[i][j][p + 1] = 1;
G[i][j] = 1;
}
}
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
cout << G[1][n] << endl;
return 0;
}