【学习笔记】线段树应用

【学习笔记】线段树应用

标题用 ##,说明太水啦~

主要是以一些题目为例谈谈线段树的一些拓展用法,感觉线段树很神!

P2146 [NOI2015] 软件包管理器 树剖+线段树

树剖+线段树板子,这种树剖的题只是加了个树剖的壳把它转换为区间问题罢了。至于为什么,这里弱弱的引用神🐟的一张图:

关于各点的 dfn,可以发现两个性质:

  1. 一条重链上的点的 dfn 连续。
  2. 一棵子树上的点的 dfn 也连续。

本题中就主要有两个操作:

  • 每次安装软件,就把根节点到 x 软件路径上的值全部变为 1
  • 每次卸载软件,就把 x 以及它的子树的值变为 0

tr[root].sum 可维护 1 的个数,答案为操作前后 tr[root].sum 的差的绝对值。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5+5;

vector<int> g[N];
int sz[N], fa[N], son[N], dep[N];
int dfn[N], top[N], T;

void dfs1(int u, int f){
    dep[u] = dep[f] + 1;
    sz[u] = 1;
    fa[u] = f;
    int maxson = -1;
    for(int v : g[u]){
        if(v == f) continue;
        dfs1(v, u);
        sz[u] += sz[v];
        if(maxson < sz[v]){
            maxson = sz[v];
            son[u] = v;
        }
    }
}

void dfs2(int u, int topf){
    dfn[u] = ++T;
    top[u] = topf;
    if(!son[u]) return;
    dfs2(son[u], topf);
    for(int v : g[u]){
        if(dfn[v]) continue;
        dfs2(v, v);
    }
}

struct node{
    int l, r;
    int sum, tag;
    #define ls x<<1
    #define rs x<<1|1
}tr[N<<2];

void build(int x, int l, int r){
    tr[x].l = l, tr[x].r = r; tr[x].sum = 0; tr[x].tag = -1;
    if(l == r) return;
    int mid = (tr[x].l+tr[x].r)>>1;
    build(ls, l, mid);
    build(rs, mid+1, r);
}

void pushup(int x){
    tr[x].sum = tr[ls].sum+tr[rs].sum;
}

void pushdown(int x){
    if(tr[x].tag == -1) return;
    tr[ls].sum = tr[x].tag*(tr[ls].r-tr[ls].l+1);
    tr[rs].sum = tr[x].tag*(tr[rs].r-tr[rs].l+1);
    tr[ls].tag = tr[rs].tag = tr[x].tag;
    tr[x].tag = -1;
}

