2024年11月

前言

算法竞赛题目考察的是选手对于数据结构的选取与算法的巧妙结合,而数据结构中线段树扮演一个至关重要的角色,而近期(CSP 结束)在 hfu 的安排下我们需要自己弄一周的 ds,所以就有了这篇奇妙的博客。

线段树基础知识

在我看来,线段树其实就是在数组的基础上添加了一些
额外
的点,这些点用于维护原数组的信息,在此基础上又添加一些点维护
之前添加的点
的信息,就这么一层一层往上直到最后只有一个点,而这个点也就管理了这整个数组。而这些点之间的管理关系也就形成了一棵树,故为线段树。

然后考虑添加的这些点,如果这些点很随意,最劣情况可能会被卡成一些神秘的东西,于是我们就让第一层维护两个点,第二层维护四个点,这样我们只会建出
\(\log\)
层,并且增加的节点数是
\(O(n)\)
的。这样线段树就有了很优美的性质,也称得上真正的线段树了。

对于用线段树维护信息,我们不必在每次修改操作将相关的节点全部修改,但是我们需要知道我们应该去修改哪些地方。比如我在树上走,走到了一个区间
\([l,r]\)
,如果这个区间被修改的区间包含,这就意味着我需要将这整个区间修改,可是直接修改是
\(O(len)\)
的,这时我们就可以先将这个地方的修改记下来,等到我查询这个区间的时候在处理它。这样在线段树上找出的区间是
\(O(\log)\)
的,所以正常的修改就是找区间加上
\(O(1)\)
打标记。

而查询和修改同理,都是在树上走每次碰到一个区间能够被全部算进贡献就直接加入贡献,否则就继续往下走,但是在修改和查询时都需要注意下放之前的标记,也就是我
下一次
走到有标记的节点时我会对其进行一些操作,这时原来的标记与现在的操作的两个区间大概率是不同的,所以在操作前我们需要下放之前的标记。

然后放两道板子。

P3372 【模板】线段树 1

我们可以将线段树给建出,过程就是一个遍历树的过程,只是到树叶(也就是原数组)时需要保存信息再一层一层传回去。修改与查询在此不再赘述。

void upd(int x){
	s[x] = s[ls] + s[rs];
}
void bld(int x, int l, int r){
	if(l == r)return (void)(s[x] = a[l]);
	int mid = l + r >> 1;
	bld(ls, l, mid), bld(rs, mid + 1, r);
	upd(x);
}
void pd(int x, int l, int r){
	if(! tg[x])return; int mid = l + r >> 1;
	tg[ls] += tg[x], tg[rs] += tg[x];
	s[ls] += tg[x] * (mid - l + 1);
	s[rs] += tg[x] * (r - mid);
	return (void)(tg[x] = 0);
}
void mdf(int x, int l, int r, int ql, int qr, ll y){
	if(ql <= l and r <= qr)return (void)(s[x] += y * (r - l + 1), tg[x] += y);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf(rs, mid + 1, r, ql, qr, y);
	upd(x);
}
ll qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return s[x];
	int mid = l + r >> 1; ll res = 0; pd(x, l, r);
	if(ql <= mid)res = qry(ls, l, mid, ql, qr);
	if(mid < qr)res += qry(rs, mid + 1, r, ql, qr);
	return res;
}

P3373 【模板】线段树 2

这时有了乘法操作修改与下放似乎变得复杂,这时我们需要先分析不同操作的优先级。

首先我们肯定对于乘法与加法分别打标记,然后对于一个加法标记,它的意义是让区间加上一个数,如果直接加那么乘法标记就会受影响。将乘法标记增加一定是错的,所以我们应该将它下放。注意到乘法的优先级是高于加法,所以在下放时要让乘法标记先下放。

如果是有一个乘法标记,这时之前区间内的加法标记也应该乘上它,因为乘法分配律。所以乘法就直接全改了就行。

void upd(int x){
	s[x] = (s[ls] + s[rs]) % p;
}
void bld(int x, int l, int r){
	tg2[x] = 1;
	if(l == r)return (void)(s[x] = a[l]);
	int mid = l + r >> 1;
	bld(ls, l, mid), bld(rs, mid + 1, r);
	upd(x);
}
void pd(int x, int l, int r){
	int mid = l + r >> 1;
	tg1[ls] = (tg1[ls] * tg2[x] + tg1[x]) % p, tg1[rs] = (tg1[rs] * tg2[x] + tg1[x]) % p;
	s[ls] = (s[ls] * tg2[x] + tg1[x] * (mid - l + 1)) % p;
	s[rs] = (s[rs] * tg2[x] + tg1[x] * (r - mid)) % p;
	tg2[ls] = tg2[ls] * tg2[x] % p;
	tg2[rs] = tg2[rs] * tg2[x] % p;
	return (void)(tg1[x] = 0, tg2[x] = 1);
}
void mdf1(int x, int l, int r, int ql, int qr, ll y){
	if(ql <= l and r <= qr)return (void)(s[x] = y * s[x] % p, tg2[x] = tg2[x] * y % p, tg1[x] = tg1[x] * y % p);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf1(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf1(rs, mid + 1, r, ql, qr, y);
	upd(x);
}
void mdf2(int x, int l, int r, int ql, int qr, ll y){
	if(ql <= l and r <= qr)return (void)(s[x] = (s[x] + y * (r - l + 1)) % p, tg1[x] = (tg1[x] + y) % p);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf2(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf2(rs, mid + 1, r, ql, qr, y);
	upd(x);
}
ll qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return s[x];
	int mid = l + r >> 1; ll res = 0; pd(x, l, r);
	if(ql <= mid)res = qry(ls, l, mid, ql, qr);
	if(mid < qr)res += qry(rs, mid + 1, r, ql, qr);
	return res % p;
}

P1253 扶苏的问题

这个题注意修改操作优先级大于加法操作,每次修改操作时需要清空标记。然后就是
不要在叶子节点
\(\text {pushdown}\)

void upd(int x){
    c[x] = max(c[ls], c[rs]);
}
void build(int x, int l, int r){
    tg1[x] = max0810;
    if(l == r)return(void)(c[x] = a[l]);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pdtg1(int x){
    if(tg1[x] == max0810)return;
    tg2[ls] = tg2[rs] = 0;
    c[ls] = c[rs] = tg1[ls] = tg1[rs] = tg1[x];
    tg1[x] = max0810;
}
void pd(int x){
    pdtg1(x);
    if(! tg2[x])return;
    c[ls] += tg2[x]; c[rs] += tg2[x];
    tg2[ls] += tg2[x]; tg2[rs] += tg2[x];
    tg2[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int op, ll y){
    if(L <= l and r <= R){
        if(op ^ 1){
            c[x] += y; tg2[x] += y;
        }
        else{
            c[x] = tg1[x] = y;
            tg2[x] = 0;
        }
        return;
    }
    int mid = l + r >> 1; pd(x);
    if(L <= mid)mdfy(ls, l, mid, L, R, op, y);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, op, y);
    upd(x);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return c[x];
    int mid = l + r >> 1; ll res = max0810; pd(x);
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid) res = max(res, qry(rs, mid + 1, r, L, R));
    return res;
}

P4513 小白逛公园
3倍经验见
这篇
(是的还是我)

线段树维护区间最大子段和的板子题,支持单点修改。

考虑上传答案的过程中需要维护的信息,首先有区间最大子段和
废话
,然后我们合并子区间时有可能从
左边区间的后缀最大加上右边区间的前缀最大
转移答案,所以还要记这两个东西。然后更新前后缀时似乎又需要记一个区间和,不然没法转(读者可自行思考)。然后查询时就同时记录这几个信息。单点修改就直接二分改了就行。

struct node{
	ll s, lm, rm, sum;
}sgt[N << 2];

void upd(node &x, node y, node z){
	x.sum = y.sum + z.sum;
	x.s = max(y.s, max(z.s, y.rm + z.lm));
	x.lm = max(y.lm, y.sum + z.lm), x.rm = max(z.rm, z.sum + y.rm);
}
void bld(int x, int l, int r){
	if(l == r)return(void)(sgt[x] = {a[l], a[l], a[l], a[l]});
	int mid = l + r >> 1;
	bld(ls, l, mid), bld(rs, mid + 1, r);
	upd(sgt[x], sgt[ls], sgt[rs]);
}
void mdf(int x, int l, int r, int pos, int y){
	if(l == r)return(void)(sgt[x] = {y, y, y, y});
	int mid = l + r >> 1;
	if(pos <= mid)mdf(ls, l, mid, pos, y);
	else mdf(rs, mid + 1, r, pos, y);
	upd(sgt[x], sgt[ls], sgt[rs]);
}
node qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return sgt[x];
	int mid = l + r >> 1; node res, res1, res2; bool o1(false), o2(false);
	if(ql <= mid)res1 = qry(ls, l, mid, ql, qr), o1 = true;
	if(mid < qr)res2 = qry(rs, mid + 1, r, ql, qr), o2 = true;
	if(! o1 or ! o2)res = o1 ? res1 : res2;
	else upd(res, res1, res2);
	return res;
}

P11071 「QMSOI R1」 Distorted Fate

首先拆位。观察式子,里面有一个按位或,说明了什么?说明如果
\(a_i\)
中出现了一那么后面的就都有值了,所以我们的任务就是每次找到区间内最左边的一然后答案就是从第一个一开始到右端点的长度。

但是看到空间限制你会发现这题有点抽象,所以你拆位后只能共用一棵线段树了。然后就是线段树维护的是区间是否有 0/1、以及每次异或后可能存在的懒标记这三者都可以用
bool
类型的数组维护。

但是这只是离线的,
因为不会在线所以自己去看吧

struct qarray{
    int op, l, r, x; ll ans;
}q[N];
bool b[N], s0[N << 2], s1[N << 2], tg[N << 2];

#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    s0[x] = s0[ls] | s0[rs];
    s1[x] = s1[ls] | s1[rs];
}

void build(int x, int l, int r){
    tg[x] = 0;
    if(l == r)return(void)(s0[x] = b[l] ^ 1, s1[x] = b[l]);
    int mid = l + r >> 1;
    build(ls, l, mid), build(rs, mid + 1, r);
    upd(x);
}
void pd(int x){
    if(! tg[x])return ;
    tg[ls] ^= 1, tg[rs] ^= 1;
    swap(s0[ls], s1[ls]);
    swap(s0[rs], s1[rs]);
    tg[x] = 0;
}
void mdf(int x, int l, int r, int ql, int qr){
    if(ql <= l and r <= qr)return(void)(swap(s0[x], s1[x]), tg[x] ^= 1);
    int mid = l + r >> 1; pd(x);
    if(ql <= mid)mdf(ls, l, mid, ql, qr);
    if(mid < qr)mdf(rs, mid + 1, r, ql, qr);
    upd(x);
}

int nkp;
bool op;

int fd(int x, int l, int r){
    if(l == r)return s1[x] ? l : - 1; pd(x); if(! s1[x])return - 1; int mid = l + r >> 1;
    if(s1[ls])return fd(ls, l, mid);
    else return fd(rs, mid + 1, r);
}

void qry(int x, int l, int r, int ql, int qr){
    if(op)return ;
    if(ql <= l and r <= qr){
        int res = fd(x, l, r);
        if(~ res)nkp = res, op = true;
        return;
    }
    int mid = l + r >> 1; pd(x);
    if(ql <= mid)qry(ls, l, mid, ql, qr);
    if(op)return ;
    if(mid < qr)qry(rs, mid + 1, r, ql, qr);
}
const int p = 1073741824;

signed main(){
    n = rd(), m = rd();
    for(int i = 1; i <= n; ++i)a[i] = rd();
    for(int i = 1; i <= m; ++i){
        int x = rd(), l = rd(), r = rd(), y;
        q[i] = {x, l, r};
        if(x == 1)q[i].x = rd();
    }
    for(int bit = 0; bit < 31; ++bit){
        for(int i = 1; i <= n; ++i)b[i] = a[i] >> bit & 1;
        build(1, 1, n);
        for(int i = 1; i <= m; ++i)if(q[i].op == 1){
            int x = q[i].x >> bit & 1;
            if(! x)continue; mdf(1, 1, n, q[i].l, q[i].r);
        }else{
            op = false, nkp = q[i].r + 1; qry(1, 1, n, q[i].l, q[i].r);
            q[i].ans += 1ll * (q[i].r - nkp + 1) * (1ll << bit) % p;
            q[i].ans %= p;
        }
    }
    for(int i = 1; i <= m; ++i)if(q[i].op ^ 1)printf("%lld\n", q[i].ans);
    return 0;
}

线段树的拓展1(可持久化)

可持久化其实就是加了一个可以访问历史版本的功能,就比如我询问之前某次修改后的答案你可以和普通线段树复杂度一样
\(O(\log n)\)
回答。

考虑具体如何实现。其实每次修改操作,我们只会修改
\(O(\log n)\)
个点,于是我们就可以把这些点组成的链扯出来,对于每次操作重新“开”一颗线段树,就像下面这样。

我们把每次操作看成一个树根,往下修改时就在原来的树上走,如果一个点不修改,就直接继承到现在的树上,否则就新开一个节点。然后其他东西就该咋弄咋弄,正常往这上面套各种线段树应该都行。

然后我们就不得不说一个非常重要的东西了。

可持久化权值线段树(主席树)

是的,我们如果让权值线段树可持久化,它就成了众所周知的主席树。这个东西能够查询静态区间第
\(k\)
小。至于动态区间第
\(k\)
小嘛,在外面再套一个树状数组不就行了?

先说静态的。我们把输入每一个数看成一次操作,而每次操作就是在值域中修改一个点。然后询问的时候就可以看成前缀和。在树上走的时候,对于寻找当前区间
\([l,r]\)
中值域里的第
\(k\)
大,我就正常线段树二分,但是同时二分两棵线段树,对于一个
\(mid\)
,前一半的值域在
\([l,r]\)
中数的个数
等价于
\([1,r]\)
中数的个数减去
\([1,l-1]\)
中数的个数

。然后就可以直接做了。

P3834 板子
以及
P1533 可怜的狗狗
双倍经验(真就直接复制)

代码如下:

int n, m, a[N], b[N], tt;
int rt[N << 5], c[N << 5], nd, ls[N << 5], rs[N << 5];

void upd(int &x, int y, int l, int r, int p){
    x = ++nd; c[x] = c[y] + 1;
    if(l == r)return; int mid = l + r >> 1;
    if(p <= mid)rs[x] = rs[y], upd(ls[x], ls[y], l, mid, p);
    else ls[x] = ls[y], upd(rs[x], rs[y], mid + 1, r, p);
}
int qry(int x, int y, int l, int r, int k){
    if(l == r)return l; int mid = l + r >> 1, res = c[ls[x]] - c[ls[y]];
    if(res >= k)return qry(ls[x], ls[y], l, mid, k);
    return qry(rs[x], rs[y], mid + 1, r, k - res);
}

signed main(){
    n = rd(), m = rd();
    for(int i = 1; i <= n; ++i)a[i] = b[i] = rd();
    sort(b + 1, b + 1 + n);
    tt = unique(b + 1, b + 1 + n) - b - 1;
    for(int i = 1; i <= n; ++i)a[i] = lower_bound(b + 1, b + 1 + tt, a[i]) - b, upd(rt[i], rt[i - 1], 1, tt, a[i]);
    for(int i = 1; i <= m; ++i){
        int l = rd(), r = rd(), k = rd();
        printf("%d\n", b[qry(rt[r], rt[l - 1], 1, tt, k)]);
    }
    return 0;
}

P2617 Dynamic Rankings

然后考虑动态区间怎么做。

我们可以给主席树套一棵树状数组,每次暴力修改完在树状数组上维护,注意树状数组记录的其实只是每一棵主席树的树根,所以跑树状数组每一个点时要在对应的主席树上进行修改。查询可类比上文,上文中我们记录了两个树根,现在我们可以把树状数组上的一些相关的树根开两个数组记一下,每次二分就把所有树的答案累加起来,其他部分与上面类似不再赘述。

代码如下:

