数据结构杂题

简介

本博客主要用于记录一些神鸡数据结构题,并记录一些有趣的 trick。

“插入——标记——回收”算法解决函数复合问题

考虑这样一个函数复合问题:

给定一串函数 \(f_i\)(有可能是分段函数,即根据 \(x\) 的范围导致的 \(f\) 解析式不同),每次询问,给出 \(l, r, x\),求 \(f_l \circ f_{l + 1} \circ f_{l + 2} \circ \cdots \circ f_r \circ x\)。允许离线。

做法是:

  • 将询问离线下来,考虑进行扫描线。
  • 对于每一个询问,考虑将其两端点各拆成一个单点操作。
  • 每个操作,就是将一个数 \(x\) 加入一个可重集(或删除出并获取答案)。
  • 那么这样,就可以考虑如何全局维护一个集合的函数。
  • 发现维护时有可能需要根据 \(x\) 的范围来确定函数种类/有无。
  • 使用平衡树(FHQ Treap)来维护这件事情。
  • 最终维护的总体时间复杂度为 \(\mathcal O(n \log{n})\)

例题:QOJ #8672. 排队

题意:给定 \(f_i(x) = x + [x \in [l_i, r_i]]\),每次询问 \(f_l \circ f_{l + 1} \circ f_{l + 2} \circ \cdots \circ f_r \circ x\)

将操作离线后,用“插入-标记-回收”算法处理,建一颗 FHQ Treap,然后每次对于一个函数,直接对平衡树内 \([l_i, r_i]\) 打上 \(+ 1\) 标签即可。

#include<bits/stdc++.h>

using namespace std;

const int N = 1e6 + 5;

mt19937 gen(145);

inline int read(){
	int x = 0; char ch = getchar();
	while(ch <'0' || ch > '9') ch = getchar();
	while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
	return x; 
}

int n, q;
int l[N], r[N];
vector<int> in[N], out[N];

struct FHQ{
	int val[N], rnd[N], ls[N], rs[N], tag[N], fa[N], stk[N];
	int idx = 0, rt = 0, tot = 0;
	pair<int, int> tmp = {0, 0};
	
	int newnode(int v){ val[++ idx] = v, fa[idx] = 0, tag[idx] = 0, ls[idx] = rs[idx] = 0, rnd[idx] = gen(); return idx; }
	
	void down(int u, int x){ val[u] += x, tag[u] += x; }
	
	void down(int u){ if(tag[u]) down(ls[u], tag[u]), down(rs[u], tag[u]), tag[u] = 0; }
	
	pair<int, int> split(int u, int x){
		if(!u) return make_pair(0, 0);
		down(u);
		if(val[u] <= x) {
			tmp = split(rs[u], x);
			rs[u] = tmp.first;
			fa[rs[u]] = u;
			fa[tmp.second] = 0;
			return {u, tmp.second};
		} else {
			tmp = split(ls[u], x);
			ls[u] = tmp.second;
			fa[ls[u]] = u;
			fa[tmp.first] = 0;
			return {tmp.first, u};
		} 
	}
	
	int merge(int u, int v){
		if(!u || !v) return u + v;
		if(rnd[u] > rnd[v]) return down(u), rs[u] = merge(rs[u], v), fa[rs[u]] = u, u;
		else return down(v), ls[v] = merge(u, ls[v]), fa[ls[v]] = v, v;
	} 
	
	int getfirst(int u){ while(ls[u]) down(u), u = ls[u]; return val[u]; }
	
	int join(int x, int y){
		int ans = 0;
		while(x){
			tmp = split(x, getfirst(y));
			ans = merge(ans, tmp.first), x = tmp.second;
			swap(x, y);
		}
		return merge(ans, y);
	}
	
	void load(int u){
		while(fa[u]) u = fa[u], stk[++ tot] = u;
		while(tot) down(stk[tot --]);
	}
} T;

int to[N], ans[N];

