倍增问题总结

倍增

当我们在进行递推时,如果状态空间很大,线性递推会超时,那么就选择成倍的增长方式,以递推状态空间中 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][j1]][j1],预处理后的大致流程为:

  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 的深度更大;否则交换二者。
  2. 利用二进制拆分的思想,把 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])]
  3. 若此时 u = v u = v u=v,则 u u u 就是二者的最近公共祖先。
  4. 否则一定存在 x x x 使得二者向上操作 x x x 次后第一次相会,这时的节点编号就是最近公共祖先。所以用类似二进制差分的思想,按二进制高位到低位,若不会使二者相会,则两节点均向上跳,否则无需再跳。
  5. 这时 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(1n1e5) 个位置,每个位置的海拔是 h i ( − 1 e 9 ≤ h i ≤ 1 e 9 ) h_i(-1e9 \leq h_i \leq 1e9) hi(1e9hi1e9),两个位置之间的距离是二者之间的差值绝对值,任意两个城市的海拔不会相同。两个人小 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(1m1e5) 次询问,每次给出一个城市编号 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 ifelse,只需要用动态数组存起来,然后排序取前两个即可。

然后提一下,迭代器的前一个和后一个可以用如下两个函数:

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(1n1e5) 的序列 a i ( 1 ≤ a i ≤ 1 e 5 ) a_i (1 \leq a_i \leq 1e5) ai(1ai1e5)。给出 q ( 1 ≤ q ≤ 1 e 5 ) q(1 \leq q \leq 1e5) q(1q1e5) 次询问,每次询问给出区间的左右端点 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](1in,2jn) 表示从位置 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][j1]][j1] 。预处理之后就能 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(1N5e5) 的数列 A ( 0 ≤ A i ≤ 2 20 A(0 \leq A_i \leq 2^{20} A(0Ai220 以及一个整数 T ( 0 ≤ T ≤ 1 0 18 ) T(0 \leq T \leq 10^{18}) T(0T1018),问最少把 N N N 分为多少段,使得每一段的校验值都不超过 T T T

解题思路

根据贪心,从第一个数开始能向右找的数作为第一段越多越好,然后以此类推。

对于一个子区间 a a a,如何求出校验值?每对数差的平方和最大,展开平方和公式,得到若 a i ∗ a j a_i*a_j aiaj 最小则差的平方和最大,联系排序不等式,每次取出最大值和最小值,累加平方和即可。

求出一段区间的校验值最少需要 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) 的。

采用如下的方式倍增:

  1. 初始化 p = 1 , R = L p = 1, R= L p=1,R=L
  2. 求出 [ 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=p2;否则 p = p / 2 p = p/ 2 p=p/2
  3. 重复上一步,直到 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(n50) 个节点 m ( 1 ≤ m ≤ 10000 ) m(1 \leq m \leq 10000) m(1m10000) 条边的有向图,只要走 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;
}

猜你喜欢

转载自blog.csdn.net/qq_44691917/article/details/119889781
今日推荐