void uby_interval(int x, int l, int r, int v){
    if(tr[x].l>=l && tr[x].r<=r){
        tr[x].sum = v*(tr[x].r-tr[x].l+1);
        tr[x].tag = v;
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    pushdown(x);
    if(l<=mid) uby_interval(ls, l, r, v);
    if(r>mid) uby_interval(rs, l, r, v);
    pushup(x);
}

void uby_lian(int x, int v){
    while(top[x] != 1){
        uby_interval(1, dfn[top[x]], dfn[x], v);
        x = fa[top[x]];
    }
    uby_interval(1, 1, dfn[x], v);
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n; cin>>n;
    for(int i=2; i<=n; i++){
        int x; cin>>x;
        g[x+1].push_back(i);
    }
    dfs1(1, 0);
    dfs2(1, 1);
    build(1, 1, n);
    int m; cin>>m;
    while(m--){
        string s; int x;
        cin>>s>>x; x++;
        int t1 = tr[1].sum;
        if(s=="install"){
            uby_lian(x, 1);
            cout<<tr[1].sum-t1<<"\n";
        } else{
            uby_interval(1, dfn[x], dfn[x]+sz[x]-1, 0);
            cout<<t1-tr[1].sum<<"\n";
        }
    }
    return 0;
}

P1486 [NOI2004] 郁闷的出纳员 权值线段树

关于权值线段树的介绍先推一篇博客。简单来说就是用线段树来维护桶。线段树的端点就是它所代表的值域。权值线段树的主要操作是求第 k 小/大。

考虑加工资不会对人数产生影响,我们直接维护偏移量即可。

对于扣工资操作,由于,可能会需要让一段工资区间内的人集体辞职,我们还需要在线段树上维护一个区间删除。

总的来说,我们对于工资建立一棵权值线段树,维护区间元素个数,需要支持单点插入,区间删除。同时维护一个偏移量,对于新插入的人进行偏移后插入至线段树中即可。

这里有一个 trick,为了防止下标变成负数,可以将权值为负的值加上一个最大的权值(也可以设为一个较大的值)。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 3e5+5;
const int rit = 2e5; // 防止减到负数

struct node{
    int l, r;
    int sum;
    #define ls x<<1
    #define rs x<<1|1
}tr[(rit+rit)<<2];

void pushup(int x){
    tr[x].sum = tr[ls].sum + tr[rs].sum;
}

void build(int x, int l, int r){
    tr[x].l = l, tr[x].r = r, tr[x].sum = 0;
    if(tr[x].l == tr[x].r) return;
    int mid = (tr[x].l+tr[x].r)>>1;
    build(ls, l, mid); build(rs, mid+1, r);
}

void update(int x, int val){
    if(tr[x].l == tr[x].r){
        tr[x].sum++;
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    if(val<=mid) update(ls, val);
    if(val>mid) update(rs, val);
    pushup(x); 
}

int query(int x, int l, int r){
    if(tr[x].l >=l && tr[x].r <= r) return tr[x].sum;
    int mid = (tr[x].l+tr[x].r)>>1, res = 0;
    if(l <= mid) res += query(ls, l, r);
    if(r > mid) res += query(rs, l, r);
    return res;
}

void clear(int x, int l, int r){
    if(tr[x].l == tr[x].r){
        tr[x].sum = 0;
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    if(l <= mid && tr[ls].sum) clear(ls, l, r);
    if(r > mid && tr[rs].sum) clear(rs, l, r);
    pushup(x);
}

int find_kth(int x, int k){
    if(tr[x].l == tr[x].r) return tr[x].l;
    if(tr[rs].sum >= k) return find_kth(rs, k);
    else return find_kth(ls, k-tr[rs].sum);
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, minn, out = 0; cin>>n>>minn;
    int begin_minn = minn;
    minn += rit; // 修改工资的操作用修改min来代替
    int front = 0; // 已经修改的工资总量,每次加入员工时减去front
    build(1, 1, rit+rit); // 以值域为端点建树
    for(int i=1; i<=n; i++){
        char s; int x; cin>>s>>x;
        if(s == 'I'){
            if(x < begin_minn) continue;
            update(1, x-front+rit);
        } else if(s == 'A'){
            front += x;
            minn -= x;
        } else if(s == 'S'){
            front -= x;
            minn += x;
            // 踢出员工
            int now = query(1, 0, minn-1);
            out += now;
            clear(1, 0, minn-1);
        } else{
            if(x > query(1, minn, rit+rit)) cout<<"-1\n";
            else cout<<find_kth(1, x)+front-rit<<"\n";
        }
    }
    cout<<out;
    return 0;
}

虽然这题用 pbds 更是平衡树裸题。

P2824 [HEOI2016/TJOI2016] 排序 线段树多次排序?

对于此题依旧推一篇博客有亿些借鉴。

先考虑对一个 01 序列多次排序:
使用线段树来维护。查询一段区间内的 1 的个数记为 cnt1,如果是升序,就将这段区间的 [rcnt1+1,r] 都更改为 1,将 [l,rcnt1] 更改为 0。降序则将 [l,l+cnt11] 更改为 1,将 [l+cnt,r] 更改为 0。这样我们就成功地把排序转化为了区间查询和区间修改。

然后就是最人类智慧的一集:
二分答案 mid。我们把原排列中大于等于 mid 的数都标记为 1,小于 mid 的都标记为 0。然后对于每个操作我们就将 01 序列排个序。最后如果第 p 个位子仍是 1 的话就是可行的。

单调性:
假设一下,如果二分的答案是 1,那么原序列所有的值都转化为了 1,所以最后肯定是 true。如果二分一个值成立当且仅当这个位子的值大于等于 mid,故如果 check 返回 true,则 l=mid+1,否则 r=mid1

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 2e5+5;

int a[N];
int n, m, q;
struct optt{
    int op, l, r;
}opt[N];

struct node{
    int l, r;
    // 区间查询和区间修改
    int sum, tag;
    #define ls x<<1
    #define rs x<<1|1
}tr[N<<2];

void pushup(int x){
    tr[x].sum = tr[ls].sum+tr[rs].sum;
}

void pushdown(int x){
    if(tr[x].tag == -1) return;
    tr[ls].sum = (tr[ls].r-tr[ls].l+1)*tr[x].tag;
    tr[rs].sum = (tr[rs].r-tr[rs].l+1)*tr[x].tag;
    tr[ls].tag = tr[rs].tag = tr[x].tag;
    tr[x].tag = -1;
}

void build(int x, int l, int r, int p){
    tr[x].l = l, tr[x].r = r; tr[x].tag = -1;
    if(l == r){
        tr[x].sum = (a[l]>=p);
        return;
    }
    int mid = (l+r)>>1;
    build(ls, l, mid, p);
    build(rs, mid+1, r, p);
    pushup(x);
}

int querynum(int x, int l, int r){
    if(tr[x].l>=l && tr[x].r<=r){
        return tr[x].sum;
    }
    int mid = (tr[x].l+tr[x].r)>>1, res = 0;
    pushdown(x);
    if(l<=mid) res += querynum(ls, l, r);
    if(r>mid) res += querynum(rs, l, r);
    return res;
}

void update(int x, int l, int r, int v){
    if(tr[x].l>=l && tr[x].r<=r){
        tr[x].sum = (tr[x].r-tr[x].l+1)*v;
        tr[x].tag = v;
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    pushdown(x);
    if(l<=mid) update(ls, l, r, v);
    if(r>mid) update(rs, l, r, v);
    pushup(x);
}

bool querypos(int x, int p){
    if(tr[x].l==tr[x].r && tr[x].l==p){
        return tr[x].sum == 1;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    pushdown(x);
    if(p<=mid) return querypos(ls, p);
    else return querypos(rs, p);
}

bool check(int mid){
    build(1, 1, n, mid);
    for(int i=1; i<=m; i++){
        auto p = opt[i];
        int cnt1 = querynum(1, p.l, p.r);
        if(p.op == 0){
            update(1, p.l, p.r-cnt1, 0);
            update(1, p.r-cnt1+1, p.r, 1);
        } else{
            update(1, p.l, p.l+cnt1-1, 1);
            update(1, p.l+cnt1, p.r, 0);
        }
    }
    return querypos(1, q);
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin>>n>>m;
    for(int i=1; i<=n; i++)
        cin>>a[i];
    for(int i=1; i<=m; i++)
        cin>>opt[i].op>>opt[i].l>>opt[i].r;
    cin>>q;
    int l = 1, r = n, ans;
    while(l <= r){
        int mid = (l+r)>>1;
        if(check(mid)){
            l = mid+1;
            ans = mid;
        } else{
            r = mid-1;
        }
    }
    cout<<ans;
    return 0;
}

P1712 [NOI2016] 区间 权值线段树+离散化+双指针

按排序后的顺序逐一加入区间,然后看看是否有一个点的被覆盖次数 m

如果有的话那就统计一下答案,然后将前面加入的按顺序删掉,直到点的覆盖次数 <m

注意离散化的一种方式。以及 pushup 操作维护的是最大值(这应该也算权值线段树的一个 trick 吧,区间内被覆盖次数最多的值)。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6+5;

int tot, L[N], R[N];
struct node1{
    int len, id;
}a[N];
struct node2{
    int v, id;
}p[N];
struct tree{
    int l, r;
    int sum, tag;
    #define ls x<<1
    #define rs x<<1|1
}tr[N<<2];

void build(int x, int l, int r){
    tr[x].l = l, tr[x].r = r, tr[x].tag = tr[x].sum = 0;
    if(l == r) return;
    int mid = (l+r)>>1;
    build(ls, l, mid);
    build(rs, mid+1, r);
}

void pushup(int x){
    tr[x].sum = max(tr[ls].sum, tr[rs].sum);
}

void pushdown(int x){
    if(!tr[x].tag) return;
    tr[ls].sum += tr[x].tag;
    tr[rs].sum += tr[x].tag;
    tr[ls].tag += tr[x].tag;
    tr[rs].tag += tr[x].tag;
    tr[x].tag = 0;
}

void update(int x, int l, int r, int v){
    if(tr[x].l>=l && tr[x].r<=r){
        tr[x].sum += v;
        tr[x].tag += v;
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    pushdown(x);
    if(l<=mid) update(ls, l, r, v);
    if(r>mid) update(rs, l, r, v);
    pushup(x);
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, m; cin>>n>>m;
    for(int i=1; i<=n; i++){
        int u, v; cin>>u>>v;
        a[i] = {v-u, i};
        p[++tot] = {u, i};
        p[++tot] = {v, i};
    }
    sort(a+1, a+1+n, [&](node1 x, node1 y){return x.len < y.len;});
    sort(p+1, p+1+tot, [&](node2 x, node2 y){return x.v < y.v;});
    int num = 0;
    p[0].v = -1;
    for(int i=1; i<=tot; i++){
        if(p[i].v != p[i-1].v)
            num++;
        int u = p[i].id;
        if(!L[u]) L[u] = num;
        else R[u] = num;
    }
    build(1, 1, num);
    int ans = INT32_MAX-1, le=0, ri=0;
    while(true){
        while(tr[1].sum<m && ri<n){
            ri++;
            update(1, L[a[ri].id], R[a[ri].id], 1);
        }
        if(tr[1].sum<m) break;
        while(tr[1].sum>=m && le<ri){
            le++;
            update(1, L[a[le].id], R[a[le].id], -1);
        }
        ans = min(ans, a[ri].len-a[le].len);
    }
    cout<<(ans==INT32_MAX-1 ? -1 : ans);
    return 0;
}

P1856 [IOI1998] [USACO5.5] 矩形周长Picture 扫描线+线段树

依旧先推一篇博客

关于答案计算:

注意先加边后删边

对于横边,答案是相邻两次修改的区间覆盖长度差(就是 tr[root].len 的差)全部加起来。即 i=2e|lenileni1|,其中 e 是横边的总边数,len 就是 tr[root].len

为什么呢?考虑两个矩形嵌套,然后在前一个矩形的下边的扫描线上的被覆盖的长度,和当前第二个矩形的下边时扫描线上的被覆盖长度的差就是第二个矩形延展出来的长度,因为取一个矩形的上边的时候可能会缩回去,所以要加绝对值。

对于竖边,朴素想法是再从左到右扫一遍。但是可以通过 tree[root].num 计算。答案是 i=2e2numi|hihi1|,其中 e 是横边的总边数,h 是当前边的高度(纵坐标),num 就是 tree[root].num

为什么竖边可以这样计算呢?请看下图:

smxshu.png

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

struct Tree{
    int l, r;
    int sum;//整个区间被整体覆盖了几次(类似lazytag,但不下传)
    int num;//整个区间被几条互不相交的线段覆盖(比如,[1,2],[4,5]则为2,[1,3],[4,5]则为1(我习惯用闭区间),[1,4],[2,2],[4,4]也为1)
    int len;//整个区间被覆盖的总长度
    bool lflag;//左端点是否被覆盖(合并用)
    bool rflag;//右端点是否被覆盖(合并用)
    #define ls x<<1
    #define rs x<<1|1
}tr[100005];
struct Edge{
    int l, r, h, flag;
}e[10005];

int egnum, ans, lst;

void add_edge(int l, int r, int h, int f){
    e[++egnum] = {l, r, h, f};
}

void build(int x, int l, int r){
    tr[x].l = l, tr[x].r = r;
    if(l == r){
        tr[x].len = 0;
        tr[x].num = 0;
        tr[x].lflag = tr[x].rflag = 0;
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    build(ls, l, mid);
    build(rs, mid+1, r);
}

void pushup(int x, int l, int r){
    if(tr[x].sum){
        tr[x].num = 1;
        tr[x].len = r-l+1;
        tr[x].lflag = tr[x].rflag = 1;
    } else if(l == r){
        tr[x].num = 0;
        tr[x].len = 0;
        tr[x].lflag = tr[x].rflag = 0;
    }    
    else{
        tr[x].len = tr[ls].len+tr[rs].len;
        tr[x].num = tr[ls].num+tr[rs].num;
        if(tr[ls].rflag && tr[rs].lflag) tr[x].num--;
        tr[x].lflag = tr[ls].lflag;
        tr[x].rflag = tr[rs].rflag;
    }
}

void add(int x, int l, int r, int v){
    if(tr[x].l>=l && tr[x].r<=r){
        tr[x].sum += v;
        pushup(x, tr[x].l, tr[x].r); // 一开始的 build 没有初始化
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    if(l<=mid) add(ls, l, r, v);
    if(r>mid) add(rs, l, r, v);
    pushup(x, tr[x].l, tr[x].r);
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n; cin>>n;
    int mx = INT32_MIN+1, mn = INT32_MAX-1;
    for(int i=1; i<=n; i++){
        int x1, y1, x2, y2; cin>>x1>>y1>>x2>>y2;
        mx = max({mx, x1, x2});
        mn = min({mn, x1, x2});
        add_edge(x1, x2, y1, 1); // 下面的边+1
        add_edge(x1, x2, y2, -1); // 上面的边-1
    }
    if(mn <= 0){
        for(int i=1; i<=egnum; i++){
            e[i].l += (-mn+1);
            e[i].r += (-mn+1);
        }
        mx -= mn;
    } // 负数下标 -> 正数下标
    sort(e+1, e+egnum+1, [&](Edge a, Edge b){
        return (a.h==b.h) ? (a.flag>b.flag) : (a.h<b.h);
    }); // 先加边再删边
    build(1, 1, mx);
    for(int i=1; i<=egnum; i++){
        add(1, e[i].l, e[i].r-1, e[i].flag);
        // 点坐标变为线段长度会减一
        while(e[i].h==e[i+1].h && e[i].flag==e[i+1].flag){
            i++;
            add(1, e[i].l, e[i].r-1, e[i].flag);
        }
        ans += abs(tr[1].len-lst);
        lst = tr[1].len;
        ans += tr[1].num*2*(e[i+1].h-e[i].h);
    }
    cout<<ans;
    return 0;
}

CF786B Legacy 线段树优化建图

题目大意:有 n 个点、q 次操作。每一种操作为以下三种类型中的一种:

  • 操作一:连一条 uv 的有向边,权值为 w
  • 操作二:对于所有 i[l,r] 连一条 ui 的有向边,权值为 w
  • 操作三:对于所有 i[l,r] 连一条 iu的有向边,权值为 w

求从点 s 到其他点的最短路。
1n,q105,1w109

再次推荐一篇博客

感觉这篇博客真得讲的很清晰,就不多写些什么了。其实是菜!

特别注意连边时都变成了 x 在线段树上的节点 leaf[x]!!!
包括在跑最短路的时候也一样(比方说起点处理的时候就可能又写错)。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int D = 5e5;
const int N = 1e5+5;
#define pii pair<ll, int>
#define fi first
#define se second

struct tree{
    int l, r;
    #define ls (x<<1) // 注意这个括号:((
    #define rs (x<<1|1)
}tr[N<<2];
int leaf[N];

struct node{
    int v;
    ll w;
};
vector<node> g[1000005];

void build(int x, int l, int r){
    tr[x].l = l, tr[x].r = r;
    if(l == r){
        leaf[l] = x;
        return;
    }
    g[x].push_back({ls, 0});
    g[x].push_back({rs, 0});
    g[ls+D].push_back({x+D, 0});
    g[rs+D].push_back({x+D, 0});
    int mid = (tr[x].l+tr[x].r)>>1;
    build(ls, l, mid);
    build(rs, mid+1, r);
}

void add(int x, int v, int l, int r, int w, int op){
    if(tr[x].l>=l && tr[x].r<=r){
        if(op==2) g[v+D].push_back({x, w});
        else g[x+D].push_back({v, w});
        return;
    }
    int mid = (tr[x].l+tr[x].r)>>1;
    if(l<=mid) add(ls, v, l, r, w, op);
    if(r>mid) add(rs, v, l, r, w, op);
}

ll dis[1000005];
bool vis[1000005];

void dijkstra(int s){
    priority_queue<pii, vector<pii>, greater<pii>> Q;
    memset(dis, 0x3f, sizeof(dis));
    dis[s] = 0; Q.push({0, s});
    while(!Q.empty()){
        int u = Q.top().se; Q.pop();
        if(vis[u]) continue;
        vis[u] = true;
        for(auto [v, w] : g[u]){
            if(dis[v] > dis[u]+w){
                dis[v] = dis[u]+w;
                Q.push({dis[v], v});
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, q, s; cin>>n>>q>>s;
    build(1, 1, n);
    for(int i=1; i<=q; i++){
        int op; cin>>op;
        if(op == 1){
            int v, u, w; cin>>v>>u>>w;
            g[leaf[v]].push_back({leaf[u], w});
        } else{
            int v, l, r, w; cin>>v>>l>>r>>w;
            add(1, leaf[v], l, r, w, op);
        }
    }
    // 叶子节点连边
    for(int i=1; i<=n; i++){
        g[leaf[i]].push_back({leaf[i]+D, 0});
        g[leaf[i]+D].push_back({leaf[i], 0});
    }
    dijkstra(leaf[s]);
    for(int i=1; i<=n; i++)
        cout<<(dis[leaf[i]]>=0x3f3f3f3f3f3f3f3fll ? -1 : dis[leaf[i]])<<" ";
    return 0;
}

关于此题还有一个小拓展:考虑一个边集的所有点向另一个边集的所有点连边?

trick:在两边集间建一个虚点,连边 [l1,r1]wpoint0[l2,r2]。这样连的边数会大大减少。

P3372 【模板】线段树 1 动态开点线段树

因为还没怎么理解过动态开点线段树,所以只能先写一波模板了。

因为感觉写的很烂,所以写了很多版,而且这题写完之后发现确实不适合用动态开点。

Code
// #include <bits/stdc++.h>
// using namespace std;
// #define ll long long
// const int N = 1e5+5;

// #define ls (tr[x].lson)
// #define rs (tr[x].rson)

// ll a[N], pre[N];

// struct tree{
//     int l, r;
//     int lson, rson;
//     ll sum, tag;
//     tree(){}
//     tree(int l_, int r_){
//         l=l_; r=r_; lson=rson=0;
//         sum=pre[r_]-pre[l_-1];
//     }
// }tr[N<<1];
// int ntot, rt;

// void pushup(int &x){
//     int mid = (tr[x].l+tr[x].r)>>1;
//     if(!ls){
//         ls = ++ntot;
//         tr[ls] = tree(tr[x].l, mid);
//     }
//     if(!rs){
//         rs = ++ntot;
//         tr[rs] = tree(mid+1, tr[x].r);
//     }
//     tr[x].sum = tr[ls].sum+tr[rs].sum;
// }

// void pushdown(int &x){
//     if(!tr[x].tag) return;
//     int mid = (tr[x].l+tr[x].r)>>1;
//     if(!ls){
//         ls = ++ntot;
//         tr[ls] = tree(tr[x].l, mid);
//     }
//     if(!rs){
//         rs = ++ntot;
//         tr[rs] = tree(mid+1, tr[x].r);
//     }
//     tr[ls].sum += (mid-tr[x].l+1)*tr[x].tag;
//     tr[rs].sum += (tr[x].r-mid)*tr[x].tag;
//     tr[ls].tag += tr[x].tag;
//     tr[rs].tag += tr[x].tag;
//     tr[x].tag = 0;
// }

// void update(int &x, int s, int t, int l, int r, ll v){
//     if(!x){
//         x = ++ntot;
//         tr[x] = tree(s, t);
//     }
//     if(tr[x].l>=l && tr[x].r<=r){
//         tr[x].sum += (tr[x].r-tr[x].l+1)*v;
//         tr[x].tag += v;
//         return;
//     }
//     pushdown(x);
//     int mid = (tr[x].l+tr[x].r)>>1;
//     if(l<=mid) update(ls, s, mid, l, r, v);
//     if(r>mid) update(rs, mid+1, t, l, r, v);
//     pushup(x);
// }

// ll query(int &x, int s, int t, int l, int r){
//     if(!x){
//         x = ++ntot;
//         tr[x] = tree(s, t);
//     }
//     if(tr[x].l>=l && tr[x].r<=r){
//         return tr[x].sum;
//     }
//     int mid = (tr[x].l+tr[x].r)>>1;
//     ll res = 0;
//     pushdown(x);
//     if(l<=mid) res += query(ls, s, mid, l, r);
//     if(r>mid) res += query(rs, mid+1, t, l, r);
//     return res;
// }

// int main(){
//     ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
//     int n, m; cin>>n>>m;
//     for(int i=1; i<=n; i++){
//         cin>>a[i];
//         pre[i] = pre[i-1]+a[i];
//     }
//     for(int i=1; i<=m; i++){
//         int op; cin>>op;
//         if(op==1){
//             int l, r; ll v; cin>>l>>r>>v;
//             update(rt, 1, n, l, r, v);
//         } else if(op==2){
//             int l, r; cin>>l>>r;
//             cout<<query(rt, 1, n, l, r)<<"\n";
//         }
//     }
//     return 0;
// }









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

// ll a[N];

// struct tree{
//     int ls, rs;
//     ll sum, tag;
// }tr[N<<1];
// int tot, rt;

// void pushup(int &x){
//     tr[x].sum = tr[tr[x].ls].sum+tr[tr[x].rs].sum;
// }

// void pushdown(int &x, int l, int r){
//     if(!tr[x].tag) return;
//     int mid = (l+r)>>1;
//     tr[tr[x].ls].sum += (mid-l+1)*tr[x].tag;
//     tr[tr[x].rs].sum += (r-mid)*tr[x].tag;
//     tr[tr[x].ls].tag += tr[x].tag;
//     tr[tr[x].rs].tag += tr[x].tag;
//     tr[x].tag = 0;
// }

// void update(int &x, int l, int r, int ql, int qr, ll v){
//     if(!x) x = ++tot;
//     if(l>=ql && r<=qr){
//         tr[x].sum += (r-l+1)*v;
//         tr[x].tag += v;
//         return;
//     }
//     pushdown(x, l, r);
//     int mid = (l+r)>>1;
//     if(ql<=mid) update(tr[x].ls, l, mid, ql, qr, v);
//     if(qr>mid) update(tr[x].rs, mid+1, r, ql, qr, v);
//     pushup(x);
// }

// ll query(int &x, int l, int r, int ql, int qr){
//     if(!x) return 0;
//     if(l>=ql && r<=qr){return tr[x].sum;}
//     int mid = (l+r)>>1;
//     pushdown(x, l, r);
//     ll res = 0;
//     if(ql<=mid) res += query(tr[x].ls, l, mid, ql, qr);
//     if(qr>mid) res += query(tr[x].rs, mid+1, r, ql, qr);
//     return res;
// }

// int main(){
//     ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
//     int n, m; cin>>n>>m;
//     for(int i=1; i<=n; i++){
//         ll x; cin>>x;
//         update(rt, 1, n, i, i, x);
//     }
//     for(int i=1; i<=m; i++){
//         int op; cin>>op;
//         if(op==1){
//             int l, r; ll v; cin>>l>>r>>v;
//             update(rt, 1, n, l, r, v);
//         } else if(op==2){
//             int l, r; cin>>l>>r;
//             cout<<query(rt, 1, n, l, r)<<"\n";
//         }
//     }
//     return 0;
// }









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

// #define ls (tr[x].lson)
// #define rs (tr[x].rson)

// ll a[N];

// struct tree{
//     int l, r;
//     int lson, rson;
//     ll sum, tag;
//     tree(){}
//     tree(int _l, int _r){ l = _l, r = _r; lson = rson = 0; }
// }tr[N<<1];

// int ntot, rt;

// void pushup(int &x){
//     tr[x].sum = tr[ls].sum+tr[rs].sum;
// }

// void pushdown(int &x){
//     if(!tr[x].tag) return;
//     tr[ls].sum += (tr[ls].r-tr[ls].l+1)*tr[x].tag;
//     tr[rs].sum += (tr[rs].r-tr[rs].l+1)*tr[x].tag;
//     tr[ls].tag += tr[x].tag;
//     tr[rs].tag += tr[x].tag;
//     tr[x].tag = 0;
// }

// void update(int &x, int s, int t, int l, int r, ll v){
//     if(!x){
//         x = ++ntot;
//         tr[x] = tree(s, t);
//     }
//     if(tr[x].l>=l && tr[x].r<=r){
//         tr[x].sum += (tr[x].r-tr[x].l+1)*v;
//         tr[x].tag += v;
//         return;
//     }
//     pushdown(x);
//     int mid = (tr[x].l+tr[x].r)>>1;
//     if(l<=mid) update(ls, s, mid, l, r, v);
//     if(r>mid) update(rs, mid+1, t, l, r, v);
//     pushup(x);
// }

// ll query(int &x, int l, int r){
//     if(!x) return 0;
//     if(tr[x].l>=l && tr[x].r<=r){
//         return tr[x].sum;
//     }
//     int mid = (tr[x].l+tr[x].r)>>1;
//     ll res = 0;
//     pushdown(x);
//     if(l<=mid) res += query(ls, l, r);
//     if(r>mid) res += query(rs, l, r);
//     return res;
// }

// int main(){
//     ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
//     int n, m; cin>>n>>m;
//     for(int i=1; i<=n; i++){
//         ll x; cin>>x;
//         update(rt, 1, n, i, i, x);
//     }
//     for(int i=1; i<=m; i++){
//         int op; cin>>op;
//         if(op==1){
//             int l, r; ll v; cin>>l>>r>>v;
//             update(rt, 1, n, l, r, v);
//         } else if(op==2){
//             int l, r; cin>>l>>r;
//             cout<<query(rt, l, r)<<"\n";
//         }
//     }
//     return 0;
// }

P3834 【模板】可持久化线段树 2 离散化+可持久化线段树

可持久化线段树的经典应用:静态区间第 k 大/小。

因为自己离散化一直理解的不是很透,所以这里多写一点关于离散化的篇幅。

离散化一般都是在值域太大的情况下使用的。目的是记录原数组下标以最后访问原数组的值。因为离散化前后对应的大小关系不变,所以需要先排序。

过程:

  1. 假设原数组为 a,新建一个数组 b,满足 bi=ai
  2. 对数组 b 排序并去重。注意 unique() 函数最后返回的是第一个重复元素的下标(原理图放在代码后)。可借此求出 b 数组的大小(同时这个值也是权值线段树的值域上界,代码中为 M)。
  3. 遍历 a 数组二分查找求出每个值 ai 现在在 b 数组中的位置。

此时 b 数组就是原数组去重后的有序数据,a 数组就是原数据在 b 数组中的下标。

for(int i=1; i<=n; i++){
    cin>>a[i];
    b[i] = a[i];
}
sort(b+1, b+1+n);
int M = unique(b+1, b+1+n)-b-1;
for(int i=1; i<=n; i++)
    a[i] = lower_bound(b+1, b+1+M, a[i]) - b;

unique.png

关于动态开点线段树的讲解本蒻只能再推一篇文章力。

Code
// 依旧写了 2 种版本qwq

// #include <bits/stdc++.h>
// using namespace std;
// #define ll long long
// const int N = 2e5+5;

// int a[N], b[N];
// int rt[N], cnt;
// int ls[N*20], rs[N*20], sum[N*20];

// int update(int x, int l, int r, int pos){
//     int nx = ++cnt;
//     ls[nx] = ls[x], rs[nx] = rs[x];
//     sum[nx] = sum[x]+1;
//     if(l == r) return nx;
//     int mid = (l+r)>>1;
//     if(pos<=mid) ls[nx] = update(ls[x], l, mid, pos);
//     else rs[nx] = update(rs[x], mid+1, r, pos);
//     return nx;
// }

// int query(int x, int y, int l, int r, int k){
//     if(l == r) return l;
//     int mid = (l+r)>>1;
//     int s = sum[ls[y]] - sum[ls[x]];
//     if(s>=k) return query(ls[x], ls[y], l, mid, k);
//     else return query(rs[x], rs[y], mid+1, r, k-s);
// }

// int main(){
//     ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
//     int n, q; cin>>n>>q;
//     for(int i=1; i<=n; i++){
//         cin>>a[i];
//         b[i] = a[i];
//     }
//     sort(b+1, b+1+n);
//     int M = unique(b+1, b+1+n)-b-1;
//     for(int i=1; i<=n; i++)
//         a[i] = lower_bound(b+1, b+1+M, a[i]) - b;
//     for(int i=1; i<=n; i++){
//         rt[i] = update(rt[i-1], 1, M, a[i]);
//     }
//     while(q--){
//         int l, r, k; cin>>l>>r>>k;
//         cout<<b[query(rt[l-1], rt[r], 1, M, k)]<<"\n";
//     }
//     return 0;
// }





#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 2e5+5;

int a[N], b[N];
int rt[N], cnt;
struct node{
    int ls, rs;
    int sum;
}tr[N*20];

int newnode(int ls, int rs, int sum){
    int idx = ++cnt;
    tr[idx] = {ls, rs, sum};
    return idx;
}

void update(int &x, int prex, int l, int r, int pos){
    x = newnode(tr[prex].ls, tr[prex].rs, tr[prex].sum+1);
    if(l == r) return;
    int mid = (l+r)>>1;
    if(pos<=mid) update(tr[x].ls, tr[prex].ls, l, mid, pos);
    else update(tr[x].rs, tr[prex].rs, mid+1, r, pos);
}

int query(int x, int y, int l, int r, int k){
    if(l == r) return l;
    int mid = (l+r)>>1;
    int s = tr[tr[y].ls].sum - tr[tr[x].ls].sum;
    if(s>=k) return query(tr[x].ls, tr[y].ls, l, mid, k);
    else return query(tr[x].rs, tr[y].rs, mid+1, r, k-s);
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, q; cin>>n>>q;
    for(int i=1; i<=n; i++){
        cin>>a[i];
        b[i] = a[i];
    }
    sort(b+1, b+1+n);
    int M = unique(b+1, b+1+n)-b-1;
    for(int i=1; i<=n; i++)
        a[i] = lower_bound(b+1, b+1+M, a[i]) - b;
    for(int i=1; i<=n; i++){
        update(rt[i], rt[i-1], 1, M, a[i]);
    }
    while(q--){
        int l, r, k; cin>>l>>r>>k;
        cout<<b[query(rt[l-1], rt[r], 1, M, k)]<<"\n";
    }
    return 0;
}

P4097 【模板】李超线段树 李超线段树

以下内容大部分摘自OI_wiki

李超线段树是运用标记永久化思想的线段树。主要用来记录区间中最优线段。

同样是记录值域,所以可以动态开点。

把题意转化为维护如下操作:

  • 加入一个一次函数,定义域为 [l,r]
  • 给定 k,求定义域包含 k 的所有一次函数中,在 x=k 处取值最大的那个,如果有多个函数取值相同,选编号最小的。

注意斜率不存在的情况。

假设需要插入一条线段 f,考虑某个被新线段 f 完整覆盖的线段树区间。

若该区间无标记,直接打上用该线段更新的标记。

否则,按新线段 f 取值是否大于原标记 g,我们可以把当前区间分为两个子区间。其中肯定有一个子区间被左区间或右区间完全包含,也就是说,在两条线段中,肯定有一条线段,只可能成为左区间的答案,或者只可能成为右区间的答案。我们用这条线段递归更新对应子树,用另一条线段作为懒标记更新整个区间,这就保证了递归下传的复杂度。当一条线段只可能成为左或右区间的答案时,才会被下传,所以不用担心漏掉某些线段。

具体来说,设当前区间的中点为 m,我们拿新线段 f 在中点处的值与原最优线段 g 在中点处的值作比较。

如果新线段 f 更优,则将 fg 交换。那么现在考虑在中点处 f 不如 g 优的情况(注意这里已经交换过位置了):

若在左端点处 f 更优,那么 fg 必然在左半区间中产生了交点,f 只有在左区间才可能优于 g,递归到左儿子中进行下传;
若在右端点处 f 更优,那么 fg 必然在右半区间中产生了交点,f 只有在右区间才可能优于 g,递归到右儿子中进行下传;
若在左右端点处 g 都更优,那么 f 不可能成为答案,不需要继续下传。
除了这两种情况之外,还有一种情况是 fg 刚好交于中点,在程序实现时可以归入中点处 f 不如 g 优的情况,结果会往 f 更优的一个端点进行递归下传。

最后将 g 作为当前区间的懒标记。

注意懒标记并不等价于在区间中点处取值最大的线段。为什么呢?考虑下图所示情况:

当我们加入橙色线段时,递归更新了 mid 右侧的标记。而当我们查询 x=k1 时,该处的懒标记还是图中 k1 经过的黑色的线段的编号,但是最大的值的线段编号应该是橙色的线段。考虑 x=k2 也一样。

所以最后查询答案时可以利用标记永久化思想,在包含 x=k 的所有线段树区间(不超过 O(logn) 个)的标记线段中,比较得出最终答案。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int MOD1 = 39989, MOD2 = 1e9;
const double eps = 1e-9;
#define ls (x<<1)
#define rs (x<<1|1)
#define pdi pair<double, int>
#define fi first
#define se second

struct line{
    double k, b;
}ln[100005];
int cnt;
int maxp[160005];

int cmp(double x, double y){
    if(x-y>eps) return 1;
    if(y-x>eps) return -1;
    return 0;
}

double calc(int id, int x){
    return ln[id].k*x + ln[id].b;
}

void add(int x0, int y0, int x1, int y1){
    cnt++;
    if(x0 == x1) // 斜率不存在
        ln[cnt].k = 0, ln[cnt].b = max(y0, y1);
    else
        ln[cnt].k = 1.0*(y1-y0)/(x1-x0), ln[cnt].b = y0-ln[cnt].k*x0;
}

void upd(int x, int l, int r, int u){
    int &v = maxp[x];
    int mid = (l+r)>>1;
    int cmid = cmp(calc(u, mid), calc(v, mid));
    if(cmid==1 || (!cmid && u<v)) swap(u, v); // !(-1) = true,经历20min实况
    int cl = cmp(calc(u, l), calc(v, l)), cr = cmp(calc(u, r), calc(v, r));
    if(cl==1 || (!cl && u<v)) upd(ls, l, mid, u); // 注意这里是交换过的
    if(cr==1 || (!cr && u<v)) upd(rs, mid+1, r, u);
}

void update(int x, int l, int r, int ql, int qr, int u){
    if(l>=ql && r<=qr){
        upd(x, l, r, u);
        return;
    }
    int mid = (l+r)>>1;
    if(ql<=mid) update(ls, l, mid, ql, qr, u);
    if(qr>mid) update(rs, mid+1, r, ql, qr, u);
}

pdi pmax(pdi x, pdi y){
    int c = cmp(x.fi, y.fi);
    if(c == 1) return x;
    if(c == -1) return y;
    return (x.se<y.se) ? x : y; 
}

pdi query(int x, int l, int r, int k){
    double res = calc(maxp[x], k);
    if(l == r) return {res, maxp[x]};
    int mid = (l+r)>>1;
    if(k<=mid) return pmax({res, maxp[x]}, query(ls, l, mid, k));
    else return pmax({res, maxp[x]}, query(rs, mid+1, r, k)); // 标记永久化,上传的时候更新
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, lastans = 0; cin>>n;
    while(n--){
        int op; cin>>op;
        if(op == 1){
            int x0, y0, x1, y1; cin>>x0>>y0>>x1>>y1;
            x0 = (x0+lastans-1+MOD1)%MOD1+1;
            x1 = (x1+lastans-1+MOD1)%MOD1+1;
            y0 = (y0+lastans-1+MOD2)%MOD2+1;
            y1 = (y1+lastans-1+MOD2)%MOD2+1;
            if(x0 > x1) swap(x0, x1), swap(y0, y1);
            add(x0, y0, x1, y1);
            update(1, 1, MOD1, x0, x1, cnt);
        } else{
            int k; cin>>k;
            k = (k+lastans-1+MOD1)%MOD1+1;
            lastans = query(1, 1, MOD1, k).se;
            cout<<lastans<<"\n";
        }
    }
    return 0;
}

待补:维护凸包。

posted @   FlyPancake  阅读(15)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
// music
点击右上角即可分享
微信分享提示