namespace SGT{
    int nd, rt[N << 9], ls[N << 9], rs[N << 9], c[N << 9];
    void gotols(){
        for(int i = 1; i <= c1; ++i)q1[i] = ls[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = ls[q2[i]];
    }
    void gotors(){
        for(int i = 1; i <= c1; ++i)q1[i] = rs[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = rs[q2[i]];
    }
    void upd(int &x, int l, int r, int p, int y){
        if(! x)x = ++nd; c[x] += y;
        if(l == r)return; int mid = l + r >> 1;
        if(p <= mid)upd(ls[x], l, mid, p, y);
        else upd(rs[x], mid + 1, r, p, y);
    }
    int qryk(int l, int r, int k){
        if(l == r)return l; int mid = l + r >> 1, res = 0;
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        if(res >= k)return gotols(), qryk(l, mid, k);
        return gotors(), qryk(mid + 1, r, k - res);
    }
    int qryrk(int l, int r, int k){
        if(l == r)return 0; int mid = l + r >> 1, res = 0;
        if(k <= mid)return gotols(), qryrk(l, mid, k);
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        return gotors(), res + qryrk(mid + 1, r, k);
    }
}
using namespace SGT;
namespace BIT{
    int lb(int x){return x & - x;}
    void get(int l, int r){
        c1 = c2 = 0;
        for(int i = l - 1; i; i -= lb(i))q1[++c1] = rt[i];
        for(int i = r; i; i -= lb(i))q2[++c2] = rt[i];
    }
    void mdf(int p, int y){
        int x = lower_bound(b + 1, b + 1 + tt, a[p]) - b;
        for(; p <= n; p += lb(p))upd(rt[p], 1, tt, x, y);
    }
    int kth(int l, int r, int k){get(l, r); return qryk(1, tt, k);}
    int rk(int l, int r, int k){int kk = lower_bound(b + 1, b + 1 + tt, k) - b; get(l, r); return qryrk(1, tt, kk) + 1;}
}

P2839 [国家集训队] middle

首先分析题目性质,想想我们的答案有什么特点。因为是找最大中位数,是不是应该可以二分啊。假设当前二分一个
\(mid\)
,有一个套路就是把大于等于它的赋值成一,否则为零,然后看求子段和是否大于等于零即可判断。对于这道题,就是中间确定部分的可以用值域线段树维护,左右两边再分别开一个线段树维护前后缀最大子段和就行。

然后关于代码,就是放的我的远古时期写的
我自己都看的难受
神秘代码。

#include<bits/stdc++.h>
#define s(x) tr[x].sum
#define ls(x) tr[x].lson
#define rs(x) tr[x].rson
#define lm(x) tr[x].lmx
#define rm(x) tr[x].rmx
#define lc(x) tr[x].ncl
#define rc(x) tr[x].ncr
using namespace std;
const int N = 2e4 + 10;
int n, m, a[N], num[N], cnt, rt[N], idq, x1, x2, x3, x4;
int L(int x){return lower_bound(num + 1, num + cnt + 1, x) - num;}
struct tree
{
    int sum, lmx, rmx;
    int lson, rson;
    bool ncl, ncr; 
}tr[N * 40];
vector < int > c[N];
void upd(int x)
{
    s(x) = s(ls(x)) + s(rs(x));
    lm(x) = max(lm(ls(x)), s(ls(x)) + lm(rs(x)));
    rm(x) = max(rm(rs(x)), s(rs(x)) + rm(ls(x)));
}
int build(int l, int r)
{
    int x = ++idq;
    if(l == r)
	{
        if(L(a[l]) < 2)s(x) = lm(x) = rm(x) = - 1;
        else s(x) = lm(x) = rm(x) = 1;
        return x;
    }
    int mid = l + r >> 1; lc(x) = rc(x) = 1;
    ls(x) = build(l, mid); rs(x) = build(mid + 1, r);
    upd(x); return x;
}
int modify(int x, int y, int l, int r, int to, int val, bool cc)
{
    if(!cc)x = ++idq;
    if(l == r)
	{
        s(x) = lm(x) = rm(x) = val;
        return x;
    }
    int mid = l + r >> 1;
    if(to <= mid)
	{
        if(!rc(x)) rs(x) = rs(y);
        if(!lc(x))lc(x) = 1, ls(x) = modify(x, ls(y), l, mid, to, val, 0);
        else ls(x) = modify(ls(x), ls(y), l, mid, to, val, 1);
    }
    else
	{
        if(!lc(x)) ls(x) = ls(y);
        if(!rc(x))rc(x) = 1, rs(x) = modify(x, rs(y), mid + 1, r, to, val, 0);
        else rs(x) = modify(rs(x), rs(y), mid + 1, r, to, val, 1);
    }
    upd(x); return x;
}
int query(int x, int l, int r, int L, int R)
{
    if(L <= l and r <= R)return s(x);
    int mid = l + r >> 1; int ret = 0;
    if(L <= mid)ret += query(ls(x), l, mid, L, R);
    if(mid < R)ret += query(rs(x), mid + 1, r, L, R);
    return ret;
}
tree query1(int x, int l, int r, int L, int R)
{
    
    if(L <= l and r <= R)return tr[x];
    int mid = l + r >> 1;
    if(L <= mid and mid < R)
	{
        tree ret;
        tree le = query1(ls(x), l, mid, L, R);
        tree ri = query1(rs(x), mid + 1, r, L, R);
        ret.sum = le.sum + ri.sum;
        ret.lmx = max(le.lmx, le.sum + ri.lmx);
        return ret;
    }
    else if(L <= mid)return query1(ls(x), l, mid, L, R);
    else return query1(rs(x), mid + 1, r, L, R);
}
tree query2(int x, int l, int r, int L, int R)
{
    if(L <= l and r <= R)return tr[x];
    int mid = l + r >> 1;
    if(L <= mid and mid < R)
	{
        tree ret;
        tree le = query2(ls(x), l, mid, L, R);
        tree ri = query2(rs(x), mid + 1, r, L, R);
        ret.sum = le.sum + ri.sum;
        ret.rmx = max(ri.rmx, ri.sum + le.rmx);
        return ret;
    }
    else if(L <= mid)return query2(ls(x), l, mid, L, R);
    else return query2(rs(x), mid + 1, r, L, R);
}
bool check(int val)
{
    int sz = 0;
    if(x2 + 2 <= x3) sz = query(rt[val], 1, n, x2 + 1, x3 - 1);
    int sr = query1(rt[val], 1, n, x3, x4).lmx;
    int sl = query2(rt[val], 1, n, x1, x2).rmx;
    return (sl + sz + sr) >= 0;
}
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)scanf("%d", &a[i]), num[++cnt] = a[i];
    sort(num + 1, num + cnt + 1);
    cnt = unique(num + 1, num + cnt + 1) - num - 1;
    for(int i = 1; i <= n; i++)c[L(a[i])].push_back(i);
    rt[1] = build(1,n);
    for(int i = 2; i <= cnt; i++)for(int j = 0; j < c[i - 1].size(); j++)
	{
	    int go = c[i - 1][j];
	    rt[i] = modify(rt[i], rt[i - 1], 1, n, go, - 1, rt[i] > 0);
	}
    scanf("%d", &m);
    int las = 0;
    int d[6];
    while(m--)
	{
        scanf("%d %d %d %d", &x1, &x2, &x3, &x4);
        d[1] = (x1 + las) % n;
        d[2] = (x2 + las) % n;
        d[3] = (x3 + las) % n;
        d[4] = (x4 + las) % n;
        sort(d + 1, d + 5);
        x1 = d[1] + 1, x2 = d[2] + 1, x3 = d[3] + 1, x4 = d[4] + 1;
        int l = 1, r = cnt;
        int ans = 0;
        while(l <= r)
		{
            int mid = l + r >> 1;
            if(check(mid))ans = mid, l = mid + 1;
            else r = mid - 1;
        }
        las = num[ans];
        printf("%d\n", las);
    }
    return 0;
}

P4137 Rmq Problem / mex

套路题。考虑我们每次记录
\(val\)
上次存在的位置,查询区间
\([l,r]\)
就是在
\(rt_r\)
子树中找满足出现时间小于
\(l\)
的最小
\(val\)
。用主席树维护最小值即可。

void upd(int &x, int y, int l, int r, int p, int val){
    if(! x)x = ++tot; if(l == r)return(void)(ans[x] = val); int mid = l + r >> 1;
    if(p <= mid)rs[x] = rs[y], upd(ls[x], ls[y], l, mid, p, val);
    else ls[x] = ls[y], upd(rs[x], rs[y], mid + 1, r, p, val);
    ans[x] = min(ans[ls[x]], ans[rs[x]]);
}
int qry(int x, int l, int r, int lim){
    if(l == r)return l; int mid = l + r >> 1;
    if(ans[ls[x]] < lim)return qry(ls[x], l, mid, lim);
    else return qry(rs[x], mid + 1, r, lim);
}

signed main(){
    // fileio(fil);
    n = rd() + 1, m = rd();
    for(int i = 1, x; i < n; ++i){
        x = rd() + 1; if(x > n)rt[i] = rt[i - 1];
        else upd(rt[i], rt[i - 1], 1, n, x, i);
    }
    for(int i = 1; i <= m; ++i){
        int l = rd(), r = rd();
        printf("%d\n", qry(rt[r], 1, n, l) - 1);
    }
    return 0;
}

线段树的拓展2(线段树合并)

注意合并要用
动态开点线段树
,不然复杂度会变成
\(O(n\log^2n)\)
的,因为线段树有
\(O(n\log n)\)
个节点,合并又是
\(O(\log n)\)
的。

怎么合并呢?我们还是在树上走,如果走到一点地方时存在节点(可能是一个或两个)为空,就可以直接返回另一个节点,否则就往下递归,到叶子时合并回来即可。但是乍一看这不是爆炸了吗?所以接下来我们需要
口胡
细致分析一下复杂度。

容易发现:在合并两棵树的过程中走到有空节点的时候就返回了,相当于走一个点就会删掉一些点,所以总复杂度不会超过总点数也就是
\(O(n\log n)\)

讲题前先谈谈我对于线段树合并的理解。首先为什么需要合并线段树,就是因为对于一些问题我们需要合并的信息是
\(O(n)\)
级别的,对于这类式子我们肯定需要一种
\(O(\log)\)
的方式去优化。然后就是如何去理解合并的过程,因为如果真的每一个点都开一个线段树那肯定爆炸没得说。其实我们是把每个点对应在一段区间上,或是下标或是值域,然后对于每一个点的线段树动态开点,最后是合并过程中并不是单纯的加加减减,而是要根据你的式子去还原整个过程,或许你现在还不太懂,但等你看了后面某道题后你便会恍然大悟。

然后看一道板子。

P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并

观察到是在树上修改,于是可以联想到某个经典套路,将树上路径转化成差分。具体的,对于一条路径
\((u,v)\)
,设
\(t=lca(u,v)\)
,则我可以把对于
\((u,v)\)
的操作看成对于
\((root,u)\)

\((root,v)\)
的操作还有
\((root,t)\)

\((root,fa_t)\)
的逆操作。然后考虑对每个点开线段树。本题可以用颜色做线段树下标,然后就似乎是单点修改(直接线段树二分)加上线段树合并。

然后稍微注意一下树上合并时需要跑一遍 dfs 序,然后递归返回时合并。

void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1, sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
int lca(int u, int v){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        u = fa[tp[u]];
    }
    return dep[u] < dep[v] ? u : v;
}

void upd(int x){
    if(s[ls[x]] > s[rs[x]] or (s[ls[x]] == s[rs[x]] and col[ls[x]] < col[rs[x]]))return(void)(s[x] = s[ls[x]], col[x] = col[ls[x]]);
    s[x] = s[rs[x]], col[x] = col[rs[x]];
}
void md(int &p, int l, int r, int c, int y){
    if(! p)p = ++tot;
    if(l == r)return(void)(s[p] += y, col[p] = c);
    int mid = l + r >> 1;
    if(c <= mid)md(ls[p], l, mid, c, y);
    else md(rs[p], mid + 1, r, c, y);
    upd(p);
}
int merge(int a, int b, int l, int r){
    if(! a or ! b)return a | b;
    if(l == r)return s[a] += s[b], a;
    int mid = l + r >> 1;
    ls[a] = merge(ls[a], ls[b], l, mid);
    rs[a] = merge(rs[a], rs[b], mid + 1, r);
    upd(a); return a;
}
void dfs(int u){
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u])continue;
        dfs(v); rt[u] = merge(rt[u], rt[v], 1, 100000);
    }
    if(s[rt[u]])ans[u] = col[rt[u]];
}

signed main(){
    n = rd(), q = rd();
    for(int i = 1; i < n; ++i){
        int u = rd(), v = rd();
        add(u, v); add(v, u);
    }
    dfs1(1, 0); dfs2(1, 1);
    for(int i = 1; i <= q; ++i){
        int u = rd(), v = rd(), w = rd();
        md(rt[u], 1, 100000, w, 1); md(rt[v], 1, 100000, w, 1);
        int f = lca(u, v);
        md(rt[f], 1, 100000, w, - 1); md(rt[fa[f]], 1, 100000, w, - 1);
    }
    dfs(1);
    for(int i = 1; i <= n; ++i)printf("%d\n", ans[i]);
    return 0;
}

P3224 [HNOI2012] 永无乡

怎么感觉有点过于板子了?

对于求第
\(k\)
大的问题我们用值域线段树,然后考虑怎么解决连通性?然后你会发现直接并查集维护,然后并查集就是菊花图,对每朵花的花心开一棵线段树,然后正常合并即可。

int fd(int x){
    return x == f[x] ? x : f[x] = fd(f[x]);
}

void upd(int x){
    s[x] = s[ls[x]] + s[rs[x]];
}
void md(int &p, int l, int r, int y){
    if(! p)p = ++cnt;
    if(l == r)return(void)(++s[p]);
    int mid = l + r >> 1;
    if(y <= mid)md(ls[p], l, mid, y);
    else md(rs[p], mid + 1, r, y);
    upd(p);
}
int merge(int a, int b, int l, int r){
    if(! a or ! b)return a | b;
    if(l == r)return s[a] += s[b], a;
    int mid = l + r >> 1;
    ls[a] = merge(ls[a], ls[b], l, mid);
    rs[a] = merge(rs[a], rs[b], mid + 1, r);
    upd(a); return a;
}
int qry(int p, int l, int r, int y){
    if(l == r)return l;
    int mid = l + r >> 1, ans;
    if(s[ls[p]] >= y)ans = qry(ls[p], l, mid, y);
    else ans = qry(rs[p], mid + 1, r, y - s[ls[p]]);
    return ans;
}

void sol1(){
    int x = rd(), y = rd();
    x = fd(x), y = fd(y);
    if(x == y)return;
    f[y] = x; rt[x] = merge(rt[x], rt[y], 1, 100000);
}

signed main(){
    n = rd(), m = rd();
    for(int i = 1, x; i <= n; ++i)md(rt[i], 1, 100000, x = rd()), f[i] = i, pos[x] = i;
    for(int i = 1; i <= m; ++i)sol1();
    q = rd();
    for(int i = 1; i <= q; ++i){
        char c; cin >> c;
        if(c == 'B')sol1();
        else{
            int x = rd(), y = rd(); x = fd(x);
            if(s[rt[x]] < y){puts("-1"); continue;}
            printf("%d\n", pos[qry(rt[x], 1, 100000, y)]);
        }
    }
    return 0;
}

是不是觉得太简单了?那么来一点有难度的。

P5298 [PKUWC2018] Minimax

对于这道题我们可以很容易想到一个暴力 dp,设
\(f_{u,i}\)
表示
\(u\)
节点为第
\(i\)
大的数的概率。然后答案就是:

\[\sum_{i=1}^mi\times V_i\times f_{1,i}^2
\]

然后开始转移,我们令当前点
\(u\)
的左儿子叫
\(l\)
,右儿子叫
\(r\)
,转移式子也就呼之欲出:

\[f_{u,i}=f_{l,i}\times p_u\times\sum_{j<i}f_{r,j}+
f_{r,i}\times p_u\times\sum_{j<i}f_{l,j}+
f_{l,i}\times (1-p_u)\times\sum_{j>i}f_{r,j}+
f_{r,i}\times (1-p_u)\times\sum_{j>i}f_{l,j}
\]

希望没有笔误

然后你就发现每次你都需要拿一段前缀出来转移,能不能优化一下啊?然而确实可以。考虑线段树维护,下标就是排名(dp 的第二维)。现在你就把每个
\(f_i\)
都看成二叉树上的点权,然后就可以合并了但是,

考虑这道题的合并并不是单纯的加减,我们上面推出的转移式中
\(f_i\)
是带了系数的,所以在合并时我们还需要记一下系数,那么系数又怎么求呢?