int main(){
	n = read(), q = read();
	for(int i = 1; i <= n; i ++) l[i] = read(), r[i] = read(); 
	for(int i = 1, x, y; i <= q; i ++) x = read(), y = read(), in[x].emplace_back(i), out[y].emplace_back(i); 
	for(int i = 1; i <= n; i ++){
		for(auto j : in[i]){
			to[j] = T.newnode(0);
			T.rt = T.join(T.rt, to[j]);
		}
		int x, y, z;
		tie(x, y) = T.split(T.rt, l[i] - 1), tie(y, z) = T.split(y, r[i]);
		T.down(y, 1);
		T.rt = T.merge(x, T.join(y, z));
		for(auto j : out[i]){
			T.load(to[j]), ans[j] = T.val[to[j]];
		}
	}
	for(int i = 1; i <= q; i ++) printf("%d\n", ans[i]);
	return 0;
} 

矩阵乘法优化线段树标记问题

D

维护一个长度为 \(T\) 的整数序列:

  • Q x y:求 \([x, y]\) 的当前最大值。
  • A x y :求 \([x, y]\) 的历史最大值(包括现在)。
  • P x y Z:将 \([x, y]\) 每个数增加 \(Z\)
  • C x y Z:将 \([x, y]\) 覆盖为 \(Z\)

我们发现实际上我们并不好维护 \([x, y]\) 的历史最大值,无法很好地描述懒标记的下传。

矩阵乘法统一操作

我们使用矩阵乘法将所有操作统一为矩阵区间乘,避免懒标记的影响。

我们使用 \((max, +)\) 半环作为我们的矩阵。

初始矩阵:

\[\begin{bmatrix} mx \\ his \\ 0 \end{bmatrix} \]

考虑矩阵操作需要常数项,所以在此处我们添加一项常数,即加法单位元,作为我们的初始矩阵。

区间加 \(z\)

构造矩阵

\[\begin{bmatrix} z & -\infty & -\infty \\ z & 0 & -\infty \\-\infty & -\infty & 0\end{bmatrix} \]

区间覆盖 \(z\)

\(\begin{bmatrix}-\infty &-\infty &z \\ -\infty &0 &z \\ -\infty &-\infty &0\end{bmatrix}\)

使用完矩阵乘法优化后,我们发现,矩阵乘法的常数依旧很大,来到了 \(27\) 的级别。

考虑优化:

我们发现,矩阵中 \((1, 2), (2, 2), (3, 1), (3, 2), (3, 3)\) 是恒不变的,故我们考虑只维护剩下四个位置的情况,这样时间常数下降,来到了 \(4\)

其他的东西就和普通的线段树差不多了,记录懒标记时,如果没有操作,则记 \(tag\)\(单位矩阵 \; I\)

上述做法就是矩阵优化线段树,主要用于懒标记维护多或者标记难以下传等问题,适配于查询历史信息等经典模型。

#include<bits/stdc++.h>

using namespace std;

#define int long long

const int N = 1e5 + 5;
const int INF = 1e18;
const int NINF = -1e18;

int n, m;
int a[N];

int add(int x, int y) { return (x == NINF || y == NINF) ? NINF : x + y; }

struct Mat {
    int a11, a13, a21, a23;
    Mat() : a11(0), a13(NINF), a21(NINF), a23(NINF) {}
    Mat(int _11, int _13, int _21, int _23) : a11(_11), a13(_13), a21(_21), a23(_23) {}
    Mat operator * (const Mat& b) const {
        Mat c;
        c.a11 = add(a11, b.a11);
        c.a13 = max(add(a11, b.a13), a13);
        c.a21 = max(add(a21, b.a11), b.a21);
        c.a23 = max(max(add(a21, b.a13), b.a23), a23);
        return c;
    }
};

namespace segtree {
    #define ls (u << 1)
    #define rs (u << 1 | 1)
    #define mid ((l + r) >> 1)
    #define segroot int u = 1, int l = 1, int r = n
    #define lson ls, l, mid
    #define rson rs, mid + 1, r

