主席树

主席树

1.算法分析

​ 主席树借用前缀和+节约空间的思想, 建立n棵权值线段树,对于[l, r]区间的所有数字,就是第r棵权值线段树-第l-1棵权值线段树,然后查询第k小的时候按照权值线段树查询的思路查询。

b6555468-33cd-453e-a2ff-cff548795ad4.png 6a2bf3fa-2c36-4039-9c62-5266188f8b46.png 82af73e2-a351-48bd-ac63-ae82e8c806f8.png

2.模板

2.1 区间静态第k小

#include <bits/stdc++.h>

using namespace std;

int const MAXN = 1e5 + 10;
int n, m, T, hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], sum[MAXN * 40], a[MAXN], tot;
vector<int> v;

// 在rt子树内插入当前的值pos,使之增加v
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

// 在[pre, now]这颗子树内查询第k小
int query(int pre, int now, int l, int r, int k) {
    if (l == r) return l;
    int lsum = sum[L[now]] - sum[L[pre]];  // 左子树的数目
    int mid = (l + r) >> 1;
    if (lsum >= k) return query(L[pre], L[now], l, mid, k);  // 如果左子树的size小于等于k,说明第k小在左子树内
    else return query(R[pre], R[now], mid + 1, r, k - lsum);  // 否则在右子树内
}

signed main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cin >> T;
    while(T--) {
        tot = 0;
        v.clear();
        cin >> n >> m;  // 读入个数和询问次数
        for (int i = 1; i <= n; ++i) cin >> a[i], v.push_back(a[i]);  // 读入数组,离散化
        sort(v.begin(), v.end());
        v.erase(unique(v.begin(), v.end()), v.end());
        int num = v.size();  // 离散化后的值域范围[1 ~ num]

        for (int i = 1; i <= n; ++i) {
            a[i] = lower_bound(v.begin(), v.end(), a[i]) - v.begin() + 1;
            update(hjt[i], hjt[i - 1], 1, num, a[i], 1);  // 插入主席树
        }

        for (int i = 1, l, r, k; i <= m; ++i) {
            cin >> l >> r >> k;
            cout << v[query(hjt[l - 1], hjt[r], 1, num, k) - 1] << endl;  // 查询[l, r]区间的第k小数字
        }
    }
    return 0;
}

2.2 区间动态第k小

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 100007;

int n, m, num, n1, n2;
int a[MAXN], b[MAXN << 1], c[MAXN], d[MAXN], e[MAXN], t1[MAXN], t2[MAXN];
int Top, Root[MAXN], val[MAXN * 400], ls[MAXN * 400], rs[MAXN * 400];

inline int lowbit(int x) { return x & (-x); }

void Add(int &rt, int l, int r, int ind, int c) {
    if (!rt) rt = ++Top;
    val[rt] += c;
    if (l == r) return;
    int m = (l + r) >> 1;
    if (ind <= m)
        Add(ls[rt], l, m, ind, c);
    else
        Add(rs[rt], m + 1, r, ind, c);
}

void Change(int ind, int val) {
    int x = lower_bound(b + 1, b + 1 + num, a[ind]) - b;
    for (int i = ind; i <= n; i += lowbit(i)) Add(Root[i], 1, num, x, val);
}

int Kth(int l, int r, int k) {  // 求第 k 大
    if (l == r) return l;
    int m = (l + r) >> 1, sum = 0;
    for (int i = 1; i <= n2; ++i) sum += val[ls[t2[i]]];
    for (int i = 1; i <= n1; ++i) sum -= val[ls[t1[i]]];
    if (sum >= k) {
        for (int i = 1; i <= n1; ++i)  // 所有树的节点保持对应
            t1[i] = ls[t1[i]];
        for (int i = 1; i <= n2; ++i) t2[i] = ls[t2[i]];
        return Kth(l, m, k);
    } else {
        for (int i = 1; i <= n1; ++i) t1[i] = rs[t1[i]];
        for (int i = 1; i <= n2; ++i) t2[i] = rs[t2[i]];
        return Kth(m + 1, r, k - sum);
    }
}

// 查询[l, r]的第k小
int Kth_pre(int l, int r, int k) {
    n1 = n2 = 0;
    for (int i = l - 1; i >= 1; i -= lowbit(i))  // 处理出需要求和的 n1 棵树
        t1[++n1] = Root[i];
    for (int i = r; i >= 1; i -= lowbit(i)) t2[++n2] = Root[i];
    return Kth(1, num, k);
}