考虑把上面的式子拆完,就是对于线段树上一个点
\(i\)
,它的系数就是:

\[\sum_{k,j<i}p_k\times f_j+\sum_{k',j'>i}(1-p_{k'})\times f_{j'}
\]

说人话就是左边(下标)比它小的点乘上对应节点的概率
\(p\)
加上右边的。所以按照上面的实现就好了。

此题小结

其实线段树合并的本质就是优化状态转移。对于合并的顺序需要注意与线段树配套用的工具具体选择,然后对于合并的式子其实就是转移式子的复现,具体一点也就是
拆分再乘法分配律合并同类项

拓展到一般的题,我们最开始可以考虑 native 的想法,写出稍微暴力的转移与维护方式,然后再观察这些东西能不能通过什么性质把他们变得具有一些优美的性质,比如可加性、可减性、单调性之类的,最后在思考如何套线段树显得优雅。

代码

const db eps = 1e-8;
const ll inf = 1e18;
const int N = 3e5 + 5, p = 998244353;

ll qmi(ll x, ll y){
	ll res = 1ll;
	for(; y; y >>= 1, x = x * x % p)if(y & 1)res = res * x % p;
	return res;
}

const ll ii = qmi(10000, p - 2);

int n, b[N], nd, m;
int rt[N << 5], ls[N << 5], rs[N << 5];
ll s[N << 5], tg[N << 5], f[N], a[N];
int fa[N], c[N], ch[N][2];

inline int jia(int x, int y){return x - p + y >= 0 ? x - p + y : x + y;}

void upd(int x){
	s[x] = jia(s[ls[x]], s[rs[x]]);
}
void updd(int x, ll y){
	if(! x)return;
	s[x] = s[x] * y % p;
	tg[x] = tg[x] * y % p;
}
void pd(int x){
	if(tg[x] == 1)return;
	updd(ls[x], tg[x]), updd(rs[x], tg[x]);
	return (void)(tg[x] = 1);
}
void addin(int &x, int l, int r, int pos){
	if(! x)tg[x = ++nd] = 1;
	if(l == r)return(void)(s[x] = 1);
	int mid = l + r >> 1; pd(x);
	if(pos <= mid)addin(ls[x], l, mid, pos);
	else addin(rs[x], mid + 1, r, pos);
	upd(x);
}
int merge(int x, int y, int l, int r, ll sx, ll sy, ll pp){
	if(! x or ! y)return updd(x | y, (x ? sx : sy)), x | y;
	int mid = l + r >> 1; pd(x), pd(y);
	ll lsl = s[ls[x]], rsl = s[rs[x]], lsr = s[ls[y]], rsr = s[rs[y]];
	ls[x] = merge(ls[x], ls[y], l, mid, jia(sx, rsr * (p + 1 - pp) % p), jia(sy, rsl * (p + 1 - pp) % p), pp);
	rs[x] = merge(rs[x], rs[y], mid + 1, r, jia(sx, lsr * pp % p), jia(sy, lsl * pp % p), pp);
	return upd(x), x;
}
void dfs(int u){
	if(! c[u])addin(rt[u], 1, m, a[u]);
	if(c[u] == 1)dfs(ch[u][0]), rt[u] = rt[ch[u][0]];
	if(c[u] == 2)dfs(ch[u][0]), dfs(ch[u][1]), rt[u] = merge(rt[ch[u][0]], rt[ch[u][1]], 1, m, 0, 0, a[u]);
}
void getans(int x, int l, int r){
	if(! x)return;
	if(l == r)return(void)(f[l] = s[x]);
	int mid = l + r >> 1; pd(x);
	getans(ls[x], l, mid); getans(rs[x], mid + 1, r);
}

int main(){
	rd(n);
	for(int i = 1; i <= n; ++i)rd(fa[i]), fa[i] ? ch[fa[i]][c[fa[i]]++] = i : 0;
	for(int i = 1; i <= n; ++i){
		rd(a[i]);
		if(c[i])a[i] = a[i] * ii % p;
		else b[++m] = a[i];
	}
	sort(b + 1, b + 1 + m);
	for(int i = 1; i <= n; ++i)if(! c[i])a[i] = lower_bound(b + 1, b + 1 + m, a[i]) - b;
	dfs(1); getans(rt[1], 1, m); ll ans = 0;
	for(int i = 1; i <= m; ++i)ans = jia(ans, 1ll * i * b[i] % p * f[i] % p * f[i] % p);
	wt(ans);
	return 0;
}

(为什么没有线段树分裂?因为
我不会
感觉不太常见就先放了)

线段树的拓展3(李超线段树)

简单介绍一下,李超线段树维护了一些线段,它可以快速查询位置上纵坐标最大/小的线段编号以及数值。考虑这种东西就很适合去解决一些需要
从区间中找最值转移状态
的题。

然后讲一下实现与应用。

李超线段树的节点维护的是在
\(mid\)
处纵坐标最大的线段,所以实际维护的时候它就很暴力。直接在树上找到它的范围然后就每次暴力比较线段就完了。分析复杂度,查找是一只
\(\log\)
,然后暴力更新的时候因为两个一次函数最多只有一个交点,所以它之后递归一个子区间,复杂度也是一只
\(\log\)
,所以一共是两只
\(\log\)

查询时候就从根走到对应的叶子节点,走的时候顺路记一下最值就完了。

板子
和他的
双倍经验

const int N = 1e5 + 5, mod1 = 39989, mod2 = 1e9 + 1;
const ll INF = 1e18;
int n;
lb k[N], b[N];
int sgt[N << 2];

#define ls x << 1
#define rs x << 1 | 1
lb calc(int x, int pos){
    return k[x] * pos + b[x];
}
bool eq(lb a, lb b){
    lb eps = 1e-10;
    if(a - b < eps and b - a < eps)return true;
    return false;
}
bool cmp(int x1, int x2, int pos){
    lb a = calc(x1, pos), b = calc(x2, pos);
    return a > b || (a == b and x1 < x2);
}
void _upd(int tmp, int x, int l, int r){
    int mid = l + r >> 1;
    if(cmp(tmp, sgt[x], mid))swap(tmp, sgt[x]);
    if(cmp(tmp, sgt[x], l))_upd(tmp, ls, l, mid);
    if(cmp(tmp, sgt[x], r))_upd(tmp, rs, mid + 1, r);
}
void upd(int x, int l, int r, int L, int R, int tmp){
    if(L <= l and r <= R)return _upd(tmp, x, l, r);
    int mid = l + r >> 1;
    if(L <= mid)upd(ls, l, mid, L, R, tmp);
    if(R > mid)upd(rs, mid + 1, r, L, R, tmp);
}
int qry(int x, int l, int r, int pos){
    int mid = l + r >> 1, res = sgt[x];
    if(l == r)return res;
    int tmp = pos <= mid ? qry(ls, l, mid, pos) : qry(rs, mid + 1, r, pos);
    if(cmp(tmp, res, pos))swap(res, tmp);
    return res;
}

signed main(){
    n = rd();
    b[0] = - INF;
    int ans = 0, cnt = 0;
    for(int i = 1; i <= n; ++i){
        int op = rd();
        if(op == 0){
            int pos = (rd() + ans + mod1 - 1) % mod1 + 1;
            printf("%d\n", ans = qry(1, 1, mod1, pos));
        }
        else{
            int x1 = rd(), y1 = rd(), x2 = rd(), y2 = rd();
            x1 = (x1 + ans - 1) % mod1 + 1;
            y1 = (y1 + ans - 1) % mod2 + 1;
            x2 = (x2 + ans - 1) % mod1 + 1;
            y2 = (y2 + ans - 1) % mod2 + 1;
            if(x1 > x2)swap(x1, x2), swap(y1, y2);
            if(x1 == x2)k[++cnt] = 0, b[cnt] = max(y1, y2);
            else{
                k[++cnt] = (lb)(y1 - y2) / (x1 - x2);
                b[cnt] = y1 - k[cnt] * x1;
            }
            upd(1, 1, mod1, x1, x2, cnt);
        }
    }
    return 0;
}

然后讲一些应用吧(?)

其实李超线段树
最主要
的运用还是结合着斜率优化 dp 一起考,本来我想放在后面与 dp 的结合的,想想还是算了。但是我在这一块的题写的很少,所以就会讲一点次要的。

P3081 [USACO13MAR] Hill Walk G

我们从
\((0,0)\)
开始走。如果直接建李超线段树那么当我们想要取下面部分的线段时就出问题了,所以我们就需要一个支持删除的东西维护它。于是考虑平衡树。

然后走的时候我们是横坐标排序,所以现在就只用考虑纵坐标。在每次走到一个线段的终点时我们需要看可行区间中比当前线段低一层的是谁。然后就想到把线段全部丢到平衡树里面,通过某种排序方使这些线段从高到低排好序。

那么如何判断两个线段的高低呢?我们通过下图来分析。

如果我要比较上面两条线段,我首先会去看它们右端点的位置。显然后者更靠右,然后我们把它们的右端点连接起来(如图红线)。如果前者更高(如图),那么
红线的斜率就会小于后面那条线段的斜率
,否则就会是图片下半部分的情况。于是我们比较线段之间的优先级时就可以先比较右端点再比较斜率了。

bool operator <(line x, line y){
	if(x.c < y.c)return 1ll * (y.c - x.c) * (y.d - y.b) < 1ll * (y.d - x.d) * (y.c - y.a);
	return 1ll * (x.c - y.c) * (x.d - x.b) > 1ll * (x.c - x.a) * (x.d - y.d);
}

注意如果直接用除法可能精度会炸。

然后就从左往右扫一遍,把线段该加入平衡树的就加进来,该删的就删掉。如果删掉的是当前所在的区间就在平衡树里查找一个比它低一层的线段然后更新答案。实际实现时可以用
set
代替平衡树。

int main(){
	rd(n);
	for(int i = 1; i <= n; ++i)rd(l[i].a, l[i].b, l[i].c, l[i].d), p[i - 1 << 1 | 1] = {l[i].a, l[i].b, i}, p[i << 1] = {l[i].c, l[i].d, i}, l[i].id = i;
	s.ins(l[1]); sort(p + 1, p + 1 + (n << 1));
	for(int i = 2; i <= (n << 1); ++i)
		if(p[i].x == l[p[i].id].a)s.ins(l[p[i].id]);
		else if(p[i].id == cur){
			set < line > :: iterator it = s.find(l[p[i].id]);
			if(it == s.begin())break;
			++ans; it--;
			cur = it -> id;
		}
		else s.del(l[p[i].id]);
	wt(ans);
	return 0;
}

本来有一道题的但是我把它放到树剖套线段树里面了。

本来还有一道题但是我把它又放到 dp 优化里面了。

所以因为各种原因,李超线段树就写到此为止吧!但是我能够保证你能在后面的内容中找到李超线段树的影子。

线段树的拓展4(扫描线)

这里只讲狭义扫描线也就是扫面积并,至于广义的扫面线等之后有空我会单独写一篇博客详细讲解。

就简单讲讲扫面积并。以从上往下扫为例,我们可以把图形拆成若干
平行于横轴的代表加一减一的线段
。每经过一个线段,这个奇形怪状的图形的横截长度就会发生变化,我们可以每次维护这个变化,并统计出上一次的横截长度对应的面积。然后对于经过线段,我们可以看成区间的加减,查询就是询问区间内非零个数。

板子

int n, cnt[N << 2], top;
ll f[N << 2], y[N << 2], ans;
struct node{
    ll x1, y1, y2;
    int op;
    bool operator < (const node &rhs)const {
        return x1 < rhs.x1;
    }
}a[N << 2];

#define ls x << 1
#define rs x << 1 | 1
void upd(int x, int l, int r){
    if(cnt[x])return (void)(f[x] = y[r] - y[l - 1]);
    f[x] = f[ls] + f[rs];
}
void modify(int x, int l, int r, int L, int R, int op){
    if(L <= l and r <= R){cnt[x] += op; upd(x, l, r); return;}
    int mid = l + r >> 1;
    if(L <= mid)modify(ls, l, mid, L, R, op);
    if(R > mid)modify(rs, mid + 1, r, L, R, op);
    upd(x, l, r);
}

signed main(){
    //freopen(, stdin);
    //freopen(, stdout);
    n = rd();
    for(int i = 1; i <= n; ++i){
        ll x1 = rd(), y1 = rd(), x2 = rd(), y2 = rd();
        a[++top] = {x1, y1, y2, 1}; y[top] = y1;
        a[++top] = {x2, y1, y2, - 1}; y[top] = y2;
    }
    sort(y + 1, y + 1 + top);
    sort(a + 1, a + 1 + top);
    n = unique(y + 1, y + 1 + top) - y - 1;
    for(int i = 1; i <= top; ++i){
        ans += f[1] * (a[i].x1 - a[i - 1].x1);
        int l = lower_bound(y + 1, y + 1 + n, a[i].y1) - y;
        int r = lower_bound(y + 1, y + 1 + n, a[i].y2) - y;
        modify(1, 1, n, l + 1, r, a[i].op);
    }
    printf("%lld", ans);
    return 0;
}

线段树的拓展5(树套树)

维护高维信息时,单纯的线段树显然无法胜任,这时我们就需要
简洁好写的 cdq 而不是使一样的
树套树。

树套树一般只套两层,套三层是真的没法写。对于两层的树来说,一般里层就是非常正常的树,它会正常地维护信息,然后外层的树有一点变化,因为外层的树不是维护一些信息而是一些树,所以为了方便我们就维护树根的信息。然后讲一讲修改。

其实也没啥好讲的,就是外层跑一遍,
只要是有关联的点都要修改!
需要修改的就去里层跑,然后递归往上更新就行。

查询就正常查询,找到区间后从直接返回答案变成查询里层。

有一道比板子简单一点的题放在主席树里面了,这里就直接放板子吧。然后这种东西讲多少遍都不如自己看别人写得好的代码然后自己想一遍。

板子

namespace SGT{
    int nd, rt[N << 9], ls[N << 9], rs[N << 9], c[N << 9];
    void gotols(){
        for(int i = 1; i <= c1; ++i)q1[i] = ls[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = ls[q2[i]];
    }
    void gotors(){
        for(int i = 1; i <= c1; ++i)q1[i] = rs[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = rs[q2[i]];
    }
    void upd(int &x, int l, int r, int p, int y){
        if(! x)x = ++nd; c[x] += y;
        if(l == r)return; int mid = l + r >> 1;
        if(p <= mid)upd(ls[x], l, mid, p, y);
        else upd(rs[x], mid + 1, r, p, y);
    }
    int qryk(int l, int r, int k){
        if(l == r)return l; int mid = l + r >> 1, res = 0;
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        if(res >= k)return gotols(), qryk(l, mid, k);
        return gotors(), qryk(mid + 1, r, k - res);
    }
    int qryrk(int l, int r, int k){
        if(l == r)return 0; int mid = l + r >> 1, res = 0;
        if(k <= mid)return gotols(), qryrk(l, mid, k);
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        return gotors(), res + qryrk(mid + 1, r, k);
    }
}
using namespace SGT;
namespace BIT{
    int lb(int x){return x & - x;}
    void get(int l, int r){
        c1 = c2 = 0;
        for(int i = l - 1; i; i -= lb(i))q1[++c1] = rt[i];
        for(int i = r; i; i -= lb(i))q2[++c2] = rt[i];
    }
    void mdf(int p, int y){
        int x = lower_bound(b + 1, b + 1 + tt, a[p]) - b;
        for(; p <= n; p += lb(p))upd(rt[p], 1, tt, x, y);
    }
    int kth(int l, int r, int k){get(l, r); return qryk(1, tt, k);}
    int rk(int l, int r, int k){int kk = lower_bound(b + 1, b + 1 + tt, k) - b; get(l, r); return qryrk(1, tt, kk) + 1;}
    int pre(int l, int r, int k){
        k = rk(l, r, k) - 1; return k > 0 ? b[kth(l, r, k)] : - INF;
    }
    int suf(int l, int r, int k){
        k = rk(l, r, k + 1); return k < r - l + 2 ? b[kth(l, r, k)] : INF;
    }
}

P4175 [CTSC2008] 网络管理

虽然这看上去很树套树+树剖但是其实可以将答案差分再合并一下扔掉一个
\(\log\)
,这样维护前缀主席树,查询时就查四棵树即可,最后就是
\(O(n\log^2n)\)
。但是我在看完题后根本没管太多,看到三只
\(\log\)
能过就写完树套树就水灵灵地过了((

void dfs1(int u, int f){
	dep[u] = dep[fa[u] = f] + 1, sz[u] = 1;
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to; if(v == f)continue;
		dfs1(v, u); sz[u] += sz[v];
		if(sz[son[u]] < sz[v])son[u] = v;
	}
}
void dfs2(int u, int tp){
	top[u] = tp; dfn[u] = ++tim, rk[tim] = u;
	if(son[u])dfs2(son[u], tp);
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to;
		if(v ^ fa[u] and v ^ son[u])dfs2(v, v);
	}
}

void gotols(){
	for(int i = 1; i <= c1; ++i)q1[i] = ls[q1[i]];
	for(int i = 1; i <= c2; ++i)q2[i] = ls[q2[i]];
}
void gotors(){
	for(int i = 1; i <= c1; ++i)q1[i] = rs[q1[i]];
	for(int i = 1; i <= c2; ++i)q2[i] = rs[q2[i]];
}
void upd(int &x, int l, int r, int pos, int y){
	if(! x)x = ++nd; s[x] += y;
	if(l == r)return; int mid = l + r >> 1;
	if(pos <= mid)upd(ls[x], l, mid, pos, y);
	else upd(rs[x], mid + 1, r, pos, y);
}
int getsum(){
	int sum = 0;
	for(int i = 1; i <= c1; ++i)sum -= s[q1[i]];
	for(int i = 1; i <= c2; ++i)sum += s[q2[i]];
	return sum;
}
int qryk(int l, int r, int k){
	if(l == r)return getsum() >= k ? l : 0; int mid = l + r >> 1, sum = 0;
	for(int i = 1; i <= c1; ++i)sum -= s[rs[q1[i]]];
	for(int i = 1; i <= c2; ++i)sum += s[rs[q2[i]]];
	if(k <= sum)return gotors(), qryk(mid + 1, r, k);
	return gotols(), qryk(l, mid, k - sum);
}

void get(int l, int r){
	for(int i = l - 1; i; i -= i & - i)q1[++c1] = rt[i];
	for(int i = r; i; i -= i & - i)q2[++c2] = rt[i];
}
void mdf(int pos, int y){
	int x = lower_bound(b + 1, b + 1 + m, a[rk[pos]]) - b;
	for(; pos <= n; pos += pos & - pos)upd(rt[pos], 1, m, x, y);
}
int kth(int u, int v,int k){
	c1 = c2 = 0;
	while(top[u] ^ top[v]){
		if(dep[top[u]] < dep[top[v]])swap(u, v);
		get(dfn[top[u]], dfn[u]);
		u = fa[top[u]];
	}
	if(dep[u] > dep[v])swap(u, v);
	get(dfn[u], dfn[v]); return qryk(1, m, k);
}

但是说实在的,实战中这玩意用的是真的少,一般时候还是 cdq 或者转化题意用复杂度更低的做法做。

线段树的一些高级玩法

这一部分主要是一些其他的算法、思想与线段树的运用,其实最主要的思想还是寻找题目性质,然后线段树优化维护信息的过程。

那就先来看一些线段树与树剖的东西。

树剖+线段树

做过序列上给一些颜色求颜色段数量的题吧?如果我把它扔到树上会怎么样呢?

P2486 [SDOI2011] 染色

结果是根本不会怎么样。直接树剖把树变成链做,套上线段树就完了。

void add(int u, int v){
    e[++cnt] = {hd[u], v}; hd[u] = cnt;
}
void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top; dfn[u] = ++tim, rk[tim] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}

struct node{
    int len, l, r;
};
#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    lc[x] = lc[ls], rc[x] = rc[rs];
    s[x] = s[ls] + s[rs] - (rc[ls] == lc[rs]);
}
void build(int x, int l, int r){
    if(l == r)return(void)(lc[x] = rc[x] = a[rk[l]], s[x] = 1);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pd(int x){
    if(! lz[x])return;
    lc[ls] = rc[ls] = lz[x];
    lc[rs] = rc[rs] = lz[x];
    s[ls] = s[rs] = 1;
    lz[ls] = lz[rs] = lz[x]; lz[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int c){
    if(L <= l and r <= R)return(void)(lc[x] = rc[x] = lz[x] = c, s[x] = 1);
    int mid = l + r >> 1; pd(x);
    if(L <= mid)mdfy(ls, l, mid, L, R, c);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, c);
    upd(x);
}
node qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return {s[x], lc[x], rc[x]};
    int mid = l + r >> 1; node res, t; pd(x);
    res = {0, 0, 0};
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid){
        t = qry(rs, mid + 1, r, L, R);
        if(! res.len)return t;
        res.len += t.len - (rc[ls] == lc[rs]);
        res.r = t.r;
    }
    return res;
}
void mpath(int u, int v, int c){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        mdfy(1, 1, n, dfn[tp[u]], dfn[u], c);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    mdfy(1, 1, n, dfn[u], dfn[v], c);
}
int qpath(int u, int v){
    int pr1 = 0, pr2 = 0, ans = 0;
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v), swap(pr1, pr2);
        node res = qry(1, 1, n, dfn[tp[u]], dfn[u]);
        ans += res.len - (pr1 == res.r);
        pr1 = res.l; u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v), swap(pr1, pr2);
    node res = qry(1, 1, n, dfn[u], dfn[v]);
    ans += res.len - (pr1 == res.l) - (pr2 == res.r);
    return ans;
}