    struct Node { int mx, his; Mat tag; } t[N*4];

    void up(int u) { t[u].mx = max(t[ls].mx, t[rs].mx), t[u].his = max(t[ls].his, t[rs].his); }

    void down(int u, const Mat& M) {
		int mx = t[u].mx, his = t[u].his;
		t[u].mx = max(add(M.a11, mx), M.a13);
		t[u].his = max(max(add(M.a21, mx), his), M.a23);
		t[u].tag = M * t[u].tag;
	}

	void down(int u) { down(ls, t[u].tag), down(rs, t[u].tag), t[u].tag = Mat(); }

	void bld(segroot) {
		t[u].tag = Mat();
		if(l == r) { return t[u].mx = t[u].his = a[l], void(); }
		bld(lson), bld(rson), up(u);
    }

	void mdf(int ql, int qr, const Mat& M, segroot) {
		if(qr < l || r < ql) return;
		if(ql <= l && r <= qr) { down(u, M); return; }
		down(u);
		mdf(ql, qr, M, lson), mdf(ql, qr, M, rson);
        up(u);
	}

	pair<int,int> query(int ql, int qr, segroot) {
		if(ql <= l && r <= qr) return {t[u].mx, t[u].his};
        down(u);
        if(qr <= mid) return query(ql, qr, lson);
        if(ql > mid) return query(ql, qr, rson);
        auto L = query(ql, qr, lson), R = query(ql, qr, rson);
        return {max(L.first, R.first), max(L.second, R.second)};
    }
}

using namespace segtree;

signed main() {
    scanf("%lld", &n);
    for(int i = 1; i <= n; ++i) scanf("%lld", &a[i]);
    bld();
    scanf("%lld", &m);
    char op; int x, y, z;
    while(m--) {
        scanf(" %c", &op);
        if(op == 'Q') {
            scanf("%lld%lld", &x, &y);
            printf("%lld\n", query(x, y).first);
        } else if(op == 'A') {
            scanf("%lld%lld", &x, &y);
            printf("%lld\n", query(x, y).second);
        } else if(op == 'P') {
            scanf("%lld%lld%lld", &x, &y, &z);
            mdf(x, y, Mat(z, NINF, z, NINF));
        } else if(op == 'C') {
            scanf("%lld%lld%lld", &x, &y, &z);
            mdf(x, y, Mat(NINF, z, NINF, z));
        }
    }
    return 0;
}

P7838 「Wdoi-3」夜雀 treating - 洛谷

一个 \(2q + 1\) 的排列,每次操作取出最中间的数,并选择再一个数从排列中删去。求最后取出得到的序列最长连续段长度最长可以是多少。

同样地,此题也是:T696069 列表 - 洛谷

引理1 一个子集 \(S\) 最后能够得到,对 \(\forall i \in Q\),初始序列中一段下标 \([N + 1 - i, N + 1 + i]\) 中的 \(2i + 1\) 个数中至少含有 \(i + 1\) 个数 \(\in S\)

证明:首先,可以证明,第 \(i + 1\) 次操作加入的数的下标一定在 \([N + 1 - i, N + 1 + i]\) 内。所以说在这个范围内合法,一定至少要有 \(i + 1\) 个数。反过来,每次删除一个数,就将里中心最近的一个不在 \(S\) 里数删掉,一定最优。

引理2 一个连续段 \(A\) 想要满足 \(A \subseteq S\),当且仅当:\(\forall i \in Q\),不在序列初始中间下标 \([N - i + 1, N + i - 1]\) 中的数最多有 \(N - i\) 个数 \(\in A\)

所以说,一个连续段 \(A\) 合法,当且仅当 \(\forall i, |A \cap [N - i + 1, N + i - 1]| \ge |A| - N + i\)

由此,我们想到做法:枚举连续数字段然后判定是否合法,时间复杂度 \(O(N^2)\)