int main() {
    // 读入
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        b[++num] = a[i];
    }
    for (int i = 1; i <= m; ++i) {
        char ch = getchar();
        while (ch != 'Q' && ch != 'C') ch = getchar();
        if (ch == 'Q')
            scanf("%d%d%d", &c[i], &d[i], &e[i]);  // 查询[c[i], d[i]]内的第e[i]小
        else {
            scanf("%d%d", &c[i], &d[i]);  // 修改a[c[i]] = d[i]
            b[++num] = d[i];  // 对于所有出现过的值(包括插入操作中的值)离散化
        }
    }
    // 离散化
    sort(b + 1, b + 1 + num);
    num = unique(b + 1, b + 1 + num) - b - 1;
    // 建树
    for (int i = 1; i <= n; ++i) Change(i, 1);
    // 处理操作&询问 因为要离散化所以要离线处理
    for (int i = 1; i <= m; ++i) {
        if (e[i]) {
            printf("%d\n", b[Kth_pre(c[i], d[i], e[i])]);  // 查询[c[i], d[i]]内的第e[i]小
        } else {
            Change(c[i], -1);
            a[c[i]] = d[i];  // 修改a[c[i]] = d[i]
            Change(c[i], 1);
        }
    }
    return 0;
}

2.3 树上第k小

#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 5;
int n, q, a[N], b[N], sz;
vector<int> g[N];
int rt[N], L[N * 20], R[N * 20], sum[N * 20], tot = 0;
int dep[N], f[N][20];

void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

// 查询树上路径 u ~ v 第k小,lca为u、v的lca,flca为lca的父节点
int query(int u, int v, int lca, int flca, int l, int r, int k) {
    if (l == r) return b[l];
    int m = (l + r) / 2;
    int num = sum[L[u]] - sum[L[lca]] + sum[L[v]] - sum[L[flca]];
    if (num >= k) return query(L[u], L[v], L[lca], L[flca], l, m, k);
    return query(R[u], R[v], R[lca], R[flca], m + 1, r, k - num);
}

// dfs预处理lca数组,顺便建立主席树
void dfs(int u, int fa) {
    dep[u] = dep[fa] + 1;
    f[u][0] = fa;
    for (int i = 1; i < 20; i++) f[u][i] = f[f[u][i - 1]][i - 1];
    int pos = lower_bound(b + 1, b + 1 + sz, a[u]) - b;
    update(rt[u], rt[fa], 1, sz, pos, 1);  // 在树上建立主席树,前一个权值线段树是当前节点的父节点
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
    }
}

int LCA(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);  ///注意是交换u和v,不是交换dep[u]和dep[v]
    int d = dep[u] - dep[v];
    for (int i = 0; i < 20; i++)  ///先调整到同一深度
        if (d & (1 << i)) u = f[u][i];
    if (u == v) return u;
    for (int i = 19; i >= 0; i--) {  ///注意是倒着for,二进制拆分,从大到小尝试
        if (f[u][i] != f[v][i]) {
            u = f[u][i];
            v = f[v][i];
        }
    }
    return f[u][0];
}

int main() {
    scanf("%d%d", &n, &q);

    // 读入、离散化
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
    sort(b + 1, b + 1 + n);
    sz = unique(b + 1, b + 1 + n) - b - 1;

    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    int last = 0, u, v, k, lca, flca;
    while (q--) {
        scanf("%d%d%d", &u, &v, &k);  // u、v、v,查询u ~ v的路径第k小
        u ^= last;
        lca = LCA(u, v);
        flca = f[lca][0];
        last = query(rt[u], rt[v], rt[lca], rt[flca], 1, sz, k);  // 查询树上路径第k小
        printf("%d\n", last);
    }
    return 0;
}

2.4 魔改

2.4.1 区间小于x的最大值
// 找到区间小于k的最大值
int query(int s, int e, int l, int r, int k) {  
    if (l == r) {
        if (k == l) return 0;  // 如果找到叶子节点且不是k,那么表示无解
        return l;
    }
    int lsum = sum[L[e]] - sum[L[s]];  // 左子树
    int rsum = sum[R[e]] - sum[R[s]];  // 右子树
    int mid = (l + r) / 2;
    int tmp = 0;
    if (rsum && mid < k) tmp = query(R[s], R[e], mid + 1, r, k);  // 如果mid值小于k且右子树有东西,那么先去右子树找
    if (lsum && !tmp) tmp = query(L[s], L[e], l, mid, k);  // 如果不在右子树,且左子树有东西,那么就去左子树找
    return tmp;
}

int main() {
    ...
    int num = query(l - 1, r, 1, n, k);  // 找到区间[l, r]内小于k的最大值
}
2.4.1 区间mex

见3.1例题:BZOJ 3585 mex

2.4.2 区间互为因子或者倍数的对数

见3.1例题:icpc2019徐州区域赛网络赛 I.query

2.4.3 主席树+差分

见3.1例题:洛谷P3168 [CQOI2015]任务查询系统

3.典型例题

3.1 区间静态第k小

hdu2665 Kth number

题意: 给定n个数字,给定q个询问,每次询问给定一个区间[l, r]和k,要求找到[l, r]区间内的第k小。 1 < = n , m < = 1 0 5 1<=n,m<=10^5 1<=n,m<=105

题解: 区间第k小模板

代码:

#include <bits/stdc++.h>

using namespace std;