这道是不是太简单了一点?那就加强一下!

P3979 遥远的国度

这下看懂了!

其实颜色段的处理不成问题,我们需要思考的是树根的变化应该如何高效处理。

这个时候我们就可以分类讨论一下树根的位置了。如果树根就是询问点那直接输出全局的答案;如果树根
不在当前询问点
\(u\)
的子树内

,是不是对
\(u\)
根本没有影响啊!最后就剩一种——在
\(u\)
的某棵子树内,这时候我们肯定要先找到是哪一棵子树,然后用全局减去这棵子树就行。

那么如何快速找子树呢?我们可以类比树剖求 LCA 的过程,然后就做完了。找子树和其他操作都是
\(O(n\log n)\)
的。

void add(int u, int v){
    e[++cnt] = {hd[u], v}; hd[u] = cnt;
}
void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top; dfn[u] = ++tim, rk[tim] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    mi[x] = min(mi[ls], mi[rs]);
}
void build(int x, int l, int r){
    if(l == r)return(void)(mi[x] = a[rk[l]]);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pd(int x){
    if(! lz[x])return;
    mi[ls] = mi[rs] = lz[x];
    lz[ls] = lz[rs] = lz[x]; lz[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int c){
    if(L <= l and r <= R)return(void)(mi[x] = lz[x] = c);
    int mid = l + r >> 1; pd(x);
    if(L <= mid)mdfy(ls, l, mid, L, R, c);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, c);
    upd(x);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return mi[x];
    int mid = l + r >> 1; ll res = INF; pd(x);
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid)res = min(res, qry(rs, mid + 1, r, L, R));
    return res;
}
void mpath(int u, int v, int c){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        mdfy(1, 1, n, dfn[tp[u]], dfn[u], c);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    mdfy(1, 1, n, dfn[u], dfn[v], c);
}
ll qtree(int u){
    if(u == root)return mi[1];
    if(dfn[root] < dfn[u] or dfn[root] >= dfn[u] + sz[u])return qry(1, 1, n, dfn[u], dfn[u] + sz[u] - 1);
    int v = root;
    while(tp[u] != tp[v]){
        v = tp[v];
        if(fa[v] == u)break;
        v = fa[v];
    }
    if(tp[u] == tp[v])v = rk[dfn[u] + 1];
    return min(qry(1, 1, n, 1, dfn[v] - 1), qry(1, 1, n, dfn[v] + sz[v], n));
}

好了也该来一道那啥一点的题了(

P2542 [AHOI2005] 航线规划

如果你会 LCT 而且代码能力强的话建议直接无脑做就完了。

考虑不会 LCT 的人会怎么做。如果用数据结构啥的维护删边可能不太现实,所以考虑倒着做就把删边变成了加边。然后就是如何维护两点之间桥的数量。我可以先把初始的桥给处理出来,跑一下 tarjan 然后图就成了一棵树,然后考虑加边操作意味着什么。如果现在需要将
\((u,v)\)
加入图中,那么树上
\(u,v\)
之间的
路径上的桥
就都没了,因为构成环。所以每次加边操作都可以看成区间赋值,然后树剖套一个果的线段树就做完了。

void addx(int u, int v){
    ex[++cntx] = {hdx[u], v}; hdx[u] = cntx;
}
void tarjan(int u, int fa){
    id[u] = low[u] = ++seq;
    st.push(u);
    for(int i = hdx[u]; i; i = ex[i].nxt){
        int v = ex[i].to;
        if(v == fa)continue;
        if(! id[v])tarjan(v, u);
        low[u] = min(low[u], low[v]);
    }
    if(low[u] == id[u]){
        ++ct;
        while(st.top() ^ u){
            col[st.top()] = ct;
            st.pop();
        }
        col[u] = ct; st.pop();
    }
}

void add(int u, int v){
    e[++cnt] = {hd[u], v}; hd[u] = cnt;
}
void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top; dfn[u] = ++tim, rk[tim] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    s[x] = s[ls] + s[rs];
}
void build(int x, int l, int r){
    if(l == r)return(void)(s[x] = 1);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pd(int x, int l, int r){
    if(! lz[x])return;
    int mid = l + r >> 1;
    s[ls] = s[rs] = 0;
    lz[ls] = lz[rs] = lz[x]; lz[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int c){
    if(L <= l and r <= R)return(void)(s[x] = c, lz[x] = 1);
    int mid = l + r >> 1; pd(x, l, r);
    if(L <= mid)mdfy(ls, l, mid, L, R, c);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, c);
    upd(x);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return s[x];
    int mid = l + r >> 1, res = 0; pd(x, l, r);
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid)res += qry(rs, mid + 1, r, L, R);
    return res;
}
void mpath(int u, int v, int c){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        mdfy(1, 1, n, dfn[tp[u]], dfn[u], c);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    mdfy(1, 1, n, dfn[u] + 1, dfn[v], c);
}
int qpath(int u, int v){
    int res = 0;
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        res += qry(1, 1, n, dfn[tp[u]], dfn[u]);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    return res + qry(1, 1, n, dfn[u] + 1, dfn[v]);
}

signed main(){
    n = rd(), m = rd();
    for(int i = 1; i <= m; ++i){
        E[i].u = rd(), E[i].v = rd();
        if(E[i].u > E[i].v)swap(E[i].u, E[i].v);
        ++mp[mkp(E[i].u, E[i].v)];
    }
    while(1){
        q[++tot][0] = rd();
        if(q[tot][0] == - 1)break;
        q[tot][1] = rd(), q[tot][2] = rd();
        if(q[tot][1] > q[tot][2])swap(q[tot][1], q[tot][2]);
        if(! q[tot][0])--mp[mkp(q[tot][1], q[tot][2])];
    }
    --tot;
    for(int i = 1; i <= m; ++i){
        int u = E[i].u, v = E[i].v;
        if(mp[mkp(u, v)])addx(u, v), addx(v, u);
    }
    tarjan(1, 0);
    for(int i = 1; i <= m; ++i){
        int u = E[i].u, v = E[i].v;
        if(mp[mkp(u, v)] and col[u] ^ col[v])add(col[u], col[v]), add(col[v], col[u]);
    }
    for(int i = 1; i <= tot; ++i)q[i][1] = col[q[i][1]], q[i][2] = col[q[i][2]];

    dfs1(1, 0); dfs2(1, 1); build(1, 1, n);
    for(int i = tot; i; --i){
        if(q[i][0])ans[++tt] = q[i][1] == q[i][2] ? 0 : qpath(q[i][1], q[i][2]);
        else mpath(q[i][1], q[i][2], 0);
    }
    for(int i = tt; i; --i)printf("%d\n", ans[i]);
    return 0;
}

P4211 [LNOI2014] LCA

对于这种题我们需要翻译题目信息。

像文中的两个点求 lca 深度的过程我们能不能用另一种视角去看待?我们可以看成一个点到根的路径加一,另一个点再查询其到一的路径的权值和。然后就好做了。具体的讲,我们可以把区间与点求 lca 的深度转换成两个前缀和相减,这不就直接离线以后扫描线扫一遍的事?

void dfs1(int u, int f){
	dep[u] = dep[f] + 1, sz[u] = 1;
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to;
		dfs1(v, u); sz[u] += sz[v];
		if(sz[son[u]] < sz[v])son[u] = v;
	}
}
void dfs2(int u, int tp){
	top[u] = tp, dfn[u] = ++tim, rk[tim] = u;
	if(son[u])dfs2(son[u], tp);
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to; if(v ^ son[u])dfs2(v, v);
	}
}

int s[N << 2], tg[N << 2];
#define ls x << 1
#define rs x << 1 | 1

int ad(int x, int y){
	return x += y, x >= p ? x - p : x;
}

void upd(int x){
	s[x] = ad(s[ls], s[rs]);
}
void pd(int x, int l, int r){
	if(! tg[x])return; int mid = l + r >> 1;
	tg[ls] = ad(tg[ls], tg[x]), tg[rs] = ad(tg[rs], tg[x]);
	s[ls] = ad(s[ls], tg[x] * (mid - l + 1) % p);
	s[rs] = ad(s[rs], tg[x] * (r - mid) % p);
	return(void)(tg[x] = 0);
}
void mdf(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return(void)(s[x] = ad(s[x], (r - l + 1) >= p ? r - l + 1 - p : r - l + 1), ++tg[x]);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf(ls, l, mid, ql, qr);
	if(mid < qr)mdf(rs, mid + 1, r, ql, qr);
	upd(x);
}
int qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return s[x];
	int mid = l + r >> 1, res = 0; pd(x, l, r);
	if(ql <= mid)res = qry(ls, l, mid, ql, qr);
	if(mid < qr)res = ad(res, qry(rs, mid + 1, r, ql, qr));
	return res;
}

void up(int u){
	while(top[u] ^ 1){
		mdf(1, 1, n, dfn[top[u]], dfn[u]);
		u = fa[top[u]];
	}
	mdf(1, 1, n, 1, dfn[u]);
}

int fd(int u){
	int sum = 0;
	while(top[u] ^ 1){
		sum = ad(sum, qry(1, 1, n, dfn[top[u]], dfn[u]));
		u = fa[top[u]];
	}
	return ad(sum, qry(1, 1, n, 1, dfn[u]));
}

struct qay{
	int x, id, o;
};
vector < qay > q[N];

int ans[N];

int main(){
	rd(n, Q);
	for(int i = 2; i <= n; ++i)rd(fa[i]), add(++fa[i], i);
	dfs1(1, 0), dfs2(1, 1);
	for(int i = 1, l, r, x; i <= Q; ++i){
		rd(l, r, x);
		q[++r].pb({++x, i, 1});
		q[l].pb({x, i, - 1});
	}
	for(int i = 1; i <= n; ++i){
		up(i);
		for(qay j : q[i])ans[j.id] = (ans[j.id] + p + j.o * fd(j.x)) % p;
	}
	for(int i = 1; i <= Q; ++i)wt(ans[i]), pc('\n');
	return 0;
}

P4069 [SDOI2016] 游戏

感觉找的题是不是太简单了

也许看到修改的式子中带了一个
\(dis\)
你会马上想到差分,但是仔细一想似乎不太对。因为它每次询问路径最小值,但是每次的操作相对独立,这样一看您觉得这是否更像线段覆盖的题。

想通了后这道题的做法就呼之欲出了,直接树剖套李超线段树板子做完。但是加线段的地方还要在 lca 处分讨一下。

void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; cnt[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to; if(v == f)continue; dis[v] = dis[u] + e[i].w;
        dfs1(v, u); if(cnt[son[u]] < cnt[v])son[u] = v;
        cnt[u] += cnt[v];
    }
}
void dfs2(int u, int top){
    tp[u] = top;
    dfn[u] = ++seq; rk[dfn[u]] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
int lca(int u, int v){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        u = fa[tp[u]];
    }
    return dep[u] < dep[v] ? u : v;
}