我们容易发现,对于一个左端点 \(l\),都有一个最大的右端点,称为 \(f(l)\),且 \(f(l) \le f(l + 1)\),于是我们可以尝试双指针。

现在我们对于连续段 $A = [l, r] $ “合法”的判定方式是:

\[\forall i, |A \cap [N - i + 1, N + i - 1]| \ge m - N + 1 \]

其中 \(m = r - l + 1\)

转化一下:对于一个位置 \(p\),到中心的距离 \(d = |p - (N + 1)|\)

这个位置 \(\in [N - i + 1, N + i + 1]\),当且仅当 \(i \ge d\)

所以我们每加入或删除一个位置的数,就会对一个后缀产生 \(+1/-1\) 的影响,故我们使用线段树维护:

\[val_i = |A \cap [N - i + 1, N + i + 1]| - i \]

那么最后我们检查是否合法,就是检查:

\[\min \{val_i\} \ge |A| - N \]

动态全局记录一个 \(|A|\) 即可。

最终时间复杂度:\(\mathcal O(N \log N)\)

#include<bits/stdc++.h>

using namespace std;

const int N = 4e5 + 5;

int n, m, Mid;
int pos[N];

struct segtree{
	#define ls u << 1
	#define rs u << 1 | 1
	#define mid (l + r >> 1)
	#define segroot int u = 1, int l = 0, int r = n
	#define lson ls, l, mid
	#define rson rs, mid + 1, r

	int mn[N << 2], tag[N << 2];

	void up(int u){ mn[u] = min(mn[ls], mn[rs]); }

	void bld(segroot){
		if(l == r) return mn[u] = -l, void();
		bld(lson), bld(rson), up(u);
	}

	void down(int u, int x){ mn[u] += x, tag[u] += x; }

	void down(int u){ down(ls, tag[u]), down(rs, tag[u]), tag[u] = 0; }

	void mdf(int ql, int qr, int v, segroot){
		if(l > qr || r < ql) return ;
		if(ql <= l && r <= qr) return down(u, v);
		down(u), mdf(ql, qr, v, lson), mdf(ql, qr, v, rson), up(u);
	}

	int qry(){ return mn[1]; }
} T;

inline int getd(int x){ return abs(pos[x] - Mid); }

int main() {
	freopen("echo.in", "r", stdin);
	freopen("echo.out", "w", stdout);
    ios::sync_with_stdio(0), cin.tie(nullptr), cout.tie(nullptr);
	cin>>n;
	int m = 2 * n + 1;
	for(int i = 1, x; i <= m; i ++) cin>>x, pos[x] = i;
	Mid = n + 1;
	T.bld();
	int ans = 0;
    for (int l = 1, r = 0; l <= m; l ++){
        r = max(r, l - 1);
        while (r < m) {
            int nxt = r + 1;
			T.mdf(getd(nxt), n, 1);
            int new_m = r - l + 2;
            if (T.qry() >= new_m - n) {
                r = nxt;
                ans = max(ans, new_m);
            } else {
                T.mdf(getd(nxt), n, -1);
                break;
            }
        }
		if (l <= r) {
            T.mdf(getd(l), n, -1);
		}
	}
	cout<<ans<<'\n';
    return 0;
}

H - 不同的整数 --- H - Distinct Integers

每次询问一个区间 \([X_i, Y_i]\) 中有多少个子区间没有重复的数字。

\(pre_x\) 表示 \([1, x)\) 中最后一个与 \(x\) 位置颜色相同的位置。

一段合法的区间满足:\(\forall i \in [l, r], pre_i < l\),那我们对于每个 \(l\) 记录一个 \(Ans = \min\{pre_r > l\}\),不太好做,我们考虑换一个思路。

我们考虑固定 \(r\),记录 \(Ans = r - \max\limits_{j=X_i}^{r} \{pre_j\}\)