int const MAXN = 1e5 + 10;
int n, m, T, hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], sum[MAXN * 40], a[MAXN], tot;
vector<int> v;

// 在rt子树内插入当前的值pos,使之增加v
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

// 在[pre, now]这颗子树内查询第k小
int query(int pre, int now, int l, int r, int k) {
    if (l == r) return l;
    int lsum = sum[L[now]] - sum[L[pre]];  // 左子树的数目
    int mid = (l + r) >> 1;
    if (lsum >= k) return query(L[pre], L[now], l, mid, k);  // 如果左子树的size小于等于k,说明第k小在左子树内
    else return query(R[pre], R[now], mid + 1, r, k - lsum);  // 否则在右子树内
}

signed main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cin >> T;
    while(T--) {
        tot = 0;
        v.clear();
        cin >> n >> m;  // 读入个数和询问次数
        for (int i = 1; i <= n; ++i) cin >> a[i], v.push_back(a[i]);  // 读入数组,离散化
        sort(v.begin(), v.end());
        v.erase(unique(v.begin(), v.end()), v.end());
        int num = v.size();  // 离散化后的值域范围[1 ~ num]

        for (int i = 1; i <= n; ++i) {
            a[i] = lower_bound(v.begin(), v.end(), a[i]) - v.begin() + 1;
            update(hjt[i], hjt[i - 1], 1, num, a[i], 1);  // 插入主席树
        }

        for (int i = 1, l, r, k; i <= m; ++i) {
            cin >> l >> r >> k;
            cout << v[query(hjt[l - 1], hjt[r], 1, num, k) - 1] << endl;  // 查询[l, r]区间的第k小数字
        }
    }
    return 0;
}

The Preliminary Contest for ICPC Asia Nanjing 2019 F. Greedy Sequence

题意: 给出n,k,然后给出一个长度为n的序列A,构造n个序列Si。要求Si序列的开头必须是Ai,且Si序列递减,Si中元素在位置上距离必须小于等于k,问每个序列的最大长度。 1 < = n < = 1 0 5 1 <= n <= 10^5 1<=n<=105

题解: 从小到大找答案,对于答案i开头的序列,通过主席树找到[pos[i]-k,pos[i]+k]区间内小于i的最大值(pos[i]表示i的位置),主席树查询的时候,先贪心往右查询,如果往右找不到,再往左找。为了优化时间,再对答案记忆化一下。

代码:

#include <bits/stdc++.h>

using namespace std;

int const MAXN = 1e5 + 10;
int n, m, T, hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], sum[MAXN * 40], a[MAXN], tot, k, pos[MAXN], res[MAXN];

// 在rt子树内插入当前的值pos,使之增加v
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

int query(int pre, int now, int l, int r, int k) {
    if (l == r) {
        if (k == l) return 0;  // 如果找到叶子节点且不是k,那么表示无解
        return l;
    }
    int lsum = sum[L[now]] - sum[L[pre]];  // 左子树
    int rsum = sum[R[now]] - sum[R[pre]];  // 右子树
    int mid = (l + r) / 2;
    int tmp = 0;
    if (rsum && mid < k) tmp = query(R[pre], R[now], mid + 1, r, k);  // 如果mid值小于k且右子树有东西,那么先去右子树找
    if (lsum && !tmp) tmp = query(L[pre], L[now], l, mid, k);  // 如果不在右子树,且左子树有东西,那么就去左子树找
    return tmp;
}

int main() {
    cin >> T;
    while (T--) {
        tot = 0;
        cin >> n >> k;
        for (int i = 1; i <= n; i++) {
            scanf("%d", a + i);
            pos[a[i]] = i;
            res[i] = 0;
            update(hjt[i], hjt[i - 1], 1, n, a[i], 1);
        }
        for (int i = 1; i <= n; ++i) {  // 遍历权值
            int l = max(1, pos[i] - k), r = min(n, pos[i] + k);
            int s = query(hjt[l - 1], hjt[r], 1, n, i);
            res[i] = res[s] + 1;  // 记忆化
            printf("%d", res[i]);
            if (i < n) printf(" ");
            else printf("\n");
        }
    }
    return 0;
}

Codeforces 840D Destiny
题意: 给出一个长为n的数组,q次询问,每次问[l, r]区间出现次数超过(r-l+1)/k的最小数是多少,不存在返回-1。 1 < = n < = 3 ∗ 1 0 5 1<=n<=3*10^5 1<=n<=3105
分析: 主席树(权值线段树)上进行搜索,先往左子树(值小的)优先搜索,加个剪枝,如果区间内出现的次数已经小于(r-l+1)/k,就直接返回。
代码:

#include <bits/stdc++.h>

using namespace std;

int const MAXN = 3e5 + 10;
int n, m, T, hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], sum[MAXN * 40], a[MAXN], tot;
vector<int> v;

// 在rt子树内插入当前的值pos,使之增加v
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