ll k[N], b[N], c[N << 3];
#define ls x << 1
#define rs x << 1 | 1
ll calc(int x, int pos){
    return k[x] * dis[rk[pos]] + b[x];
}
bool cmp(int x1, int x2, int pos){
    ll a = calc(x1, pos), b = calc(x2, pos);
    return a < b or (a == b and x1 < x2); 
}
void updd(int x, int l, int r){
    ll a = calc(sgt[x], l), b = calc(sgt[x], r);
    c[x] = min(c[x], min(a, b));
    c[x] = min(c[x], min(c[ls], c[rs]));
}
void _upd(int tmp, int x, int l, int r){
    int mid = l + r >> 1;
    if(cmp(tmp, sgt[x], mid))swap(tmp, sgt[x]);
    if(cmp(tmp, sgt[x], l))_upd(tmp, ls, l, mid);
    if(cmp(tmp, sgt[x], r))_upd(tmp, rs, mid + 1, r);
    updd(x, l, r);
}
void upd(int x, int l, int r, int L, int R, int tmp){
    if(L <= l and r <= R)return _upd(tmp, x, l, r);
    int mid = l + r >> 1;
    if(L <= mid)upd(ls, l, mid, L, R, tmp);
    if(R > mid)upd(rs, mid + 1, r, L, R, tmp);
    updd(x, l, r);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return c[x];
    int mid = l + r >> 1; ll res = min(calc(sgt[x], max(l, L)), calc(sgt[x], min(r, R)));
    if(L <= mid)res = min(res, qry(ls, l, mid, L, R));
    if(R > mid)res = min(res, qry(rs, mid + 1, r, L, R));
    return res;
}
void usum(int u, int v, int tmp){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        upd(1, 1, n, dfn[tp[u]], dfn[u], tmp);
        u = fa[tp[u]];
    }
    if(dfn[u] < dfn[v])swap(u, v);
    upd(1, 1, n, dfn[v], dfn[u], tmp);
}
ll qsum(int u, int v){
    ll res = INF;
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        res = min(res, qry(1, 1, n, dfn[tp[u]], dfn[u]));
        u = fa[tp[u]];
    }
    if(dfn[u] < dfn[v])swap(u, v);
    res = min(res, qry(1, 1, n, dfn[v], dfn[u]));
    return res;
}

signed main(){
    for(int i = 0; i < N << 3; ++i)c[i] = INF;
    n = rd(), m = rd(); int lcnt = 0;
    for(int i = 1; i < n; ++i){
        int u = rd(), v = rd(); ll w = rd();
        add(u, v, w); add(v, u, w);
    }
    dfs1(1, 0); dfs2(1, 1); b[0] = INF;
    for(int i = 1; i <= m; ++i){
        int op = rd(), u = rd(), v = rd();
        if(op ^ 1)printf("%lld\n", qsum(u, v));
        else{
            int t = lca(u, v);
            ll A = rd(), B = rd();
            k[++lcnt] = - A; b[lcnt] = A * dis[u] + B;
            usum(u, t, lcnt);
            k[++lcnt] = A; b[lcnt] = A * (dis[u] - 2 * dis[t]) + B;
            usum(t, v, lcnt);
        }
    }
    return 0;
}

线段树与 dp

dp 有关的技巧与知识在此不赘述,可以翻一下我写的 dp 的一些博客。这里是线段树专题,所以我想讲一点线段树的东西。

首先是何时用线段树的问题。如果 dp 式子中出现了需要葱区间转移的东西我们会先考虑记一下关键信息,转移时直接合并,当我们无法快速查找或合并时我们就会上线段树。

然后就是线段树需要维护什么?我们启用线段树的初衷是要维护一些不太好用数组记录的信息,或是转移或查询的时候不方便的东西。所以我们在维护线段树过程中就应该结合实际去记录,而不是把原来的信息原封不动扔进去。比如原来的信息差分后有良好的性质我就只需要把差分的东西丢进去,查询时在进行合并。

最后是选用什么线段树。当我们维护的信息只有可合并的特点时我们就上最普通的线段树,如果信息具有一定的可加可减性可以考虑可持久化。如果是高维信息就先看能不能 cdq,然后看树套树,最后考虑 kd-tree(虽然我不会这个,也就只能看看)。

P1442 铁球落地

因为是我很早以前做过的,还写了题解,所以就摘录的当时题解的原话。

因为铁球从高到低落下去,则每一个平台最短时间的贡献只会来源于它头上的一个(或几个)平台,所以是线性的,于是考虑dp。

考虑按照由低到高的顺序dp。令
\(dp[i][0/1]\)
表示从
\(x\)
轴到第
\(i\)
个平台的左/右端点最短用时。

首先可以推出第
\(i\)
个平台上任意一点
\((k,a_i.h)\)

\(x\)
轴最短用时为:
\(\min(k-a_i.l+dp[i][0],a_i.r-k+dp[i][1])\)

然后平台间转移:

\[dp[i][0]=\begin{cases}a_i.h&a_i.h<=H\\a_i.h-a_j.h+\min(a_i.l-a_j.l+dp[j][0],a_j.r-a_i.l+dp[j][1])&a_i.h-a_j.h<=H\end{cases}
\]

就可以用数据结构维护
\(j\)
了。具体的,每次更新
\(i\)
的答案后用当前平台编号覆盖之前的平台编号,但是数据太大需要离散化一下。维护用线段树和
set
均可。

所以说这道题就这么被我用
set
水过了。但是因为我非常负责所以还是放一个代码:

int n, m, x, y, f[N][2];
struct line{
    int h, l, r;
}a[N];
set < pair < int, int > > s;
bool cmp(line x, line y){
    return x.h < y.h;
}

void upd(int &ff, int nw, int h, int j){
    if(h - a[j].h > m)return;
    if(! j)return(void)(ff = h);
    ff = h - a[j].h + min(nw - a[j].l + f[j][0], a[j].r - nw + f[j][1]);
}

signed main(){
    n = rd(); m = rd(); x = rd(); y = rd();
    for(int i = 1; i <= n; ++i)a[i].h = rd(), a[i].l = rd(), a[i].r = rd();
    memset(f, INF, sizeof f); sort(a + 1, a + 1 + n, cmp);
    s.insert({INF, 0});
    for(int i = 1; i <= n; ++i){
        int lx = a[i].l, rx = a[i].r, hx =  a[i].h;
        auto lp = s.lower_bound({lx, 0}), rp = s.lower_bound({rx, 0});
        upd(f[i][0], lx, hx, lp -> second); upd(f[i][1], rx, hx, rp -> second);
        int tg = lp -> second;
        for(; lp -> first <= rx; lp = s.erase(lp)); s.insert({lx, tg}); s.insert({rx, i});
    }
    int res; upd(res, x, y, s.lower_bound({x, 0}) -> second);
    printf("%d", res);
    return 0;
}

看一道典题。

P4655 [CEOI2017] Building Bridges

这个 dp 似乎就很一眼。


\(f_i\)
表示从
\(1\)
走到
\(i\)
的最小代价,然后转移方程就是:

\[f_i=\min_{j<i}\{f_j+(h_i-h_j)^2+\sum_{k=j+1}^{i-1}w_k\}
\]


\(w\)
记一个前缀和,并把式子拆一拆,就变成了:

\[f_i=\min_{j<i}\{f_j+h_i^2-2h_ih_j+h_j^2+s_{i-1}-s_j\}
\]

然后把不变的提出来:

\[f_i=\min_{j<i}\{f_j-2h_ih_j+h_j^2-s_j\}+h_i^2+s_{i-1}
\]

现在的目标就是如何快速查询
\(\min\)
。如果你对于每一个
\(j\)
把所有与其有关的东西看成一个函数,再把
\(i\)
有关的
\(h_i\)
看成一个变量,我是不是就把
\(\min\)
里面的一坨抽象成了一个一次函数。我令
\(k=h_j,b=f_j+h_j^2-s_j\)
,然后这一坨就变成了
\(k\times h_i+b\)
,问题就变成在当前所有线段中查询位置
\(h_i\)
处的最小值,于是就李超线段树了。

int n;
ll h[N], w[N];
ll s[N], f[N];
namespace SGT{
	ll k[N], b[N];
	int sgt[M << 2];
	#define ls x << 1
	#define rs x << 1 | 1
	const int lim = M - 3;
	ll calc(int x, int pos){return k[x] * pos + b[x];}
	bool cmp(int x1, int x2, int pos){
		ll t1 = calc(x1, pos), t2 = calc(x2, pos);
		return t1 < t2;
	}
	void upd(int x, int l, int r, int id){
		int mid = l + r >> 1;
		if(cmp(id, sgt[x], mid))swap(sgt[x], id);
		if(cmp(id, sgt[x], l))upd(ls, l, mid, id);
		if(cmp(id, sgt[x], r))upd(rs, mid + 1, r, id);
	}
	void mdf(int x, int l, int r, int ql, int qr, int id){
		if(ql <= l and r <= qr)return(void)(upd(x, l, r, id));
		int mid = l + r >> 1;
		if(ql <= mid)mdf(ls, l, mid, ql, qr, id);
		if(mid < qr)mdf(rs, mid + 1, r, ql, qr, id);
	}
	int qry(int x, int l, int r, int pos){
		if(l == r)return sgt[x];
		int mid = l + r >> 1, res;
		res = pos <= mid ? qry(ls, l, mid, pos) : qry(rs, mid + 1, r, pos);
		return cmp(res, sgt[x], pos) ? res : sgt[x];
	}
}
using namespace SGT;

int main(){
	rd(n); for(int i = 1; i <= n; ++i)rd(h[i]); b[0] = inf;
	for(int i = 1; i <= n; ++i)rd(w[i]), s[i] = s[i - 1] + w[i];
	for(int i = 1; i <= n; ++i){
		int j = qry(1, 0, lim, h[i]);
		if(i ^ 1)f[i] = f[j] + h[j] * h[j] - s[j] - 2 * h[i] * h[j] + h[i] * h[i] + s[i - 1];
		k[i] = - 2 * h[i]; b[i] = f[i] + h[i] * h[i] - s[i];
		mdf(1, 0, lim, 0, lim, i);
	}
	wt(f[n]);
	return 0;
}

Problem - 1476F - Codeforces

考虑令
\(f_i\)
表示前
\(i\)
盏灯能够照亮的最大前缀。考虑转移。

对于当前的灯有两种情况,一种是前面的灯照不到它,这时就不用管直接跳过,还有就是照的到就更新当前最大值。

但实际上还有一种就是当前的朝左,然后能被它照到的就全向右,这时
\(f_i=\max_{i-p_i\le j<i}\{j+p_j\}\)

然后其实用 st 表就可以维护了,但是这道题有一定的价值所以我还是放到这里了。

最后输出方案就倒推一遍即可。

void init(int nn){
	f[0] = f[1] = 0;
	for(int i = 1; i <= nn; ++i)op[i] = 0;
}
void make_st(){
	for(int j = 1; j <= lg[n]; ++j)
		for(int i = 1; i + (1 << j) - 1 <= n; ++i)st[i][j] = max(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
}
int query(int l, int r){
	if(l > r)return 0;
	int j = lg[r - l + 1];
	return max(st[l][j], st[r - (1 << j) + 1][j]);
}

int find(int l, int r, int x){
	int id = - 1;
	while(l <= r){
		int mid = l + r >> 1;
		if(f[mid] >= x)id = mid, r = --mid;
		else l = ++mid;
	}
	return id;
}

void solve(int x){
	if(! x)return;
	if(f[x] == f[x - 1])return (void)solve(x - 1);
	if(f[x] == st[x][0] and f[x - 1] >= x)return (void)(op[x] = 1, solve(x - 1));
	int y = find(0, x - 1, x - p[x] - 1);
	for(int i = y + 1; i < x; ++i)op[i] = 1;
	solve(y);
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int T; cin >> T;
	for(int i = 2; i < N; ++i)lg[i] = lg[i >> 1] + 1;
	while(T--){
		init(n);
		cin >> n;
		for(int i = 1; i <= n; ++i)cin >> p[i], st[i][0] = i + p[i];
		make_st();
		for(int i = 1; i <= n; ++i){
			f[i] = f[i - 1];
			if(f[i] >= i)f[i] = max(f[i], st[i][0]);
			int id = find(0, i - 1, i - p[i] - 1);
			if(id != - 1)f[i] = max(f[i], max(i - 1, query(id + 1, i - 1)));
		}
//		for(int i = 1; i <= n; ++i)cout << "f[" << i << "] = " << f[i] << ' '; cout << '\n';
		if(f[n] < n){
			cout << "NO" << '\n';
			continue;
		}
		cout << "YES" << '\n';
		solve(n);
		for(int i = 1; i <= n; ++i)cout << (op[i] ? 'R' : 'L');
		cout << '\n';
	}
	return 0;
}

线段树优化建图

顾名思义,就是在建图过程中可能会出现点向区间连边或区间向点连边的情况,这时就需要线段树优化建图,只是图上操作时复杂度多一只
\(\log\)

Problem - 786B - Codeforces

这道题就是出现了上述情况,在此重点讲建图的方法。首先我们需要满足区间能够与点联系起来,这就需要线段树的节点与点要相连。我们可以效仿线段树维护信息的方式,将树上的节点一层一层连起来,最后叶子节点其实就代表真正的点。

其次是我们多连的这些虚边不能影响答案。这时边权的设计就需要仔细考虑。比如此题是求最短路,所以我把边权设为零,那如果是跑网络流,我们的边权(容量)就应该设成正无穷。

下面代码只展示连边方法,相信能够看到这里的读者都会最短路。

void build(int x, int l, int r){
    t[x].l = l, t[x].r = r;
    if(l == r){
        int n1 = l + (n << 3), n2 = x + (n << 2);
        add(n1, x, 0); add(x, n1, 0);
        add(n1, n2, 0); add(n2, n1, 0);
        return;
    }
    int mid = l + r >> 1, t = n << 2;
    add(x, ls, 0); add(x, rs, 0);
    add(ls + t, x + t, 0); add(rs + t, x + t, 0);
    build(ls, l, mid); build(rs, mid + 1, r);
}
void modify(int x, int L, int R, int u, ll w, bool op){
    int l = t[x].l, r = t[x].r, mid = l + r >> 1;
    int t1 = n << 2, t2 = n << 3;
    if(L == l and r == R){
        if(op)return (void)(add(x + t1, u + t2, w));
        else return (void)(add(u + t2, x, w));
    }
    if(R <= mid)modify(ls, L, R, u, w, op);
    else if(L > mid)modify(rs, L, R, u, w, op);
    else{
        modify(ls, L, mid, u, w, op);
        modify(rs, mid + 1, R, u, w, op);
    }
}
signed main(){
    //freopen(,stdin);
    //freopen(,stdout);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> q >> s;
    build(1, 1, n);
    for(int i = 1, op, u, v, l, r; i <= q; ++i){
        cin >> op >> u;
        ll w;
        if(op == 1){
            cin >> v >> w;
            add(u + (n << 3), v + (n << 3), w);
        }
        else{
            cin >> l >> r >> w;
            modify(1, l, r, u, w, op - 2);
        }
    }
    s += (n << 3);
    dijkstra();
    for(int i = 1, t = n << 3; i <= n; ++i){
        if(d[i + t] < 2e18)cout << d[i + t] << ' ';
        else cout << - 1 << ' ';
    }
    return 0;
}

P6348 [PA2011] Journeys

问题升级!如果是区间向区间连边呢?如果把区间抓出来对应连是
\(O(\log^2)\)
的,然后 bfs 也是
\(O(\log^2)\)
的所以就会被卡。这时我们可以继续建虚点,将区间与虚点连边即可。

void build(int x, int l, int r){
    t[x].l = l, t[x].r = r;
    if(l == r)return (void)(id[l] = x);
    int mid = l + r >> 1, t = n << 2;
    add(x, ls, 0); add(x, rs, 0);
    add(ls + t, x + t, 0); add(rs + t, x + t, 0);
    build(ls, l, mid); build(rs, mid + 1, r);
}
void modify(int x, int L, int R, int u, bool op){
    int l = t[x].l, r = t[x].r, mid = l + r >> 1;
    if(l == L and r == R){
        if(op)return (void)(add(x + (n << 2), u, 0));
        return (void)(add(u, x, 0));
    }
    if(R <= mid)modify(ls, L, R, u, op);
    else if(L > mid)modify(rs, L, R, u, op);
    else{
        modify(ls, L, mid, u, op);
        modify(rs, mid + 1, R, u, op);
    }
}

signed main(){
    //freopen(,stdin);
    //freopen(,stdout);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m >> s;
    build(1, 1, n);
    cnt = n << 3;
    for(int i = 1, l1, l2, r1, r2; i <= m; ++i){
        int u = ++cnt, v = ++cnt;
        cin >> l1 >> r1 >> l2 >> r2;
        add(v, u, 1); modify(1, l2, r2, u, 0); modify(1, l1, r1, v, 1);
        u = ++cnt, v = ++cnt;
        add(v, u, 1); modify(1, l1, r1, u, 0); modify(1, l2, r2, v, 1);
    }
    for(int i = 1, t = n << 2; i <= n; ++i){
        add(id[i], id[i] + t, 0);
        add(id[i] + t, id[i], 0);
    }
    s = id[s] + (n << 2);
    bfs();
    for(int i = 1; i <= n; ++i)cout << d[id[i]] << '\n';
    return 0;
}

然后
车站分级
大家应该都做过吧?如果把它的数据范围改到
\(n=10^5\)
又该怎么做?

考虑线段树优化建图。每一趟车相当于一些区间向一些点连边,就像上面的题一样,每次新建一个虚点,区间连向虚点、虚点连向单点。只不过因为要跑 toposort,所以线段树上的边需要改成单向边,这时注意方向一定是从叶子连向根。代码懒得找就不放了。

线段树的更难的应用(杂题)

P10856 【MX-X2-T5】「Cfz Round 4」Xor-Forces

一道非常不错的题!

先找题目性质,然后发现操作一可以合并,所以只用考虑如何快速做完操作一并得到操作二的答案。先考虑如何在动态序列上维护颜色段,我们可以维护满足
\(col_{i-1}=col_{i}=col_{i+1}\)
的数量再用总数减去就是答案。对于区间的端点我们暴力判断就行因为不会超过
\(O(\log n)\)
。那么操作一又如何处理呢?

考虑每次操作后有的区间会交换但是有的却
保持相对静止
,然后建议随便写几个数用纸和笔手摸一下操作。

你会发现我其实是把
\([0,1]\)
这个区间平移到了
\([6,7]\)
,其他可以类比。试想对于线段树上一段长度为
\(len\)
的区间,我做操作一后出现的情况只会有
\(len\)
种。设异或上
\(sum\)
,若
\(sum>len\)
其实就可以看成
\(sum\bmod len\)
,因为把这些东西都放在二进制下你会发现他多出来的那些数位上的一都没有用,而
\(sum\)
取零到
\(len-1\)
时序列的变换方式又互不相同,所以有
\(len\)
种情况。然后我其实可以直接把这些情况记下来。时间复杂度是
\(\sum_{i=0}^{2^i\le len}\frac{len}{2^i}=2\times len\)
的,所以建树总复杂度还是
\(O(n\log n)\)
的。

然后其实就直接在线段树上走就行了,对于查询到的区间,若当前操作的值小于等于就可以直接把答案取出来,否则就需要
平移
到同一层的某个点上,调用它的答案。对于平移操作我们又如何处理呢?

我们可以记录一个数组
\(jump_{l,i}\)
代表序列上左端点是
\(l\)
且线段树节点大小为
\(2^i\)
的点是谁,然后跳的时候相当于二进制下
数位小于等于当前走到的点
就直接调用平移后的答案,大于等于的部分就异或上区间左端点直接跳,最后中点的答案就暴力合并即可。

(如果有点晕建议结合上面的图画一下)

代码:

void bld(int x, int l, int r){
	int len = r - l, mid = l + r >> 1;
	jump[l][lg[len]] = x; f[x].rsz(len);
	if(l + 1 == r)return;
	bld(ls, l, mid), bld(rs, mid, r);
	for(int i = 0; i < len; ++i)
		if(i < (len >> 1))f[x][i] = f[ls][i] + f[rs][i] + (a[mid - 1 ^ i] == a[mid ^ i]);
		else f[x][i] = f[ls][i - (len >> 1)] + f[rs][i - (len >> 1)] + (a[mid - 1 ^ i] == a[mid ^ i]);
}
int qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r - 1 <= qr)return f[jump[l ^ (opt >> lg[r - l] << lg[r - l])][lg[r - l]]][opt % (1 << lg[r - l])];
	int mid = l + r >> 1, ansl(0), ansr(0); bool fl(false), fr(false);
	if(ql < mid)fl = true, ansl = qry(ls, l, mid, ql, qr);
	if(mid <= qr)fr = true, ansr = qry(rs, mid, r, ql, qr);
	return fl ? (fr ? (ansl + ansr + (a[mid ^ opt] == a[mid - 1 ^ opt])) : ansl) : ansr;
}

