吉司机线段树

吉司机线段树是势能线段树的一种。由吉如一老师在论文中提出。看本文确认弄懂了【势能线段树】。

【吉司机线段树】

【例一】

HDU 5306 Gorgeous Sequence

支持:

  1. 区间对某个数取 \(\min\)

  2. 区间求最大值。

  3. 区间求和。

\(n,q\le 10^5\)


考虑剪枝。对结点记录 \(mx,t,se\) 表示最大值、最大值个数、次大值,同时 \(sum\) 表示区间和。对 \(x\)\(\min\) 时:

  1. \(x\ge mx\),不用动。

  2. \(se<x<mx\),只需要把 \(mx\)\(sum\) 改一下。

  3. 否则暴力递归。

需要注意的是怎么打区间 \(\min\) 的标记?我们的 \(mx\) 不仅是剪枝用的标记,也充当区间取 \(\min\) 的标记!

对于复杂度分析,令结点 \(u\) 的势能为 \(u\) 的区间内不同值的个数,那么如果暴力递归至少把最大值和次大值都变成相同的了,所以每对一个结点暴力递归,总势能至少减少 \(1\)。而初始的总势能是 \(O(n\log n)\) 的,因此总复杂度是 \(O((n+m)\log n)\) 的。

【例二】

支持:

  1. 区间对某个数取 \(\min\)

  2. 区间加。

  3. 区间求和。

  4. 区间求最大值。


多了一个区间加。事实上,我们把取 \(\min\) 的 tag 升级为二元:\((a,b)\),表示先加 \(a\) 再对 \(b\)\(\min\)。有 \((a,b)+(c,d)=(a+c,\min(b+c,d))\),可合并。

对于复杂度分析,这里令 \(u\) 的势能为 "子树内,每个颜色在线段树上的虚树结点个数之和",据说可以分析到两个 log,但是我不太会。

【例三】

【UR #19】前进四 - 题目 - Universal Online Judge

支持:

  1. 单点修改。

  2. 查询后缀最小值的不同值个数。

\(n,q\le 10^5\)


  1. 位置和时间换维。

巧妙的吉司机的题。

\(a\) 为原序列,\(b\)\(a\) 的后缀最小值序列。题目要查询 \(b_x\sim b_n\) 的不同值个数。

问题涉及两个维度:位置、时间。我们往常的思路是枚举时间,对位置维护 DS;但这题不一样,我们枚举位置,对时间维护 DS。

设有 \(M\) 个查询,\(x_t\) 为第 \(t\) 次询问的 \(x\)。用 \(a[i](t)\) 表示 \(t\) 时刻 \(i\) 位置的 \(a\) 值,\(b[i](t)\) 表示 \(t\) 时刻 \(i\) 位置的 \(b\) 值。不妨 \(b[n+1](1\sim M)=+\infty\)

一个显而易见的观察是:\(b[i](t)\le b[i+1](t)\),同时 \(ans(t)=\sum_{i=x_t}^n[b[i](t)<b[i+1](t)]\)

我们的思路是从后往前枚举位置,用 \(b[i+1]()\)\(b[i]()\)。即将 \(b[1]\sim b[n]\) 视作关于 \(t\) 的函数,维护这个函数在 \(t=1\sim M\) 处的值。

观察变化:\(b[i](t)=\min(b[i+1](t),a[i](t))\)。同时我们注意到单点修改次数是 \(O(q)\) 的,所以 \(a[]()\) 的段数不会很多:\(a[1]\sim a[n]\)\(n\) 个函数在 \(1\sim M\) 上看,总段数是 \(O(n+q)\) 的。所以我们甚至可以直接枚举每个位置的每一段修改!

假设我们维护了一个 DS,第 \(p\) 个位置正记录着 \(b[i+1](p)\)。现在我们枚举 \(a[i]()\) 的每一段,相当于要支持区间对某个数取 \(\min\)。这里已经有点吉司机的味道了。

那怎么求答案呢?对于一个询问 Query x,设它位于时刻 \(t\)。当我们从后往前枚举到位置 \(x\) 的时候处理这个询问。正如刚才所说,它的答案等于 \(b[x](t)\sim b[n](t)\) 之间取小于号的位置个数——同时因为 \(b[x](t)=\min(b[x+1](t),a[x](t))\),而且我们是枚举 \(a[x]()\) 的段进行区间取 \(\min\),这个问题相当于询问在进行过的所有区间取 \(\min\) 中,真正让 \(t\) 这个位置的值改变的次数。注意,"\(t\) 这个位置" 对应的是 \(b[x](t),b[x+1](t),\dots\),就是我们想要求的。