int query(int pre, int now, int l, int r, int k) {
    if (l == r) return l;
    int lsum = sum[L[now]] - sum[L[pre]];  // 左子树大小,如果求右子树大小就是sum[R[e]] - sum[R[s]]
    int rsum = sum[R[now]] - sum[R[pre]];
    int mid = (l + r) / 2;
    if (lsum > k) {
        int res = query(L[pre], L[now], l, mid, k);
        if (res != -1) return res;
    }
    if (rsum > k) {
        int res = query(R[pre], R[now], mid + 1, r, k);
        if (res != -1) return res;
    }
    return -1;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) scanf("%d", a + i), update(hjt[i], hjt[i - 1], 1, n, a[i], 1);
    while (m--) {  
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        int ans = query(hjt[l - 1], hjt[r], 1, n, (r - l + 1) / k);
        printf("%d\n", ans);
    }
    return 0;
}

**SPOJ - DQUERY **
题意: 给定n个数字,q个询问,每个询问给定l和r,问[l, r]内不同数字有多少个。
题解: 主席树保存的是每个数出现的位置,区间[l,r]表示位置属于这个区间的数有多少个,因为要去掉重复数的影响,所以在当前这颗线段树上要在对应区间(相同数上一次出现的位置)减去影响(-1),再在这个位置相应区间加上影响(+1)。可以发现,这样构造时,当r确定了,那么[l, r]内不同元素的出现次数为大于等于l的数目,那么使用线段树区间查询操作即可,所以直接在r的线段树内去查。

代码:

#include <bits/stdc++.h>

using namespace std;

int const MAXN = 1e6 + 10;
int n, m, T, hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], sum[MAXN * 40], a[MAXN], tot, last[MAXN];
vector<int> v;

// 在rt子树内插入当前的值pos,使之增加v
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

//二分查找
int query(int rt, int l, int r, int qL, int qR) {
    if (qL <= l && r <= qR) return sum[rt];
    int res = 0;
    int mid = (l + r) / 2;
    if (qL <= mid) res += query(L[rt], l, mid, qL, qR);
    if (mid < qR) res += query(R[rt], mid + 1, r, qL, qR);
    return res;
}

inline int read() {
   int s = 0, w = 1;
   char ch = getchar();
   while (ch < '0' || ch > '9') {if (ch == '-') w = -1; ch = getchar();}
   while (ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
   return s * w;
}

int main() {
    n = read();
    for (int i = 1; i <= n; i++) {
        a[i] = read();
        if (!last[a[i]]) update(hjt[i], hjt[i - 1], 1, n, i, 1);   // 主席树维护的是位置
        else {
            int tmp;
            update(tmp, hjt[i - 1], 1, n, last[a[i]], -1); 
            update(hjt[i], tmp, 1, n, i, 1); 
        }
        last[a[i]] = i;
    }
    m = read();
    while (m--) {  
        int l, r;
        l = read(), r = read();
        printf("%d\n", query(hjt[r], 1, n, l, r));  // 查询[l,r]区间时,就去第r棵线段树上找左右大于等于l的值即可
    }
    return 0;
}

BZOJ 3585 mex
题意: 有一个长度为n的数组{a1,a2,…,an}。m次询问,每次询问一个区间内最小没有出现过的自然数。 1 < = n , m < = 200000 , 0 < = a i < = 1 0 9 , 1 < = l < = r < = n 1<=n,m<=200000,0<=a_i<=10^9,1<=l<=r<=n 1<=n,m<=2000000<=ai<=1091<=l<=r<=n

题解: 主席树维护位置,主席树节点对应[l,r]区间表示值域在此范围内的每个数最后出现位置的最小值。查询的时候在rt[r]这颗线段树上面查询,如果区间每个数最后出现位置的最小值小于查询区间左端点l时,继续往左找,这样可以保证结果是最小的且未在区间[l,r]内出现过。
代码:

#include <bits/stdc++.h>

using namespace std;

int const MAXN = 2e5 + 10, MAXM = 1e9;
int n, m, T, tot, hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], Min[MAXN * 40];

void update(int &rt, int pre, int l, int r, int val, int pos) {
    rt = ++tot;
    L[rt] = L[pre], R[rt] = R[pre], Min[rt] = pos;
    if (l == r) return ;
    int mid = (l + r) >> 1;
    if (val <= mid) update(L[rt], L[pre], l, mid, val, pos);
    else update(R[rt], R[pre], mid + 1, r, val, pos);
    Min[rt] = min(Min[L[rt]], Min[R[rt]]);
}

int query(int rt, int l, int r, int limit) {
    if (l == r) return l;
    int mid = (l + r) >> 1;
    if (Min[L[rt]] < limit) return query(L[rt], l, mid, limit);
    else return query(R[rt], mid + 1, r, limit); 
}

signed main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cin >> n >> m;
    for (int i = 1, t; i <= n; ++i) {
        cin >> t;
        update(hjt[i], hjt[i - 1], 0, MAXM, t, i);  // 维护位置
    }
    for (int i = 1, l, r; i <= m; ++i) {
        cin >> l >> r;
        cout << query(hjt[r], 0, MAXM, l) << endl;  // 在第r棵权值线段树内查询出现位置小于等于l的最小的元素
    }
    return 0;
}