P2757 [国家集训队] 等差子序列

此题性质就是考虑只要满足有三个数满足条件就行。然后中间的这个数是最特殊的!因为输入的数构成排列,假设我对当前点左边的点全染黑色(1),右边全染白色(0),如果
\(\exists t,col_{x+t}=1,col_{x-t}=0\)
那么就说明找到了。

然后我们就考虑如何快速查找与更新。如果像上面一样给位置染色,我如何快速判断呢?我可以在值域上染色,把这个 01 序列看成一个高位数,实际操作时我可以对于黑点和白点各维护一个高位数,然后就去比较他们的某几位是否是回文数。细想,这不就是哈希判断回文串吗?于是两棵线段树维护正/反哈希就行了。

但是因为以前数据过水导致我用
bitset
艹过了,现在也懒得写代码于是就咕了。

P4197 Peaks

观察到限制是
\(\le x\)
所以我们直接离线下来按限制从小到大排序然后线段树合并暴力做。

P7834 [ONTAK2010] Peaks 加强版
考虑加强

现在你不可以离线。又应该这么做呢?

其实做法还是基于此题的限制
\(\le x\)
。因为有这个关系,所以我们可以建 kruskal 重构树。这样如果两个点能够互相抵达当且仅当它们的 lca 小于等于 x,于是我们就从当前点一直往上跳,直到不能再走到其他点。这一过程可以倍增实现,并且倍增同时能够维护出最大值。然后跳到最高点后我们就用主席树查询即可。

struct Edge{
    int u, v; ll w;
    friend bool operator < (Edge a, Edge b){
        return a.w < b.w;
    }
}E[M];
struct edge{
    int nxt, to;
}e[M << 1];
void add(int u, int v){
    e[++cnt] = {hd[u], v}, hd[u] = cnt; ++in[v];
}
int fd(int x){
    return x == ff[x] ? x : ff[x] = fd(ff[x]);
}
void dfs(int u, int fa){
    lq[dfn[++tim] = u] = tim, f[u][0] = fa;
    for(int i = 1; (1 << i) <= n; ++i)f[u][i] = f[f[u][i - 1]][i - 1];
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to; if(v == fa)continue;
        dfs(v, u), sz[u] += sz[v];
    }
    if(! sz[u])sz[u] = 1; rq[u] = tim;
}

int rt[N], ls[N << 5], rs[N << 5], t[N << 5];
void upd(int &x, int y, int l, ll r, int pos, int val){
    if(! x)x = ++tot; t[x] = t[y] + val;
    if(l == r)return; ll mid = l + r >> 1;
    if(pos <= mid)rs[x] = rs[y], upd(ls[x], ls[y], l, mid, pos, val);
    else ls[x] = ls[y], upd(rs[x], rs[y], mid + 1, r, pos, val);
}
int kth(int x, int y, int l, ll r, int k){
    if(l == r)return l; ll mid = l + r >> 1, s = t[rs[y]] - t[rs[x]];
    if(k > s)return kth(ls[x], ls[y], l, mid, k - s);
    else return kth(rs[x], rs[y], mid + 1, r, k);
}
int qry(int u, int lim, int k){
    for(int i = 23; ~ i; --i)if(f[u][i] and c[f[u][i]] <= lim)u = f[u][i];
    if(sz[u] < k)return - 1;
    return kth(rt[lq[u] - 1], rt[rq[u]], 0, inf, k);
}

signed main(){
    // fileio(fil);
    int mod;
    mod = n = rd(), m = rd(), q = rd();
    for(int i = 1; i <= n; ++i)h[i] = rd();
    for(int i = 1; i <= (n << 1); ++i)ff[i] = i;
    for(int i = 1; i <= m; ++i){
        int u = rd(), v = rd(); ll w = rd();
        E[i] = {u, v, w};
    }
    sort(E + 1, E + 1 + m);
    for(int i = 1; i <= m; ++i){
        int u = fd(E[i].u), v = fd(E[i].v); ll w = E[i].w;
        if(u == v)continue;
        ff[u] = ff[v] = ++n; c[n] = w;
        add(n, u), add(n, v);
    }
    for(int i = 1; i <= n; ++i)if(! in[i])dfs(i, 0);
    for(int i = 1; i <= n; ++i){
        if(dfn[i] <= mod)upd(rt[i], rt[i - 1], 0, inf, h[dfn[i]], 1);
        else rt[i] = rt[i - 1];
    }
    ll lastans = 0;
    for(int i = 1; i <= q; ++i){
        ll u = (rd() ^ lastans) % mod + 1, lim = rd() ^ lastans, k = (rd() ^ lastans) % mod + 1;
        printf("%lld\n", lastans = qry(u, lim, k)); lastans = max(0ll, lastans);
    }
    return 0;
}

P4898 [IOI2018] seats 排座位

评价是:真的神仙题!

我们需要思考的是如何翻译题目。我们假设已经被安排的座位是一(黑点),否则是零(白点)。正常想法是类比一维的时候,我们记录区间内一的个数,然后看是否等于区间大小。这可以线段树维护。但是考虑到调换座位的操作会对一段区间造成影响,如果我每个都要
\(O(1)\)
修改肯定就炸了,我们就需要一种
可以合并的信息
去代替现在的。

区间操作我们可以想到区间加,那么我们现在能不能记录一些可加的信息来描述合法情况?

我们来考虑每一个点的情况,假设当前状态已经合法,现在是一个矩形然后四周可能有一圈白点。假设这个点是黑点,想想它满足什么性质时矩形合法而什么时候矩形不合法。本题最妙的地方来了!我们可以关注它周围点的状态来确定是否合法。具体的,如果这个点左边的点和上面的点有黑点,那么他就只能是矩形的一部分,否则它就必定为矩形的左上角,而左上角显然只有一个。也就是说我们要维护黑点
左边和上面都是白点的黑点
的数量,当数量为一时满足条件。

然后考虑白点周围状态。如果它周围有至少两个黑点那么当前状态就不合法。所以我们可以再记录一下
周围有至少两个黑点的白点
的数量,当数量为零时满足条件。

然后聪明如你,你会发现这两个状态需要同时满足,所以可以把它们放在一起维护。最后就是支持区间加,查询全局最小值数量。修改操作其实就是暴力交换后周围的一些点可能会有影响,把这些点重新算一遍就好了。

const int dir[4][2] = {1, 0, - 1, 0, 0, 1, 0, - 1};

int h, w, q;
vector < vector < int > > g;
int tt[N];

struct node{
	int x, y, tim;
}a[N];

int res[N << 2], cnt[N << 2], tg[N << 2];

#define ls x << 1
#define rs x << 1 | 1