那对于所有的 \(r\)\(Ans = \sum\limits_{r=X_i}^{Y_i}(r - \max\limits_{j=X_i}^{r} \{pre_j\}) = \sum\limits_{r=X_i}^{Y_i}r - \sum\limits_{r=X_i}^{Y_i}\max\limits_{j=X_i}^{r} \{pre_j\}\)

我们现在要维护的就是前缀最大值的和。

类比楼房重建,我们维护最大值与前缀最大值之和。

对于左子树的和直接加起来,右子树直接递归类比线段树二分求解。

然后对于修改操作,我们需要支持插入或删除,使用平衡树或者开一个 set 维护 \(pre\) 即可。

最终时间复杂度为:\(\mathcal O(n \log^2n)\)

P2839 国家集训队 middle - 洛谷

求左端点在 \([a, b]\) 右端点在 \([c, d]\) 的所有区间的最大中位数。

我们钦定一个中位数 \(k\),再来判断 \(k\) 是不是真的是中位数。

我们把小于 \(k\) 的看作 \(-1\),大于 \(k\) 的看作 \(1\),等于 \(k\) 的看作 \(0\)

一个区间 \([l, r]\) 就可以分成三段,\([l, b]\)\((b, c)\)\([c, r]\)

我们只需要判断这里的和是否 \(\ge 0\),如果是,那么中位数还可以更大,不是的话,中位数就需要往小了找。

那怎么建立这样的线段树呢?

我们使用主席树。

考虑当 \(k\)\(k\) 变成 \(k+1\) 时,我们只需要对于有影响的点改动就可以修改成 \(k+1\) 的版本。

复杂度均摊下来就可以做到 \(\mathcal O(n \log n)\)

#include<bits/stdc++.h>

using namespace std;

const int N = 20005;
const int M = N * 20;

int n, m, cnt;
int a[N], val[N];
int rt[N];

struct Node {
    int s, l, r;
    Node(int _s = 0, int _l = 0, int _r = 0) : s(_s), l(_l), r(_r) {}
};

inline Node merge(const Node &x, const Node &y) {
    return Node(x.s + y.s, max(x.l, x.s + y.l), max(y.r, y.s + x.r));
}

struct PST {
    #define mid ((l + r) >> 1)
    #define segroot int &u, int l = 1, int r = n
    #define lson ls[u], l, mid
    #define rson rs[u], mid + 1, r

    int ls[M], rs[M], sum[M], lmx[M], rmx[M], idx;

    void up(int u) {
        sum[u] = sum[ls[u]] + sum[rs[u]];
        lmx[u] = max(lmx[ls[u]], sum[ls[u]] + lmx[rs[u]]);
        rmx[u] = max(rmx[rs[u]], sum[rs[u]] + rmx[ls[u]]);
    }

    void bld(segroot) {
		if(!u) u = ++ idx;
        if (l == r) return sum[u] = lmx[u] = rmx[u] = 1, void();
        bld(lson), bld(rson), up(u);
    }

    void mdf(int pos, int v, segroot) {
		if(l > pos || r < pos) return ;
        u = ++ idx;
        ls[u] = ls[v], rs[u] = rs[v], sum[u] = sum[v]; lmx[u] = lmx[v], rmx[u] = rmx[v];
        if (l == r) return sum[u] = lmx[u] = rmx[u] = -1, void();
        mdf(pos, ls[v], lson), mdf(pos, rs[v], rson), up(u);
    }

    Node qry(int ql, int qr, int u, int l=1, int r=n) {
        if (ql > qr) return Node(0, 0, 0);
        if (ql <= l && r <= qr) return Node(sum[u], lmx[u], rmx[u]);
        if (qr <= mid) return qry(ql, qr, lson);
        if (ql > mid) return qry(ql, qr, rson);
        return merge(qry(ql, qr, lson), qry(ql, qr, rson));
    }
} T;