洛谷P3168 [CQOI2015]任务查询系统

题意: 有n个任务,一个任务用一个三元组(s, e, p)表示,意思是:从第s秒开始,到第e秒结束,其优先级为p。有m个询问,每个询问给定x a b c,则k = (a * pre + b) mod c,pre为上一次答案,要求计算出第x秒正在运行的任务中优先级最小的k个任务的优先级之和是多少。 1 < = m , n , s , e , p , a , b < = 1 0 5 , 1 < = p < = 1 0 7 1<=m,n,s,e,p,a,b<=10^5,1<=p<=10^7 1<=m,n,s,e,p,a,b<=105,1<=p<=107

题解: 差分+主席树。差分+主席树。我们对于每个时间点建立一颗主席树,那么要求出优先级最小的k个任务之和,只需要在权值线段树上维护节点数目和权值和即可。对于每个任务,都相当于区间修改,就是把区间[s, e]中每个点的权值线段树都放入一个权值为p的点。对于区间修改操作,我们可以使用差分来完成,而差分需要计算前缀和才能知道当前实践的任务,然而主席树就是权值线段树的前缀和,因此我在建立主席树的同时就是相当于求了个前缀和。对于询问,我们只需要去以x为根的权值线段树上查询即可。

代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
#define N 4000010

struct Node {
    LL vl, typ;
};
LL cnt = 0, tot = 0, maxt = 0, pre, b[N];
int hjt[N], L[N], R[N], siz[N];
LL sum[N];
vector<Node> G[N];
map<LL, LL> mp;

void update(int &rt, int pre, int l, int r, int val, int pos, int type) { 
    rt = ++cnt;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + val, siz[rt] = siz[pre] + type;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, val, pos, type);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, val, pos, type);  // 否则插入右子树
}

LL query(int now, int l, int r, int k) {
    if (k >= siz[now]) return sum[now];
    if (l == r) return (LL)sum[now] / siz[now] * k; 
    int lsum = siz[L[now]];  // 左子树的数目
    int mid = (l + r) >> 1;
    if (lsum >= k) return query(L[now], l, mid, k);  // 如果左子树的size小于等于k,说明第k小在左子树内
    else return sum[L[now]] + query(R[now], mid + 1, r, k - lsum);  // 否则在右子树内
}

int main() {
    LL n, m;
    cin >> n >> m;
    for (LL i = 1; i <= n; i++) {
        LL s, e, p;
        cin >> s >> e >> p;
        b[i] = p;
        G[s].push_back((Node){p, 1});  // 差分数组
        G[e + 1].push_back((Node){-p, -1});
        maxt = max(maxt, e + 1);  // 最大的作为权值边界
    }
    sort(b + 1, b + n + 1);
    for (LL i = 1; i <= n; i++)
        if (!mp[b[i]]) mp[b[i]] = ++tot;
    for (int i = 1; i <= maxt; i++) {
        hjt[i] = hjt[i - 1];
        for (int j = 0; j < G[i].size(); j++)
            update(hjt[i], hjt[i], 1, tot, G[i][j].vl, mp[labs(G[i][j].vl)], G[i][j].typ);
    }
    int pre = 1;
    for (LL i = 1; i <= m; i++) {
        LL x, a, b, c, k;
        cin >> x >> a >> b >> c;
        k = (a * pre + b) % c + 1;
        printf("%lld\n", pre = query(hjt[x], 1, tot, k));
    }
    return 0;
}

icpc2019徐州区域赛网络赛 I.query

题意: 求区间[l,r]内满足 m i n ( a [ i ] , a [ j ] ) = g c d ( a [ i ] , a [ j ] ) ( l < = i < j < = r ) min(a[i],a[j])=gcd(a[i],a[j])(l<=i<j<=r) min(a[i],a[j])=gcd(a[i],a[j])(l<=i<j<=r)的对数。 1 < = n < = 1 0 5 、 1 < = m < = 1 0 5 1<=n<=10^5、1<=m<=10^5 1<=n<=1051<=m<=105
题解: 题目实际上是求一个区间内一个数是另外一个数的倍数的有序对数,因为是一个排列,所以容易知道这样的对数不超过nlogn。我们可以建立主席树,每颗主席树保存在他前面是当前位置数的因子的数的位置,然后直接在第r颗线段树上查找位置在询问区间里面的个数就好了。为什么这么做可以呢?对于每一对元素(l, r),我们只保存它的l,因此如果出现一个l,就说明它的后面存在一个r。那么我现在权值线段树上存储的是数字的位置,我查询的区间是[L, R],那么每出现一个数字就说明在[L, R]这个区间里面的数字后面会对应一个数字和它组成一对,这就保证了找到了(l, r)的左边界的范围,而我是在根为r的权值线段树内去查询的,那么就保证了右边界的范围不超过R,两头一卡,找到的就是在[L, R]里面的对数

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;