怎么做呢?相当于要额外维护 \(w[1\sim M]\),在某个位置真的被改变时就要 \(+1\)

我们对线段树结点记录 \(mx,se\) 表示区间最大值、区间次大值,还额外记录一个 \(tag\):表示使结点区间内所有值等于 \(mx\) 的位置权值 \(+tag\)。 同时 \(mx\) 还兼任取 \(\min\) 的标记。

这两个标记是可以下传的。当祖先的 \(mx\) 比当前结点小,说明曾经祖先取 \(\min\) 是会影响到当前结点的。

void mktag(int x, int v, int t) {
    if (val[x].mx > v) {
        tag[x] += t;
        val[x].mx = v;
    }
} 
void pushdown(int x) {
    mktag(x * 2, val[x].mx, tag[x]);
    mktag(x * 2 + 1, val[x].mx, tag[x]);
    tag[x] = 0;
}

完整代码:

#include <bits/stdc++.h>

using namespace std;
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;

struct Val {
    int mx, mx2, cnt;
    Val(int _m = 0, int _m2 = 0, int _c = 0) {
        mx = _m, mx2 = _m2, cnt = _c;
    }
} val[N * 4];
Val operator+(Val x, Val y) {
    Val ans = Val();
    if (x.mx > y.mx) 
        ans.mx = x.mx, ans.cnt = x.cnt, ans.mx2 = max(x.mx2, y.mx);
    else if (x.mx < y.mx)
        ans.mx = y.mx, ans.cnt = y.cnt, ans.mx2 = max(x.mx, y.mx2);
    else
        ans.mx = x.mx, ans.cnt = x.cnt + y.cnt, ans.mx2 = max(x.mx2, y.mx2);
    return ans;
}
int tag[N << 2] = {};

void pushup(int x) {
    val[x] = val[x * 2] + val[x * 2 + 1];
}
void mktag(int x, int v, int t) {
    if (val[x].mx > v) {
        tag[x] += t;
        val[x].mx = v;
    }
} 
void pushdown(int x) {
    mktag(x * 2, val[x].mx, tag[x]);
    mktag(x * 2 + 1, val[x].mx, tag[x]);
    tag[x] = 0;
}
void build(int x, int lx, int rx) {
    if (lx + 1 == rx) {
        val[x] = Val(inf, 0, 1);
        return;
    }
    int mid = (lx + rx) / 2;
    build(x * 2, lx, mid);
    build(x * 2 + 1, mid, rx);
    pushup(x);
}
void mdf(int x, int lx, int rx, int l, int r, int v) {
    if (val[x].mx <= v || rx <= l || r <= lx)
        return ;
    if (l <= lx && rx <= r && val[x].mx2 < v) {
        mktag(x, v, 1);
        return ;
    }
    pushdown(x);
    int mid = (lx + rx) / 2;
    mdf(x * 2, lx, mid, l, r, v);
    mdf(x * 2 + 1, mid, rx, l, r, v);
    pushup(x);
}
int qry(int x, int lx, int rx, int p) {
    if (lx + 1 == rx)
        return tag[x];
    pushdown(x);
    int mid = (lx + rx) / 2;
    if (p < mid)
        return qry(x * 2, lx, mid, p);
    return qry(x * 2 + 1, mid, rx, p);
}

int n, m;
int a[N];

void read(int &x) {
    x = 0;
    char ch = getchar();
    while (ch < '0' || ch > '9')
        ch = getchar();
    while (ch >= '0' && ch <= '9')    
        x = x * 10 + ch - '0', ch = getchar();
}
void write(int x) {
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

vector<pair<int, int> > res[N];
vector<int> ask[N];
int ans[N] = {};

int main() {
    read(n), read(m);
    for (int i = 1; i <= n; i++) {
        read(a[i]);
        res[i].push_back(make_pair(1, a[i]));
    }
    for (int i = 1, op, x, v; i <= m; i++) {
        read(op), read(x);
        if (op == 1) {
            read(v);
            res[x].push_back(make_pair(i, v));
        }
        else 
            ask[x].push_back(i);
    }
    build(1, 1, m + 1);
    for (int i = n; i >= 1; i--) {
        for (int j = 0; j < (int)res[i].size(); j++) 
            mdf(1, 1, m + 1, res[i][j].first, (j == (int)res[i].size() - 1 ? m + 1 : res[i][j + 1].first), res[i][j].second);
        for (auto j: ask[i])
            ans[j] = qry(1, 1, m + 1, j);
    }
    for (int i = 1; i <= m; i++)
        if (ans[i])
            write(ans[i]), putchar('\n');
    return 0;
}
posted @ 2025-11-15 21:52  FLY_lai  阅读(13)  评论(0)    收藏  举报