void upd(int x){
	res[x] = min(res[ls], res[rs]);
	cnt[x] = cnt[ls] * (res[x] == res[ls]) + cnt[rs] * (res[x] == res[rs]);
}
void init(int x, int l, int r){
	if(l == r)return(void)(cnt[x] = 1);
	int mid = l + r >> 1;
	init(ls, l, mid), init(rs, mid + 1, r);
	upd(x);
}
void pd(int x){
	if(! tg[x])return;
	tg[ls] += tg[x], res[ls] += tg[x];
	tg[rs] += tg[x], res[rs] += tg[x];
	return(void)(tg[x] = 0);
}
void mdf(int x, int l, int r, int ql, int qr, int y){
	if(ql <= l and r <= qr)return(void)(res[x] += y, tg[x] += y);
	int mid = l + r >> 1; pd(x);
	if(ql <= mid)mdf(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf(rs, mid + 1, r, ql, qr, y);
	upd(x);
}

int get(int x, int y){
	if(x < 0 or y < 0 or x >= h or y >= w)return inf;
	return a[g[x][y]].tim;
}
int loc(int x, int y){
	if(x < 0 or y < 0 or x >= h or y >= w)return - 1;
	return g[x][y];
}

void option(int i, int op){
	vector < int > lp; int t1, t2;
	lp.pb(get(a[i].x + 1, a[i].y));
	lp.pb(get(a[i].x, a[i].y + 1));
	lp.pb(t1 = get(a[i].x - 1, a[i].y));
	lp.pb(t2 = get(a[i].x, a[i].y - 1));
	sort(lp.bg(), lp.ed()); int lpos = lp[1];
	if(a[i].tim > lpos)mdf(1, 1, h * w, lpos, a[i].tim - 1, op);
	t1 = min(t1, t2); if(t1 > a[i].tim)mdf(1, 1, h * w, a[i].tim, t1 - 1, op);
}

int main(){
	rd(h, w, q); g.rsz(h); for(int i = 0; i < h; ++i)g[i].rsz(w);
	for(int i = 1; i <= h * w; ++i){
		rd(a[i].x, a[i].y);
		g[a[i].x][a[i].y] = a[i].tim = i;
		tt[i] = i;
	}
	init(1, 1, h * w);
	for(int i = 1; i <= h * w; ++i)option(i, 1);
	for(int i = 1, u, v; i <= q; ++i){
		rd(u, v); ++u, ++v;
		int used[12], cc;
		used[1] = u = tt[u], used[cc = 2] = v = tt[v];
		int xx = a[u].x, yy = a[u].y;
		for(int j = 0; j < 4; ++j){
			int tmp = loc(xx + dir[j][0], yy + dir[j][1]);
			if(~ tmp)used[++cc] = tmp;
		}
		xx = a[v].x, yy = a[v].y;
		for(int j = 0; j < 4; ++j){
			int tmp = loc(xx + dir[j][0], yy + dir[j][1]);
			if(~ tmp)used[++cc] = tmp;
		}
		sort(used + 1, used + 1 + cc); cc = unique(used + 1, used + 1 + cc) - used - 1;
		for(int j = 1; j <= cc; ++j)option(used[j], - 1);
		swap(a[u].tim, a[v].tim);
		swap(tt[a[u].tim], tt[a[v].tim]);
		for(int j = 1; j <= cc; ++j)option(used[j], 1);
		wt(cnt[1]); pc('\n');
	}
	return 0;
}

后记

从起稿开始(2024.11.6 16:00)到竣工(2024.11.8.15.26)一共花了我两天时间。其间我写了十道题左右,并重新梳理了暑假讲的和国庆拔高的
几乎
所有题目,当然一些太逆天的题我还是没能写完。从明天开始我就准备复习 dp 部分的内容,但可能很难再像这次写下这洋洋几千上万字了。今天下午,随着半期考试的落幕,没有停课的同学们也会陆续归队,机房不再冷清。但这样安静投入的去做一件事情是真的很好!每天回宿舍都很疲惫,晚上冼个澡,躺下就睡,早上总是 6:30 准时醒,生物钟就这样形成。

不知又要到何时,我内心才能如此平静?或许,改变自己的心态,既是一种挑战,也是一份礼物。当你成功克服自我后,也许会收获意想不到的惊喜!

但无论如何,ds 方面的内容算是弄完了。cdq 和整体二分我会在今天下午后半程复习完毕,下一次很系统的复习应该就要到 noip 后了,希望不要给自己留遗憾,也希望大家都能有光明的前途!

当今的应用市场中,传统的应用产品已经难以完全满足消费者的多样化需求。为了在竞争激烈的市场中脱颖而出,企业需要深入洞察用户需求,提供个性化的服务体验和差异化的产品创新,以吸引并留住消费者。

比如,购物类App通过虚实结合的方式实现虚拟穿搭;室内设计行业App可将虚拟家具模型放置于现实环境中给用户演示;游戏行业App可将虚拟游戏画面放置于现实环境中,从而提供给用户更沉浸式的游戏体验。

以上创新化的应用场景,都依赖于一项核心的技术能力,即增强现实(AR)技术,HarmonyOS SDK
AR引擎服务
(AR Engine)正好具备这样的能力。
AR Engine是一个用于在HarmonyOS上构建增强现实应用的引擎,基于计算机实时计算和多传感器的结合,可以通过设备摄像头将虚拟物体逼真地叠加到现实场景中,让应用实现虚拟世界与现实世界的融合,为用户提供全新的视觉体验和交互方式。

AR Engine提供了运动跟踪、环境跟踪和命中检测等空间计算能力,开发者可以借助这些能力实现如AR导航、AR测量和AR摆放等应用,为用户提供虚实融合的全新交互体验。

运动跟踪能力

运动跟踪能力统一了虚拟坐标系和现实坐标系,是实现虚拟物体与现实环境逼真融合的基础能力。

AR Engine使用运动跟踪能力通过终端设备摄像头标识特征点,并跟踪其移动变化,同时将这些点的移动变化与终端设备惯性传感器结合,来不断跟踪终端设备的位置和姿态。通过持续跟踪设备相对于周围环境的位置和姿态变化轨迹,建立了虚拟数字世界和现实物理世界统一的几何空间坐标系,从而实现设备在不同视角和距离情况下,虚拟物体保持与真实环境几何的一致性,开发者可从观察者视角将渲染的虚拟物体叠加到摄像头图像中,实现虚实融合。

比如AR Engine应用在AR车展上时,用户可通过转动设备摄像头从不同角度来查看虚拟汽车模型,实现沉浸式的AR观展,这里就用到了运动跟踪的能力。

环境跟踪

AR Engine环境跟踪能力可以快速识别出现实场景中的平面信息,并识别出对应的平面类型和形状等信息,从而辅助应用实现数字虚拟物体以场景化的方式融入现实物理世界中。环境跟踪主要包括平面检测、平面语义、目标语义三个能力。

平面检测

平面检测能力可通过识别水平和垂直平面上的成群特征点以及平面的边界,得到现实世界中的平面信息,例如地面或墙面。检测到这些平面后,应用可基于平面来放置虚拟物体,或进行导航等应用场景的开发。

平面语义

平面语义是指检测现实世界平面的类型,当前支持的平面类型共8种,分别为:墙面、地面、座椅面、桌面、天花板、门面、窗面、床面。

目标语义

目标语义是指当目标物体位于平面上时,检测目标物体的形状,当前包括矩形和圆形。

命中检测

命中检测技术可以帮助AR Engine实现虚实世界两点之间的连接。AR Engine通过命中检测技术,获取兴趣点到AR世界兴趣点的映射关系,开发者可以基于此能力帮助用户实现与AR世界中虚拟物体的交互。

image

命中检测能力示意图

在实际应用场景中,用户可点击终端设备屏幕,选中现实环境中的兴趣点,AR应用调用碰撞检测接口获取AR世界的兴趣点并返回相关信息,用户即可在终端设备屏幕所展示的AR世界兴趣点上完成模型放置等交互动作,比如用户可以通过摄像头将虚拟家具放置于现实室内环境中,并从各个角度观察判断家具是否符合整体设计,从而减少大件商品的购买出错率。

运动跟踪能力统一了现实世界和虚拟世界的坐标系,环境跟踪能力识别出了现实世界的平面信息,而命中检测能力建立了两个世界之间的交互锚点,AR Engine通过这三个能力,从点、线、面多个维度打破了物理世界与数字世界的壁垒,实现了两者的叠加统一,这种统一不仅为用户提供了前所未有的体验,也为开发者开辟了广阔的创新空间,为各行各业带来了更多的可能性。随着技术的不断演进和市场的不断扩大,我们可以期待AR技术在未来将有更广泛的应用和更深远的影响。

了解更多详情>>

访问
AR Engine联盟官网

获取
AR Engine开发指导文档

一、起因
最近在开发Lattice的一款低功耗FPGA时,遇到了reg初始化复位问题,经过在网上搜寻相关资料整理如下;
二、FPGA中reg的初始化方式

  1. 在定义时初始化,例如:
    reg r_test = 1'b1;
  2. 在复位语句中,对reg进行赋值,例如:
reg r_test;
always@(posedge sys_clk) begin
   if(~sys_rst_n) begin
       r_test <= 'b0;
   end
   else begin
       //logic
   end
end
  1. initial语句初始化
    很多人认为initial语句只可以用于仿真中,不可以被综合,实际上initial语句是可以被综合的(至少在vivado中可以),比如:
reg r_test;
initial begin
   r_test = 'b0;
end

三、问题
以上三种方式在Xilinx FPGA中都可以成功的对reg初始化,但是在Lattice的ICE5LP4K中,赋值方式1似乎不同,例如以下代码:

     reg             [15:0]              r_ADC_READ_NUM          = 16'd64        ;
    always @ (posedge SYS_CLK ) begin
        if( r_CMD_VALID)begin
            r_TX_BUF_RCNT	<= r_ADC_READ_NUM;
        end else if(r_TX_BUF_REN)begin
            r_TX_BUF_RCNT	<= r_TX_BUF_RCNT - 1'b1 ;
        end
        //  r_TX_BUF_RCNT_DONE	<= r_TX_BUF_RCNT == 10'd0? 1'b1 : 1'b0 ;

        r_TX_BUF_PRE	<= {r_TX_BUF_PRE[14:0] , r_CMD_VALID };
        r_TX_BUF_REN	<= r_QSPI_TN & (r_TX_BUF_PRE[13] | r_QSPI_OFF_SEL==4'd0 & s_QSPI_CLK_UP ) & r_TX_BUF_RCNT>0 ;// MCU READ 128 Byte once
    end

虽然r_ADC_READ_NUM寄存器已经通过方式1进行了初始化,但是在上板测试中r_TX_BUF_REN一直不能被拉高,最后让我不得不怀疑上了r_TX_BUF_RCNT的数值,该寄存器在r_CMD_VALID时被赋值为r_ADC_READ_NUM,结果发现r_TX_BUF_RCNT一直为0,说明r_ADC_READ_NUM 并没有被初始化,对其进行复位初始化后,系统逻辑正常运行:

always @(posedge SYS_CLK or negedge sys_nRst) begin
    if(~sys_nRst) begin
        r_ADC_READ_NUM <= 16'd64; // initial value: default 64
    end
    else begin
    end
end

四、总结
对于xilinx FPGA中,赋值方式1可以正常工作,在lattice中不 work,因此,为了避免这种情况,建议养成复位赋值的方式对reg初始化,以确保FPGA上电后reg中的数据为一个我们设定的初值。

XAML平台和跨平台开发策略

本文基于Vicky&James 2024年10月22日在韩国Microsoft总部BMW meetup会议上的演讲内容重新整理而成。这次研讨会我们深入探讨了基于XAML的各种平台、跨平台战略以及为有效的项目架构设计所需的核心技术。

介绍

大家好,我们是中韩Microsoft MVP夫妇Vicky&James。我们从WPF开始就对包括Uno Platform在内的基于XAML的框架和项目架构设计有着深入的兴趣和经验。大家可以在我们的GitHub仓库中查看和下载各种项目的源代码:
GitHub - jamesnetgroup


目录

  1. XAML平台和跨平台概述
  2. 考虑跨平台的.NET版本选择策略
  3. View和ViewModel的连接策略分析
  4. 框架设计的必要功能及实现方案
  5. 在其他平台上有效使用WPF技术的核心策略
  6. 分布式项目管理的Bootstrapper设计方法论
  7. 在桌面跨平台中最大化利用WPF技术的策略


1. XAML平台和跨平台概述

XAML是一种用于以声明方式定义UI的标记语言,被用于多个平台。XAML具有由对象(即类)组成的层次结构,使开发人员能够以面向对象的方式设计和管理UI。由于这种结构,开发人员直接处理XAML是很自然的。

当WPF首次出现时,它强调了开发人员和设计师之间的协作,但实际上XAML领域通常也由开发人员负责。这是因为XAML不仅仅是简单的设计,而是形成了基于对象的层次结构,在复杂的自定义控件实现中也发挥着重要作用。这种面向开发人员的设计方法促使XAML不仅在WPF中,而且在随后出现的许多平台中都成为核心组件。

特别是,WPF对所有基于XAML的平台都产生了重大影响,并成为这些平台最重要的参考。

1.1 主要的XAML框架

  • WPF
    : 用于Windows桌面应用程序开发的强大框架,提供丰富的UI和图形功能。
  • Silverlight
    : 用于在web浏览器中运行的互联网应用程序的平台,目前已停止支持。它是WPF的轻量级版本,以插件方式运行。由于Web标准政策的变化,基于插件的Web平台消失,在Silverlight 2中引入了
    VisualStateManager(VSM)
    来弥补Trigger的缺点。
  • Xamarin.Forms
    : 支持iOS、Android和Windows的移动应用开发平台。作为基于Mono的第一个基于XAML的跨平台,被Microsoft战略性收购并成为.NET Core的基础。
  • UWP (Universal Windows Platform)
    : 用于开发运行在Windows 10及以上版本的应用程序的平台。需要注册Microsoft Store,并有WinAPI使用限制等约束。支持与WPF相同的自定义控件设计。
  • WinUI 3
    : Windows原生UI框架,是最新Windows应用开发的下一代UI平台。继承了UWP的所有优点,同时解决了其限制,并采纳了WPF的可扩展性。
  • MAUI (.NET Multi-platform App UI)
    : 从.NET 6开始引入的跨平台UI框架,可以在单一项目中开发移动和桌面应用。
  • Uno Platform
    : 允许在各种平台上使用UWP和WinUI的API的框架,支持Web(WebAssembly)、移动和桌面。支持几乎所有平台,并提供与WPF相同的自定义控件设计。
  • Avalonia UI
    : 允许在跨平台上使用WPF风格XAML的开源UI框架。支持与WPF相同的自定义控件设计,通过独特的技术扩展支持各种平台。
  • OpenSilver
    : 为将旧的Silverlight迁移到OpenSilver而优化的开源平台。以与Silverlight几乎相同的方式运行,也为WPF开发人员提供熟悉的环境。

2. 考虑跨平台的.NET版本选择策略

在跨平台应用程序开发中,需要谨慎选择要使用的.NET版本。因为这将直接影响兼容性、功能性和目标平台支持。

2.1 .NET版本选项

  • .NET Framework
    : 仅用于Windows,主要用于现有的WPF和WinForms应用程序。
  • .NET Standard 2.0 / 2.1
    : 为提供各种.NET实现之间的兼容性而设计的标准。
  • .NET (Core) 3.0及以上
    : 支持Windows、macOS、Linux的跨平台.NET实现,包含最新功能和性能改进。

2.2 选择标准和考虑因素

如果需要跨平台支持,应该选择
.NET Core
或最新的
.NET
。如果与现有.NET Framework库或包的兼容性很重要,那么则应该使用
.NET Standard 2.0
。如果想要最新功能和性能改进,就需要考虑
.NET 5及以上
版本。

此外,跨平台框架从.NET 5.0开始考虑兼容性,并且基于最新版本持续进行功能改进,因此建议大家选择最新的.NET版本。

战略建议
:

  • 将通用库编写为
    .NET Standard 2.0
    以确保最大兼容性。
  • 为每个平台创建项目并引用通用库。
  • 如果可能,使用
    .NET 6及以上
    版本以获得最新功能和性能改进。

3. View和ViewModel的连接策略分析

在MVVM(Model-View-ViewModel)模式中,View和ViewModel的连接是核心部分。连接方式的不同会导致使用MVVM的方式完全不同。因此,我们需要根据使用MVVM的目的来决定DataContext分配方式。

3.1 传统的DataContext直接分配方式

在代码后台创建ViewModel并直接分配给View的DataContext。

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainViewModel();
}

优点
:

  • 实现简单直观
  • 可以明确控制视图模型的创建时机
  • 可以向构造函数传递所需参数

缺点
:

  • View和ViewModel之间产生强耦合
  • 单元测试时难以模拟(Mock)ViewModel
  • 难以利用依赖注入(DI)
  • 需要直接指定DataContext分配时机,可能难以保持一致性

3.2 在XAML中创建ViewModel实例

在XAML中设置DataContext来实例化ViewModel。

<Window x:Class="MyApp.MainWindow"
        xmlns:local="clr-namespace:MyApp.ViewModels">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <!-- Window content -->
</Window>

优点
:

  • XAML的智能感知支持可以减少绑定错误
  • 可以在设计器中预览实际的数据绑定
  • 明确表达View和ViewModel之间的关系

缺点
:

  • ViewModel创建时难以进行依赖注入
  • 复杂的初始化逻辑或参数传递受限
  • DataContext分配时机被强制化,降低了灵活性

3.3 ViewModel直接创建及依赖传递

在代码后台创建ViewModel直接创建及依赖传递时直接传递所需的依赖。

public MainWindow()
{
    InitializeComponent();
    var dataService = new DataService();
    var loggingService = new LoggingService();
    DataContext = new MainViewModel(dataService, loggingService);
}

优点
:

  • 可以在创建ViewModel时明确传递所需的依赖
  • 可以实现复杂的初始化逻辑
  • 可以根据Runtime灵活创建ViewModel实例

缺点
:

  • View需要了解ViewModel的依赖关系
  • 随着依赖增加,代码后台会变得复杂
  • View和ViewModel之间的耦合度仍然很高
  • 需要直接指定DataContext分配时机,可能难以保持一致性
  • 由于不使用DI管理项目,可能会出现依赖关系混乱的情况

3.4 活用依赖注入(DI)容器

使用DI容器来管理ViewModel及其依赖可以降低View和ViewModel之间的耦合度。

public MainWindow()
{
    InitializeComponent();
    DataContext = ServiceProvider.GetService<MainViewModel>();
}

优点
:

  • 降低View和ViewModel之间的耦合度
  • 依赖关系集中管理,提高可维护性
  • 便于进行单元测试
  • 可以在运行时灵活更改依赖关系

缺点
:

  • DI容器的初始配置可能较为复杂
  • 团队成员需要理解DI模式
  • 仍然需要直接在DataContext中创建视图模型,分配时机的一致性可能难以保持
  • 需要决定是将视图模型作为单例还是实例来管理,并考虑视图的生命周期。需要制定明确的规则并严格遵守

3.5 View的自动ViewModel创建策略

为了解决上述问题,我们可以考虑在View创建创建的约定时间点,通过依赖注入创建ViewModel并且分配给DataContext。例如,设计一个基于ContentControl的Veiw,自动创建ViewModel就是一个有效的方法。

public class UnoView : ContentControl
{
    public UnoView()
    {
        this.Loaded += (s, e) =>
        {
            var viewModelType = ViewModelLocator.GetViewModelType(this.GetType());
            DataContext = ServiceProvider.GetService(viewModelType);
        };
    }
}

优点
:

  • 可以保持DataContext分配时机的一致性
  • 降低View和ViewModel之间的耦合度
  • ViewModel的创建和依赖注入自动处理
  • View不需要知道自己应该使用哪个ViewModel

这几乎是一个没有缺点的方法,通过管理单一的View,可以统一处理时机和处理逻辑。在结构性完善和扩展方面也能保证很好的设计。

不过需要View和ViewModel之间的Mapping,可以使用Dictionary或Mapping Table 来实现。这样可以集中管理View和ViewModel之间的连接信息。关于连接管理的映射方法,我们将在后面的
Bootstrapper设计方法论
中详细讨论。

4. 框架设计的必要功能及实现方案

在设计应用程序架构时,构建考虑
可重用性和可扩展性
的框架非常重要。为此,使用依赖注入(DI)容器是必不可少的。

4.1 依赖注入(DI)容器的使用

DI是现代软件开发中不可或缺的模式,对依赖关系管理和降低耦合度有很大帮助。然而,在像WPF这样的桌面应用程序中,Web应用程序中常用的
Microsoft.Extensions.DependencyInjection
等DI容器可能并不完全适合。

4.1.1 Microsoft.Extensions.DependencyInjection的使用和注意事项

Microsoft.Extensions.DependencyInjection
是.NET官方提供的DI容器,可以说是.NET Foundation的标准。它被用于ASP.NET Core、EntityFrameworkCore、MAUI等各种平台和框架中的几乎所有系统中使用,并提供
Transient

Scoped

Singleton
等生命周期管理功能。

然而,在WPF中,这种标准DI的生命周期可能和WPF实际情况并不完全匹配。

注意事项
:

  • 在WPF等桌面应用程序中,可能不需要
    Scoped
    生命周期
  • Transient

    Singleton
    的概念是为服务或Web应用程序设计的,在WPF中某些功能可能不适用
  • 可能带来不必要的复杂性,对于WPF的使用场景来说,更简单轻量的DI容器可能更合适

当然,即使不使用
Transient
这样的生命周期也可以使用DI,但准确理解这些要点是非常重要的。

4.1.2 CommunityToolkit.Mvvm的DI

CommunityToolkit.Mvvm
并不直接提供像
Microsoft.Extensions.DependencyInjection
这样的DI。这可能是因为
Microsoft.Extensions.DependencyInjection
和WPF的生命周期特性不完全匹配。

但是,
CommunityToolkit.Mvvm
通过提供
Ioc.Default
允许开发者使用任何想要的DI容器。任何实现了
System.IServiceProvider
接口的DI容器都可以注册使用。

因此,使用
CommunityToolkit.Mvvm
时可以选择DI。最常用的DI之一无疑是
Microsoft.Extensions.DependencyInjection
,使用
Prism
这样的DI也是非常有效的组合。

4.1.3 直接设计DI容器的优势

基于
IServiceProvider
接口设计DI的方法可以注册到
CommunityToolkit.Mvvm

Ioc.Default
中,实现内部功能的连接和兼容。由于
IServiceProvider
只要求实现
GetService
等最基本的功能,因此可以实现非常简单的DI。

优点
:

  • 实现只包含必要功能的简单DI容器,降低项目复杂性
  • 可以在内部设计、控制和扩展各种功能
  • 可以精确构建整体框架架构和项目设计
  • 提供不依赖特定平台的统一DI容器,有利于跨平台开发

示例代码
:

// 基于IServiceProvider的DI容器实现
public class SimpleServiceProvider : IServiceProvider
{
    private readonly Dictionary<Type, Func<object>> _services = new();

    public void AddService<TService>(Func<TService> implementationFactory)
    {
        _services[typeof(TService)] = () => implementationFactory();
    }

    public object GetService(Type serviceType)
    {
        return _services.TryGetValue(serviceType, out var factory) ? factory() : null;
    }
}

// DI容器注册和使用
var serviceProvider = new SimpleServiceProvider();
serviceProvider.AddService<IMainViewModel>(() => new MainViewModel());

Ioc.Default.ConfigureServices(serviceProvider);

这样基于
IServiceProvider
接口实现简单的DI容器,就可以注册到
CommunityToolkit.Mvvm

Ioc.Default
中,实现内部功能的连接和兼容。如果觉得使用
Microsoft.Extensions.DependencyInjection

Prism
等主流DI太繁重,自己直接来实现一个是非常有吸引力的选择。

注意
:
如果不遵循
IServiceProvider

System.ComponentModel
标准,可能会失去和
CommunityToolkit.Mvvm

Ioc
兼容性。但是我们可以将
CommunityToolkit.Mvvm
仅作为MVVM相关的模块,创建一个更专业、更统一的、不依赖特定平台或框架的DI容器。这对于创建可以在跨平台等多个XAML平台上通用的框架是非常适合的。

5. WPF技术在其他平台上的有效使用策略

要在其他XAML平台上最大限度地利用WPF强大的功能,我们需要了解一些历史背景和核心策略。同时也需要详细了解可以直接使用WPF技术的平台特性。

5.1 平台间的特征和差异理解

  • UWP和WinUI 3的差异
    : UWP作为Windows 10的专用平台,由于应用商店注册指南和WinAPI限制等原因,与WPF和WinForms等传统平台的兼容性较差。因此,WinUI 3应运而生,它不仅继承了UWP的所有优点,还改进了其存在的问题,发展成为了一个像WPF一样具有高自由度的平台。

  • Uno Platform桌面版与WinUI 3的一致性
    : Uno Platform支持Windows、macOS、Linux的桌面平台完全遵循WinUI 3的方式。因此,WinUI 3直接使用UWP的核心库,而Uno Platform也同样采用WinUI 3的方式,这意味着所有以
    Microsoft.*
    开头的DLL库都可以共享使用。

理解这些平台间的特征可以让我们认识到
Uno Platform Desktop
是一个非常高效且具有吸引力的平台。因此,在WPF和Uno Platform之间进行技术共享和转换的策略非常有效且高效,因为它们与WinUI 3和UWP都有着紧密的联系。

5.2 充分利用VisualStateManager(VSM)

由于不是所有平台都可以直接使用WPF的Trigger,所以我们就需要一个替代策略。
VisualStateManager(VSM)
在解决这个问题上起着核心作用。

VSM是在Silverlight 2.0中引入的,用来弥补Trigger不足的,对自定义控件和XAML之间的状态处理进行了优化。随后在.NET 4.0中,VSM也被引入到WPF中,WPF的Button、CheckBox、DataGrid、Window等所有CustomControl的内部设计都从Trigger改为了VSM。

优点
:

  • 在不能直接使用Trigger的平台上可以通过VSM实现相同功能
  • 可以有效实现UI状态管理和动画
  • 可以通过VSM统一不同平台的不同行为

最终,通过集中使用VSM,就可以实现在WPF、Uno Platform Desktop、WinUI 3、UWP等平台上构建相同的XAML和CustomControl,源代码也可以完全共享。

5.3 灵活运用IValueConverter

IValueConverter
是一个允许在数据绑定时转换值的接口,对于抽象化平台间差异非常有用。

策略性使用
:

  • 可以实现和替代几乎与Trigger相同的功能,编写简单有效的源代码
  • 由于每次都需要创建Converter,而且且重用性标准模糊,建议不要过分追求重用性,而是灵活使用
  • 即使没有重用性也要直观使用,重要的是通过明确的命名来尽量减少分支,专门化使用

局限和补充
:

  • 仅使用
    IValueConverter
    是有限的
  • IValueConverter
    应用于简单转换,复杂场景的管理会带来负担,这时我们应通过
    VSM
    解决
  • 复杂的状态处理最好使用
    VisualStateManager

总之,
IValueConverter
补充了VSM的不足,对于简单直接的转换工作应该直观灵活使用,不要过分追求重用性。

6. 分布式项目管理的Bootstrapper设计方法论

随着应用程序变得复杂和模块化,初始化过程和依赖管理变得越来越重要。
Bootstrapper模式
在集中管理这些初始化逻辑方面非常有用。

虽然所有平台都以Application设计为基础,但由于各平台的特性和方式不同,Application设计各不相同。因此,为了在所有平台上保持相同的开发方式,使用
Bootstrapper结构设计
是非常有效的。

6.1 Bootstrapper的作用和必要性

Bootstrapper的功能
:

  • 依赖注入设置
    :初始化DI容器,注册必要的服务、View和ViewModel。
  • 管理视图和视图模型的连接
    :通过依赖注入注册View,管理View和ViewModel之间的映射。
  • 集中化配置管理
    :所有配置都在Bootstrapper中管理,使应用程序项目只执行其角色,其余功能实现通过项目分布式和模块化来管理。
  • 此外,还可以灵活地扩展集中化管理项目,且不会影响Application。

优点
:

  • 通过分离应用程序的初始化逻辑来提高代码的可读性和可维护性。
  • 通过项目分布式和模块化,可以独立开发功能实现。
  • 最小化平台之间的结构差异,保持一致的架构。

6.2 Bootstrapper的设计方案

示例代码
:

namespace Jamesnet.Core;

public abstract class AppBootstrapper
{
    protected readonly IContainer Container;
    protected readonly ILayerManager Layer;
    protected readonly IViewModelMapper ViewModelMapper;

    protected AppBootstrapper()
    {
        Container = new Container();
        Layer = new LayerManager();
        ViewModelMapper = new ViewModelMapper();
        ContainerProvider.SetContainer(Container);
        ConfigureContainer();
    }

    protected virtual void ConfigureContainer()
    {
        Container.RegisterInstance<IContainer>(Container);
        Container.RegisterInstance<ILayerManager>(Layer);
        Container.RegisterInstance<IViewModelMapper>(ViewModelMapper);
        Container.RegisterSingleton<IViewModelInitializer, DefaultViewModelInitializer>();
    }

    protected abstract void RegisterViewModels();
    protected abstract void RegisterDependencies();

    public void Run()
    {
        RegisterViewModels();
        RegisterDependencies();
        OnStartup();
    }

    protected abstract void OnStartup();
}

通过这样的抽象化,可以明确强调管理结构,并通过虚方法控制时间点和顺序。这有助于灵活扩展和完善,并且不影响Application,使其在各种平台上以一致的方式运行。

7. 在跨平台桌面环境中最大化WPF技术的策略

通过在其他基于XAML的跨平台框架中最大限度地利用WPF中使用的技术和模式,可以提高开发效率。

7.1 实现可在所有平台上运行的框架

Jamesnet.Core是基于
.NET Standard 2.0
的框架,允许在WPF、Uno Platform和WinUI 3中实现相同的项目设计。该框架具有以下特点:

  • DI设计:利用基于IServiceProvider的DI容器,可以与CommunityToolkit.Mvvm配合使用。
  • MVVM Bootstrapper:集中管理项目的初始化和依赖注入。
  • View和ViewModel之间的连接管理:通过层管理等方式降低View和ViewModel的耦合度。
  • 在所有基于XAML的平台上统一运行。
  • 直接引用存储库源代码,便于调试、功能实现、扩展和研究。

优点
:

  • 无论使用WPF、Uno Platform还是WinUI 3开发,都可以保持相同的架构。
  • 使用
    Uno Platform Desktop
    可以在macOS和Linux上进行开发和运行。
  • 可以使用
    JetBrains Rider
    构建跨平台开发环境。

7.2 实际实现案例分析

英雄联盟客户端重构项目
利用Jamesnet.Core框架,在WPF、Uno Platform和WinUI 3等不同平台上使用相同的代码库和架构实现。




战略方法
:

  • 通过
    Jamesnet.Core框架
    保持
    项目设计的统一性
  • 利用
    DI容器和Bootstrapper
    集中管理视图和视图模型。
  • 使用
    VisualStateManager(VSM)
    替代Trigger,在不同平台上以相同方式管理UI状态。

成果
:

  • 97%以上的代码共享
    ,最大化了向其他平台扩展的可能性。
  • 在各种平台上提供
    一致的用户体验和开发方法论
    ,使技术转换更容易。
  • 通过项目分散化、模块化和管理集中化,大大降低了开发和维护成本。
  • 通过
    CustomControl的模块化开发
    提高了重构和扩展性,在GPT、Claude等
    AI应用
    方面,分散的视图系统也更加有效。

结论

WPF技术和模式仍然强大,将其应用于跨平台开发可以提高开发效率和代码重用性。特别是使用
Jamesnet.Core框架
,通过DI容器的集中化管理策略和引入Bootstrapper,有助于降低视图和视图模型之间的耦合度,提高可维护性。

此外,通过积极使用
VisualStateManager

IValueConverter
,可以最小化平台之间的差异并保持一致的设计。通过这些策略,可以超越WPF基础,战略性地实现跨平台技术扩展。

特别是,
UWP

WinUI 3

Uno Platform
之间100%相同地使用XAML相关DLL,因此这些平台之间几乎没有差异。因此,对WPF开发者来说,
使用Uno Platform桌面版非常有效且具有战略意义
。这是因为从WPF转换到Uno可以在几小时内完成,转换到WinUI 3也非常容易。

未来,WPF技术和基于XAML的框架将继续发展,利用这些进行跨平台开发将变得更加重要。开发人员需要很好地把握这些趋势,制定适当的策略,开发高质量的应用程序。

参考

主要仓库

目前已更新的WPF教程(自定义控件)

《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

在Rust中,结构体(Struct)是一种自定义数据类型,它允许我们将多个相关的值组合在一起,形成一个更复杂的数据结构。结构体在Rust中被广泛应用于组织和管理数据,具有灵活性和强大的表达能力。本节将详细介绍Rust中结构体的概念、定义语法、方法以及相关特性,并提供代码示例来帮助读者更好地理解结构体的使用方法。

8.3.1 结构体的定义

Rust 中的结构体与元组都可以将若干类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它的成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫作“字段”。

在Rust中,我们可以使用struct关键字定义一个结构体。结构体允许我们定义多个字段(Fields),每个字段都有自己的类型和名称。通过将字段组合在一起,可以创建自己的数据类型,以便更好地表示和操作数据。以下是一个简单的结构体定义的示例:

structPoint {
x: i32,
y: i32,
}

在上述示例中,我们定义了一个名为Point的结构体,它具有两个字段x和y,分别是i32类型的整数。再来看一个结构体定义:

structSite {
domain: String,
name: String,
nation: String,
found: u32
}

注意:如果你常用C/C++,请记住在Rust中struct语句仅用来定义,不能声明实例,结尾不需要“;” 符号,而且每个字段定义之后用“,”分隔。

8.3.2 结构体实例化

一旦定义了结构体,可以通过实例化结构体来创建具体的对象。可以通过以下两种方式实例化结构体:

1)声明式实例化