int hjt[N], L[N * 200], R[N * 200], sum[N * 200], tot = 0;
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

// 查询权值线段树上权值在[qL, qR]范围的权值和
int query(int now, int l, int r, int qL, int qR) {
    if (qL <= l && qR >= r) return sum[now];
    int m = (l + r) / 2, ans = 0;
    if (qL <= m) ans += query(L[now], l, m, qL, qR);
    if (qR > m) ans += query(R[now], m + 1, r, qL, qR);
    return ans;
}

int n, q, l, r, a[N], p[N];
vector<int> g[N];
int main() {
    scanf("%d%d", &n, &q);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        p[a[i]] = i;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 2 * i; j <= n; j += i) {
            // 记录一下当前数字前面是它因子或者倍数的位置
            if (p[i] < p[j]) g[p[j]].push_back(p[i]);
            if (p[i] > p[j]) g[p[i]].push_back(p[j]);
        }
    }
    for (int i = 1; i <= n; i++) {
        // 把当前位置数字的因子或者倍数都插入到当前的权值线段树内
        hjt[i] = hjt[i - 1];  
        for (auto u : g[i]) update(hjt[i], hjt[i], 1, n, u, 1);
    }
    while (q--) {
        scanf("%d%d", &l, &r);
        int ans = query(hjt[r], 1, n, l, r);
        printf("%d\n", ans);
    }
    return 0;
}

3.2 区间动态第k小

luogu P2617 Dynamic Rankings

题意: 给定一个含有 n 个数的序列 a 1 , a 2 … , a n a_1,a_2 \dots, a_n a1,a2,an ,需要支持两种操作:

  • Q l r k 表示查询下标在区间 [l,r]中的第 k 小的数
  • C x y 表示将 a x a_x ax改为 y

题解:

代码:

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 100007;

int n, m, num, n1, n2;
int a[MAXN], b[MAXN << 1], c[MAXN], d[MAXN], e[MAXN], t1[MAXN], t2[MAXN];
int Top, Root[MAXN], val[MAXN * 400], ls[MAXN * 400], rs[MAXN * 400];

inline int lowbit(int x) { return x & (-x); }

void Add(int &rt, int l, int r, int ind, int c) {
    if (!rt) rt = ++Top;
    val[rt] += c;
    if (l == r) return;
    int m = (l + r) >> 1;
    if (ind <= m)
        Add(ls[rt], l, m, ind, c);
    else
        Add(rs[rt], m + 1, r, ind, c);
}

void Change(int ind, int val) {
    int x = lower_bound(b + 1, b + 1 + num, a[ind]) - b;
    for (int i = ind; i <= n; i += lowbit(i)) Add(Root[i], 1, num, x, val);
}

int Kth(int l, int r, int k) {  // 求第 k 大
    if (l == r) return l;
    int m = (l + r) >> 1, sum = 0;
    for (int i = 1; i <= n2; ++i) sum += val[ls[t2[i]]];
    for (int i = 1; i <= n1; ++i) sum -= val[ls[t1[i]]];
    if (sum >= k) {
        for (int i = 1; i <= n1; ++i)  // 所有树的节点保持对应
            t1[i] = ls[t1[i]];
        for (int i = 1; i <= n2; ++i) t2[i] = ls[t2[i]];
        return Kth(l, m, k);
    } else {
        for (int i = 1; i <= n1; ++i) t1[i] = rs[t1[i]];
        for (int i = 1; i <= n2; ++i) t2[i] = rs[t2[i]];
        return Kth(m + 1, r, k - sum);
    }
}

// 查询[l, r]的第k小
int Kth_pre(int l, int r, int k) {
    n1 = n2 = 0;
    for (int i = l - 1; i >= 1; i -= lowbit(i))  // 处理出需要求和的 n1 棵树
        t1[++n1] = Root[i];
    for (int i = r; i >= 1; i -= lowbit(i)) t2[++n2] = Root[i];
    return Kth(1, num, k);
}

int main() {
    // 读入
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        b[++num] = a[i];
    }
    for (int i = 1; i <= m; ++i) {
        char ch = getchar();
        while (ch != 'Q' && ch != 'C') ch = getchar();
        if (ch == 'Q')
            scanf("%d%d%d", &c[i], &d[i], &e[i]);  // 查询[c[i], d[i]]内的第e[i]小
        else {
            scanf("%d%d", &c[i], &d[i]);  // 修改a[c[i]] = d[i]
            b[++num] = d[i];  // 对于所有出现过的值(包括插入操作中的值)离散化
        }
    }
    // 离散化
    sort(b + 1, b + 1 + num);
    num = unique(b + 1, b + 1 + num) - b - 1;
    // 建树
    for (int i = 1; i <= n; ++i) Change(i, 1);
    // 处理操作&询问 因为要离散化所以要离线处理
    for (int i = 1; i <= m; ++i) {
        if (e[i]) {
            printf("%d\n", b[Kth_pre(c[i], d[i], e[i])]);  // 查询[c[i], d[i]]内的第e[i]小
        } else {
            Change(c[i], -1);
            a[c[i]] = d[i];  // 修改a[c[i]] = d[i]
            Change(c[i], 1);
        }
    }
    return 0;
}