int main() {
    scanf("%d", &n);
    vector<int> vec;
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        vec.push_back(a[i]);
    }
    sort(vec.begin(), vec.end());
    vec.erase(unique(vec.begin(), vec.end()), vec.end());
    cnt = vec.size();
    for(int i = 1; i <= n; i ++) val[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;
    vector<vector<int>> pos(cnt + 1);
    for(int i = 1; i <= n; i ++) pos[val[i]].push_back(i);
    T.bld(rt[1]);
    int cur = rt[1];
    for(int i = 2; i <= cnt; i ++){
        for(int p : pos[i - 1]){
            int new_rt = 0;
            T.mdf(p, cur, new_rt);
            cur = new_rt;
        }
        rt[i] = cur;
    }
    scanf("%d", &m);
    int ans = 0;
    while (m --) {
        int q[4];
        scanf("%d%d%d%d", &q[0], &q[1], &q[2], &q[3]);
        for (int i = 0; i < 4; ++i) q[i] = (q[i] + ans) % n;
        sort(q, q + 4);
        int a = q[0] + 1, b = q[1] + 1, c = q[2] + 1, d = q[3] + 1;
        int l = 1, r = cnt;
        while(l <= r){
            int md = (l + r) >> 1;
            Node L = T.qry(a, b, rt[md]);
            Node M = T.qry(b + 1, c - 1, rt[md]);
            Node R = T.qry(c, d, rt[md]);
            if (L.r + M.s + R.l >= 0) ans = vec[md - 1], l = md + 1;
            else r = md - 1;
        }
        printf("%d\n", ans);
    }
    return 0;
}

P4735 最大异或和 - 洛谷

进行前缀异或和处理。

对于一个区间。 \(pre_N \oplus x\) 是一个定值 \(k\),我们现在就要求 \(pre_{p - 1} \oplus k\) 最大。

发现如果没有区间限制,就会是一个简单的 \(\text{Trie}\) 可做的事情。

对于区间询问, 我们考虑使用可持久化 \(\text{01Trie}\),用 \([1, r]\) 中的信息除去 \([1, l - 1]\) 中的信息即可。

#include<bits/stdc++.h>

using namespace std;

const int N = 3e5 + 5;

int n, m;
int nxt[N * 25][2], idx, cnt[N * 25];
int rt[N * 2], sum[N * 2];

void add(int u, int v, int w, int x){
	if(w < 0) return ;
	int c = (x >> w) & 1;
	nxt[u][c ^ 1] = nxt[v][c ^ 1];
	nxt[u][c] = ++ idx;
	cnt[nxt[u][c]] = cnt[nxt[v][c]] + 1;
	add(nxt[u][c], nxt[v][c], w - 1, x);
}

int qry(int u, int v, int x, int w){
	if(w < 0) return 0;
	int c = (x >> w) & 1;
	if(cnt[nxt[v][c ^ 1]] > cnt[nxt[u][c ^ 1]]) return qry(nxt[u][c ^ 1], nxt[v][c ^ 1], x, w - 1) ^ (1 << w);
	return qry(nxt[u][c], nxt[v][c], x, w - 1);
}

int main(){
	scanf("%d%d", &n, &m);
	add(rt[0] = ++ idx, 0, 24, 0);
	for(int i = 1, x; i <= n; i ++){
		scanf("%d", &x);
		sum[i] = sum[i - 1] ^ x;
		add(rt[i] = ++ idx, rt[i - 1], 24, sum[i]);
	}
	while(m --){
		char op; int x, y, z; scanf("\n%c%d", &op, &x);
		if(op == 'A'){
			n ++;
			sum[n] = sum[n - 1] ^ x;
			add(rt[n] = ++ idx, rt[n - 1], 24, sum[n]);
		} else {
			scanf("%d%d", &y, &z);
			x --, y --;
			if(x == 0) printf("%d\n", qry(0, rt[y], sum[n] ^ z, 24));
			else printf("%d\n", qry(rt[x - 1], rt[y], sum[n] ^ z, 24));
		}
	}
	return 0;
}
posted @ 2026-07-20 14:14  Hty111  阅读(9)  评论(0)    收藏  举报