let p = Point { x: 10, y: 20 };

在上述示例中,我们使用Point结构体的定义创建了一个名为p的实例,同时指定了字段x和y 的值。

2)可变实例化

如果需要修改结构体的字段值,可以在定义结构体变量时加上mut,代码如下:

let mut p = Point { x: 10, y: 20};
p.x
= 30;
p.y
= 40;

在上述示例中,我们创建了一个可变实例p,并修改了字段x和y的值。

Rust很多地方受JavaScript的影响,在实例化结构体的时候用JSON对象的key: value语法来实现,比如:

let mysite =Site {
domain: String::
from("www.qq.com"),
name: String::
from("qq"),
nation: String::
from("China"),
found:
2024};

如果你不了解 JSON 对象,可以不用管它,记住格式就可以了:

结构体类名 {
字段名 : 字段值,
...
}

这样的好处是不仅使程序更加直观,还不需要按照定义的顺序来输入成员的值。如果正在实例化的结构体有字段名称和现存变量名称一样,可以简化书写:

let domain = String::from("www.qq.com");
let name
= String::from("qq");
let runoob
=Site {
domain,
//等同于 domain : domain, name, //等同于 name : name, nation: String::from("China"),
traffic:
2024};

有这样一种情况:想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中一两个字段的值,可以使用结构体更新语法:

let site =Site {
domain: String::
from("www.qq.com"),
name: String::
from("qq"),
..qq
};

注意:..qq后面不可以有逗号。这种语法不允许一成不变地复制另一个结构体实例,意思就是至少重新设定一个字段的值,才能引用其他实例的值。

8.3.3 结构体的方法
在Rust中,结构体可以拥有自己的方法。方法是与结构体关联的函数,可以通过结构体实例调用。以下是一个结构体方法的示例:

structRectangle {
width: u32,
height: u32,
}

impl Rectangle {
//使用关键字impl来定义结构体的一个或多个方法 fn area(&self) -> u32 { //用关键字fn定义具体的函数 self.width *self.height
}
}

fn main() {
let rect
= Rectangle { width: 10, height: 20};
let area
=rect.area();
println
!("Area: {}", area);
}

在上述示例中,我们定义一个名为Rectangle的结构体,并为其实现一个area方法,用于计算矩形的面积。在main函数中,我们创建一个Rectangle实例rect,然后通过调用area方法计算矩形的面积并打印出来。

8.3.4 结构体的关联函数

除实例方法外,结构体还可以定义关联函数(Associated Functions)。关联函数是直接与结构体关联的函数,不需要通过结构体实例来调用。以下是一个关联函数的示例:

structCircle {
radius: f64,
}

impl Circle {
fn
new(radius: f64) ->Circle {
Circle { radius }
}

fn area(
&self) ->f64 {
std::f64::consts::PI
* self.radius *self.radius
}
}

fn main() {
let circle
= Circle::new(5.0);
let area
=circle.area();
println
!("Area: {}", area);
}

在上述示例中,我们定义一个名为Circle的结构体,并为其实现一个关联函数new,用于创建一个新的Circle实例。在main函数中,我们通过调用Circle::new关联函数创建一个Circle实例circle,然后通过调用area方法计算圆的面积并打印出来。

8.3.5 结构体的特性

Rust的结构体具有两个特性:元组结构体(Tuple Struct)和类单元结构体(Unit-Like Struct)。

元组结构体是一种特殊类型的结构体,它没有命名的字段,只有字段的类型。元组结构体使用圆括号而不是花括号来定义。比如:

struct Color(i32, i32, i32);

在上述示例中,我们定义了一个名为Color的元组结构体,它包含3个i32类型的字段。

类单元结构体是一种没有字段的结构体,类似于空元组。比如:

struct Empty;

在上述示例中,我们定义了一个名为Empty的类单元结构体。