3.3 树上路径第k小

洛谷P2633 Count on a tree

题意: 给定一棵 n 个节点的树,每个点有一个权值。有 m 个询问,每次给你 u,v,k,你需要回答 u xor last 和 v 这两个节点间第 k 小的点权。

其中 last 是上一个询问的答案,定义其初始为 0,即第一个询问的 u 是明文。 1 < = n 、 m < = 1 0 5 1<=n、m<=10^5 1<=nm<=105

题解: 模板题。对于一条路径u ~ v,记lca为u、v的最近公共祖先,flca为lca的父节点,则树上路径相当于 根到u的主席树 + 根到v的主席树 - 根到flca的主席树 - 根到lca的主席树。 所以我一开始读入数据时在树上建立主席树,然后每次查询多个权值线段树相减即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 5;
int n, q, a[N], b[N], sz;
vector<int> g[N];
int rt[N], L[N * 20], R[N * 20], sum[N * 20], tot = 0;
int dep[N], f[N][20];

void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

// 查询树上路径 u ~ v 第k小,lca为u、v的lca,flca为lca的父节点
int query(int u, int v, int lca, int flca, int l, int r, int k) {
    if (l == r) return b[l];
    int m = (l + r) / 2;
    int num = sum[L[u]] - sum[L[lca]] + sum[L[v]] - sum[L[flca]];
    if (num >= k) return query(L[u], L[v], L[lca], L[flca], l, m, k);
    return query(R[u], R[v], R[lca], R[flca], m + 1, r, k - num);
}

// dfs预处理lca数组,顺便建立主席树
void dfs(int u, int fa) {
    dep[u] = dep[fa] + 1;
    f[u][0] = fa;
    for (int i = 1; i < 20; i++) f[u][i] = f[f[u][i - 1]][i - 1];
    int pos = lower_bound(b + 1, b + 1 + sz, a[u]) - b;
    update(rt[u], rt[fa], 1, sz, pos, 1);  // 在树上建立主席树,前一个权值线段树是当前节点的父节点
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
    }
}

int LCA(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);  ///注意是交换u和v,不是交换dep[u]和dep[v]
    int d = dep[u] - dep[v];
    for (int i = 0; i < 20; i++)  ///先调整到同一深度
        if (d & (1 << i)) u = f[u][i];
    if (u == v) return u;
    for (int i = 19; i >= 0; i--) {  ///注意是倒着for,二进制拆分,从大到小尝试
        if (f[u][i] != f[v][i]) {
            u = f[u][i];
            v = f[v][i];
        }
    }
    return f[u][0];
}

int main() {
    scanf("%d%d", &n, &q);

    // 读入、离散化
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
    sort(b + 1, b + 1 + n);
    sz = unique(b + 1, b + 1 + n) - b - 1;

    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    int last = 0, u, v, k, lca, flca;
    while (q--) {
        scanf("%d%d%d", &u, &v, &k);  // u、v、v,查询u ~ v的路径第k小
        u ^= last;
        lca = LCA(u, v);
        flca = f[lca][0];
        last = query(rt[u], rt[v], rt[lca], rt[flca], 1, sz, k);  // 查询树上路径第k小
        printf("%d\n", last);
    }
    return 0;
}

洛谷P3066 [USACO12DEC]Running Away From the Barn G

题意: 给定一颗 n个点的有根树,边有边权,节点从 1 至 n 编号,1 号节点是这棵树的根。再给出一个参数 t,对于树上的每个节点 u,请求出 u 的子树中有多少节点满足该节点到 u 的距离不大于 t。 1 < = p i < i , 1 < = w i < = 1 0 12 , 1 < = n < = 2 ∗ 1 0 5 , 1 < = t < = 1 0 18 1<=p_i<i, 1<=w_i<=10^{12}, 1<=n<=2*10^5,1<=t<=10^{18} 1<=pi<i,1<=wi<=1012,1<=n<=2105,1<=t<=1018

题解: dfs+主席树。一个点的子树可以用dfs找,因此我们可以dfs上建立主席树。记u点的子树起始点dfs序为dfn[u],最后一个点的dfs序为out[u],那么我们只需要将主席树hjt[out[u]] - hjt[dfn[u] - 1]得到的就是子树的权值线段树,那么我在这个权值线段树上找小于等于k的数字个数。

代码:

#include <bits/stdc++.h>

#define int long long
using namespace std;

int const MAXN = 2e5 + 10, MAXM = MAXN  * 2;
int n, m, t;

int dfn[MAXN], ed[MAXN], re[MAXN];
int e[MAXM], ne[MAXM], w[MAXM], v[MAXN], h[MAXN], idx, tot, dep[MAXN * 2], backup[MAXN * 2];
int hjt[MAXN * 40], L[MAXN * 40], R[MAXN * 40], sum[MAXN * 40], tot_z;

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u, int fa) {  // 求dfs序
    dfn[u] = ++tot;
    re[tot] = u;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j == fa) continue;
        dep[j] = dep[u] + w[i];
        dfs(j, u);
    }
    ed[u] = tot;
}

// 在rt子树内插入当前的值pos,使之增加v
void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++tot_z;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

// 计算小于等于k的点数目
int query(int pre, int now, int l, int r, int k) {
    if (l == r) return sum[now] - sum[pre];
    int mid = (l + r) >> 1;
    if (mid >= k) return query(L[pre], L[now], l, mid, k);  // 如果左子树的size小于等于k,说明第k小在左子树内
    else return sum[L[now]] - sum[L[pre]] + query(R[pre], R[now], mid + 1, r, k);  // 否则在右子树内
}

signed main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cin >> n >> t;
    memset(h, -1, sizeof h);
    for (int i = 2, a, b; i <= n; ++i) {
        cin >> a >> b;
        add(i, a, b), add(a, i, b);
    }
    dfs(1, -1);
    for (int i = 1; i <= n; ++i) dep[i + n] = dep[i] + t;
    memcpy(backup, dep, sizeof backup);
    sort(backup + 1, backup + 1 + 2 * n);
    int m = unique(backup + 1, backup + 1 + 2 * n) - backup - 1;
    for (int i = 1; i <= tot; ++i) update(hjt[i], hjt[i - 1], 1, m, lower_bound(backup + 1, backup + 1 + m, dep[re[i]]) - backup, 1);  // 建树
    for (int i = 1; i <= n; ++i) cout << query(hjt[dfn[i] - 1], hjt[ed[i]], 1, m, lower_bound(backup + 1, backup + 1 + m, dep[i] + t) - backup) << endl;
    return 0;
}

P3899 [湖南集训]谈笑风生

题意: 给定一棵n个节点的树,有q个询问,每次给定a和k,求任何一个距a距离不超过给定的k的b,然后求一个c使得是a,b的后代,计算这样的(a, b, c)有多少对。 n < = 300000 , q < = 300000 n<=300000, q<=300000 n<=300000,q<=300000
题解: 容易知道,a,b,c肯定在一条链上,如果b是a祖先,那么这部分的贡献通过乘法原理很容易求出来,就是:size[a] * min(k, dep[a])。 如果b是a的儿子,就是查询一个子树中,深度在deep[p]+1–deep[p]+k的点的权值和。因此我们可以按照dfs序建立主席树,然后主席树维护的是权值和,这样查询的时候只需要给定l和r,就可以计算出权值在这个[l, r]中的总的权值和是多少了
代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 1e6 + 5;
int n, q, dep[N], dfn[N], son[N], tot, mxdep;
vector<int> g[N];
int hjt[N], L[N * 20], R[N * 20], cnt = 0;
LL sum[20 * N];

void update(int &rt, int pre, int l, int r, int pos, int v) { 
    rt = ++cnt;  // 新建一个节点
    L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + v;  // 将原先子树节点赋值过来
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(L[rt], L[pre], l, mid, pos, v);  // 如果权值小于等于mid,那么插入左子树
    else update(R[rt], R[pre], mid + 1, r, pos, v);  // 否则插入右子树
}

LL query(int pre, int now, int l, int r, int qL, int qR) {
    if (qL <= l && qR >= r) return sum[now] - sum[pre];
    int m = (l + r) / 2;
    LL ans = 0;
    if (qL <= m) ans += query(L[pre], L[now], l, m, qL, qR);
    if (qR > m) ans += query(R[pre], R[now], m + 1, r, qL, qR);
    return ans;
}

void dfs(int u, int fa) {
    son[u] = 1;
    dfn[u] = ++tot;
    dep[u] = dep[fa] + 1;
    mxdep = max(mxdep, dep[u]);
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
        son[u] += son[v];
    }
}

void dfs2(int u, int fa) {
    update(hjt[dfn[u]], hjt[dfn[u] - 1], 1, mxdep, dep[u], son[u] - 1);
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs2(v, u);
    }
}

int main() {
    scanf("%d%d", &n, &q);
    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);  // 求dfs序
    dfs2(1, 0);  // 在dfs序上建立主席树
    while (q--) {
        int p, k;
        scanf("%d%d", &p, &k);
        LL ans = 1LL * (son[p] - 1) * min(dep[p] - 1, k);  // a为的父节点
        ans += query(hjt[dfn[p]], hjt[dfn[p] + son[p] - 1], 1, mxdep, min(mxdep, dep[p] + 1), min(mxdep, dep[p] + k));  // a为p的子节点
        printf("%lld\n", ans);
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值