In mathematics you don't understand things, you just get used to them.

标签 data structures 下的文章

我说怎么 T2 被暴切,原来大家都做过这道题,这下是 🤡 了。

Desc.

Link.

有 $m$ 次事件,和一个初始为空的双端队列,格式如下:

  • IF w v:$\text{PUSH-FRONT}(w, v)$;
  • IG w v:$\text{PUSH-BACK}(w, v)$;
  • DF:$\text{POP-FRONT}$;
  • DG:$\text{POP-BACK}$;
  • QU l r:询问在当前队列中选取若干二元组 $S$,使得 $\sum w \in S \bmod p \in [l, r]$,且 $\sum v \in S$ 最大。

$m \leqslant 5 \times 10^4$,$p \leqslant 500$。

Sol.

朴素 DP 很容易,设 $f_{i, s}$ 表示前 $i$ 个元素中,$w$ 和模 $p$ 的余数为 $s$ 的最大 $v$ 和。关键是如何维护双端队列的操作。

如果维护的数据结构是栈,那么这个题就变得很容易了,恰好,我们有用栈实现双端队列的方法,以下是 tly 的解说:

至少支持:双端插入删除(删除时非空),维护半群运算(只有结合律),做到均摊 $\mathcal O(n)$ 次运算。

比如双端插入删除元素,求矩阵乘或者 min 之类的不可减信息。

……

具体做法是维护两个栈,分别用于前端插入删除/后端插入删除。这个时候就是「插入同端删除,也即可撤回」的情况了。

如果一个栈空了,这个时候就不能直接把另一个栈弄过来。

考虑将另一个栈的元素按中点划分开,分别装入两个栈,这样复杂度是均摊 $\mathcal O(n)$。
39
具体原因考虑势能函数 $\Phi = |size_1 - size_2|$,每次 $\Phi$ 至多增加 $1$,重构则清零(或变成 $1$),因此复杂度均摊下来线性。

(有删改)

代码很简洁。

int __tmp, m, p, w[2][50100], v[2][50100], top[2], q[600];
ll dp[2][50100][600];
string op;
void insert(int a, int b, int f) {
    w[f][++top[f]] = a, v[f][top[f]] = b;
    for (int i=0;i<p;++i)
        dp[f][top[f]][(i+a)%p] = max(dp[f][top[f]-1][i]+b, dp[f][top[f]-1][(i+a)%p]);
}
void remove(int f) {
    if (top[f] == 0) {
        int tmp = top[f^1];
        top[0] = top[1] = 0;
        for (int i=(tmp+1)/2;i>=1;--i) insert(w[f^1][i], v[f^1][i], f);
        for (int i=(tmp+1)/2+1;i<=tmp;++i) insert(w[f^1][i], v[f^1][i], f^1);
    }
    top[f]--;
}
ll ask(int l, int r) {
    ll res = -1, *f = dp[0][top[0]], *g = dp[1][top[1]];
    int h = 1, t = 0;
    for (int i=r;i>=l;--i) {
        while (h <= t && g[i] >= g[q[t]]) t--;
        q[++t] = i;
    }
    for (int i=0;i<p;++i) {
        while (h <= t && ((i+q[h])%p < l || (i+q[h])%p > r)) h++;
        while (h <= t && g[(l+p-i)%p] >= g[q[t]]) t--;
        q[++t] = (l+p-i)%p;
        cmax(res, f[i]+g[q[h]]);
    }
    return res;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    for (int i=0;i<2;++i)
        memset(dp[i][0]+1, 0xcf, sizeof dp[i][0]);
    cin >> __tmp >> m >> p;
    for (int a, b; m--;) {
        cin >> op;
        if (op[0] == 'I' || op[0] == 'Q') cin >> a >> b;
        if (op == "IF") insert(a%p, b, 0);
        else if (op == "IG") insert(a%p, b, 1);
        else if (op == "DF") remove(0);
        else if (op == "DG") remove(1);
        else cout << ask(a, b) << "\n";
    }
}

我既没有愁苦到足以成为诗人,又没有冷漠到像个哲学家。但我清醒到足以成为一个废人。

—— E·M·齐奥朗 - 眼泪与圣徒

Desc.

Link.

给出一棵 $n$ 个节点的树,在其中 $m$ 条边上俢设收费站,通过收费站 $i$ 可选择付一枚金币或 $c_i$ 枚银币。现在有 $q$ 个市民,要从 $s_i$ 旅行到 $t_i$,带有 $x_i$ 枚金币和 $y_i$ 枚银币。最大化旅行结束后金币的保有量,或报告无解。

$1\leqslant n, q \leqslant 10^5$。

Sol.

由于金币的支出总为一,因此贪心策略很显然,将路径上的 $c$ 排序,从小到大能选辄选。能够选择的数量可以二分算,由于在树上,用可持久化线段树维护即可。

具体而言,每次新建一棵树,都从父亲继承,询问时需要使用到的子树和,可以通过求 LCA 转化成树上差分。

这个线段树上二分的写法有点说头。在当前节点做决策而非在递归左右子树之前做判断能让代码更简洁,具体见代码。

最后要注意,离散化的时候要让相同权值的收费站占据不同的编号,而不能直接合并,原因很简单,你可能终止在相同的收费站之间。

// test: https://www.luogu.com.cn/
int n, m, q, rt[100100], tot;
int e[100100][2];
vi mnt[100100], grp[100100];
map<int, int> buc;
int ls[3000100], rs[3000100];
struct node {
    int cnt;
    ll sum;
} tr[3000100];
int upd(int v, int p, ll w, int l = 0, int r = m) {
    int u = ++tot;
    tr[u] = tr[v];
    tr[u].cnt++, tr[u].sum += w;
    if (l+1 == r) return u;
    int mid = (l+r)/2;
    if (mid > p) rs[u] = rs[v], ls[u] = upd(ls[v], p, w, l, mid);
    else ls[u] = ls[v], rs[u] = upd(rs[v], p, w, mid, r);
    return u;
}
int ask(int u, int v, int lca, ll all, int l = 0, int r = m) {
    if (all <= 0) return tr[u].cnt+tr[v].cnt-2*tr[lca].cnt;
    else {
        if (tr[u].sum+tr[v].sum-2*tr[lca].sum <= all) return 0;
        if (l+1 == r) return tr[u].cnt+tr[v].cnt-2*tr[lca].cnt;
        int mid = (l+r)/2;
        ll tmp = tr[ls[u]].sum+tr[ls[v]].sum-2*tr[ls[lca]].sum;
        return ask(ls[u], ls[v], ls[lca], all, l, mid)+ask(rs[u], rs[v], rs[lca], all-tmp, mid, r);
    }
}
int fa[20][100100], t, dep[100100];
void dfs(int u, int Fu) {
    dep[u] = dep[Fu]+1;
    fa[0][u] = Fu;
    for (int i=1;i<t;++i)
        fa[i][u] = fa[i-1][fa[i-1][u]];
    for (int v : grp[u])
        if (v != Fu) dfs(v, u);
}
int LCA(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int i=t-1;i>=0;--i)
        if (dep[fa[i][u]] >= dep[v]) u = fa[i][u];
    if (u == v) return u;
    for (int i=t-1;i>=0;--i)
        if (fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v];
    return fa[0][u];
}
void dfs2(int u) {
    rt[u] = rt[fa[0][u]];
    for (int x : mnt[u])
        rt[u] = upd(rt[u], buc[x]++, x);
    for (int v : grp[u])
        if (v != fa[0][u]) dfs2(v);
}
int main() {
    ignore = freopen("in", "r", stdin);
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m >> q;
    t = __lg(n)+1;
    for (int i=0;i<n-1;++i) {
        cin >> e[i][0] >> e[i][1];
        e[i][0]--, e[i][1]--;
        grp[e[i][0]].pb(e[i][1]);
        grp[e[i][1]].pb(e[i][0]);
    }
    for (int i=0;i<t;++i)
        fill(fa[i], fa[i]+n+1, n);
    dfs(0, n);
    vi tmp;
    for (int i=0,p,c;i<m;++i) {
        cin >> p >> c;
        tmp.pb(c);
        p--;
        if (dep[e[p][0]] == dep[e[p][1]]+1) swap(e[p][0], e[p][1]);
        mnt[e[p][1]].pb(c);
    }
    sort(begin(tmp), end(tmp));
    for (int i=m-1;i>=0;--i) buc[tmp[i]] = i;
    dfs2(0);
    while (q--) {
        int u, v, x;
        ll y;
        cin >> u >> v >> x >> y;
        u--, v--;
        int ret = ask(rt[u], rt[v], rt[LCA(u, v)], y);
        cout << max<ll>(x-ret, -1) << "\n";
    }
}

/ 白头如新,倾盖如故。/

—— 邹阳 - 狱中上梁王书

Desc.

Link.

给定一棵 $n$ 个节点的树, $m$ 个关键点 $\{a_m\}$, 和 $q$ 次询问 $l_i, r_i$. 每次询问点集 $\{a_l, \dots, a_r\}$ 在原树上虚树的大小.

$1 \leqslant n, q \leqslant 10^5$.

Sol.

考虑离线扫描线, 按 $l$ 排序. 每次扫描线加入一个点 $i$ 时, 就将 $a_i$ 到根节点这条路径上的节点的颜色覆盖为 $i$, 然后查询的答案就是树上所有颜色 $\leqslant r_i$ 的节点数减去 $a_l, \dots a_r$ 的 LCA 的深度.

具体实现, 先采用树剖, 颜色覆盖用 std::set 维护 (基于随机的颜色段均摊), 这里的复杂度是有保障的. 至于查询的第一部分, 使用树状数组维护即可. 第二部分可以用一个经典结论, 即若干个点的 LCA 等于 DFN 最小和最大的两点的 LCA, 用 RMQ 维护即可.

复杂度不会算, 但是跑得飞快.

struct RMQ {
    const int n;
    int t;
    vvi fi, fx;
    RMQ(const vi& v) : n(v.size()) {
        t = __lg(n)+1;
        fi = fx = vvi(t);
        fi[0] = fx[0] = v;
        for (int i=1;i<t;++i) {
            fi[i] = fx[i] = vi(n-(1<<i)+1);
            for (int j=0;j+(1<<i)<=n;++j) {
                fi[i][j] = min(fi[i-1][j], fi[i-1][j+(1<<(i-1))]);
                fx[i][j] = max(fx[i-1][j], fx[i-1][j+(1<<(i-1))]);
            }
        }
    }
    int evalmin(int l, int r) {
        assert(0 <= l && l < r && r <= n);
        int k = __lg(r-l);
        return min(fi[k][l], fi[k][r-(1<<k)]);
    }
    int evalmax(int l, int r) {
        assert(0 <= l && l < r && r <= n);
        int k = __lg(r-l);
        return max(fx[k][l], fx[k][r-(1<<k)]);
    }
};
struct Interval {
    int l, r;
    mutable int v;
    Interval(int _l, int _r, int _v) : l(_l), r(_r), v(_v) {}
    bool operator<(const Interval& rhs) const { return l < rhs.l; }
};
set<Interval> odt;
int n, m, q, dfn[100100], times, ans[100100], dep[100100], fa[100100], t, rev[100100];
int son[100100], top[100100], sz[100100];
vi grp[100100];
void dfs(int u, int Fu) {
    dep[u] = dep[Fu]+1;
    fa[u] = Fu;
    sz[u] = 1;
    for (int v : grp[u])
        if (v != Fu) {
            dfs(v, u);
            sz[u] += sz[v];
            if (sz[v] > sz[son[u]]) son[u] = v;
        }
}
void dfs2(int u, int ct) {
    rev[dfn[u] = times++] = u;
    top[u] = ct;
    if (son[u] < n) dfs2(son[u], ct);
    for (int v : grp[u])
        if (v != fa[u] && v != son[u]) dfs2(v, v);
}
int glca(int u, int v) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        u = fa[top[u]];
    }
    return dep[u] < dep[v] ? u : v;
}
int bit[100100];
void add(int p, int x) {
    for (p++;p<=m;p+=p&-p) bit[p] += x;
}
int ask(int p) {
    int res = 0;
    for (;p;p-=p&-p) res += bit[p];
    return res;
}
///  @returns: [l, x), pointer at [x, r]
auto split(int x) {
    if (x == n) return odt.end();
    auto it = --odt.upper_bound(Interval(x, 0, 0));
    if (it->l == x) return it;
    auto [l, r, v] = *it;
    odt.erase(it);
    odt.emplace(l, x-1, v);
    return odt.emplace(x, r, v).fs;
}
void assign(int l, int r, int x) {
    auto ir = split(r+1), il = split(l);
    for (auto it=il;it!=ir;++it)
        if (it->v >= 0) add(it->v, it->l-it->r-1);
    odt.erase(il, ir);
    odt.emplace(l, r, x);
    add(x, r-l+1);
}
void access(int u, int x) {
    for (;u<n;u=fa[top[u]])
        assign(dfn[top[u]], dfn[u], x);
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m >> q;
    t = __lg(n)+1;
    for (int i=1,u,v;i<n;++i) {
        cin >> u >> v;
        u--, v--;
        grp[u].pb(v), grp[v].pb(u);
    }
    fill(son, son+n, n);
    fa[n] = n;
    dfs(0, n);
    dfs2(0, 0);
    vi init(m);
    for (int i=0,x;i<m;++i) {
        cin >> x;
        x--;
        init[i] = dfn[x];
    }
    RMQ st(init);
    vt<vt<pii>> mnt(m);
    for (int i=0,l,r;i<q;++i) {
        cin >> l >> r;
        l--;
        assert(0 <= l && l < m);
        mnt[l].eb(r, i);
        int xl = st.evalmin(l, r);
        int xr = st.evalmax(l, r);
        ans[i] -= dep[glca(rev[xl], rev[xr])]-1;
    }
    odt.emplace(0, n-1, -1);
    for (int i=m-1;i>=0;--i) {
        access(rev[init[i]], i);
        for (auto&& [r, id] : mnt[i]) ans[id] += ask(r);
    }
    for (int i=0;i<q;++i) cout << ans[i] << "\n";
}

君子之交淡若水,因为淡所以不腻,才能持久。

—— 梁实秋 - 谈友谊

Link.

今天的题解写得有点水, 没什么参考价值.

A. 出关 (laozi)

可以写出朴素的方程式:

$$ f_{i}+A_{s_i} \rightarrowtail f_{i+1} \\ f_i+B+k \times C \rightarrowtail f_{2i-k} $$

$\mathcal O(\frac {n^2}T)$. 其中 $T$ 是某个类似周期的数. 注意字符串是 0-indexed, 而 DP 数组是 1-indexed 的. 对第二个方程换元 $f_i + B + 2i \times C -(2i-k) \times C \rightarrowtail f_{2i-k}$, 令 $v_i = B + 2i \times C + f_i$, 则 $v_i - j \times C \rightarrow f_j$, 那么用线段树维护区间取 min, 单点求值, 标记永久化即可. 求范围需要求解 Z function.

B. 补天 (nvwa)

有点恶心. 首先, 合法的方案一共两种, 取决于第一个放置的颜色. 那么要统计的就是 $(i+j)\bmod 2 = 0$ 的位置数或 $(i+j)\bmod 2 = 1$ 的位置数. 先将所有区间离散化, 使用并查集维护连续段. 并查集的节点上再挂载一个 $cnt_{0/1}$, 分别表示连续段内符合上述两种条件的位置数. 再使用线段树维护区间内... [咕]

C. 理水 (dayu)

发现以前根本没做过正经换根 DP.

先对原图进行边双缩点, 得到一棵树. 如果一个边双连通分量里面存在一条 $1$ 边, 则经过该点的路径均为合法路径. 对于固定的根节点, 设 $f_u$ 表示从 $u$ 出发, $u$ 的子树里的合法节点数. 然后换根即可.

D. 非攻 (mozi)

高一学弟的做法非常简洁, 很有水平! 以下就是他的做法.

考虑固定排列 $P = (p_1, \dots, p_n)$, 连边 $i \rightarrow p_i$, 解决交换次数最小的限制很容易, 注意到每次操作最多让一个环里面的节点变成自环即可. 然后是代价最小的限制, 一个下界是 $\displaystyle \sum m_i(s_i-m_i)$ 其中 $m_i$ 和 $s_i$ 分别表示第 $i$ 个环的最小值和权值和. 手玩发现, 一次交换使一个点变成自环, 剩下的点构成一个新环, 因此这个下界总是取得到.

接下来解决计数. 对一对数 $(i, j)$, 考虑 $i \times j$ 对答案的贡献次数, 这对数对答案产生贡献当且仅当:

  • $i$ 是环中最小值;
  • $i$ 和 $j$ 在同一环里.

进行构造:

  1. 把前 $i-1$ 个数放进图里, 这部分可以随意放, 方案数 $(i-1)!$;
  2. 把 $i$ 放进来, 由于 $i$ 是最小值, 因此 $i$ 只能构成一个新环, 方案数 $1$;
  3. 把 $j$ 放进和 $i$ 同一环里, 因为此时环里仅有 $i$ 一个元素, 方案数 $1$;
  4. 把 $(i, j)$ 和 $(j, n]$ 中的数放进来, 这部分可以随意放, 方案数 $\frac{n!}{(i+1)!}$.

整理以下, 答案就是 $\displaystyle \sum_i \sum_j ij\times \frac{n!(i-1)!}{(i+1)!}$, 预处理阶乘计算即可.


/ 如在黑夜中 被熄灭了星空 /

/ 荒原上看不到尽头 /

/ 只有这一路相随的孤独 /

/ 是我黑暗中唯一的盟友 /

—— Kide - ft. 星尘


date: '2023-10-16'
title: 'Solution Set -「LOC 4326」'


Link.

A. 路径 (path)

设 $f_u$ 表示到 $u$ 时凑出的路径节点个数. 以拓扑序转移即可.

B. 异或 (xor)

先差分, 将区间操作转化为双点 (或单点) 操作. 把双点操作抽象成连边, 每个连通块的步数下界是连通块大小减一, 上界是连通块大小.

结论: 取得下界当且仅当连通块异或和为零.

证明:

  • 必要性: 因为是一个连通块, 又只进行了大小减一次操作, 因此每次操作都是双点. 因此连通块异或和不变, 因此连通块异或和是 $0$;
  • 充分性: 当连通块异或和为 $0$, 设最后一个元素为 $x$, 则前面元素的异或和也为 $x$, 每次操作选择一个未被选择过的非最后一个元素的元素, 令其与最后一个元素均异或其, 操作次数为连通块大小减一.

答案即为 $n$ 减去最大的异或和为零的子序列划分数. 状压即可.

C. 距离 (distance)

我甚至没学过点分树...

考虑单点. 建出点分树, 令 $d(x, y)$ 表示原树上 $x, y$ 的距离, $f(p)$ 表示目前已经插入点集且在点分树中 $p$ 的子树里的点中, 距离 $p$ 最近的点. 那么对于一个询问 $a$, 枚举 $a$ 和 $a$ 在点分树上的祖先 $p$, 答案就是 $d(a, p) + \min(f(p))$, 注意我们不需要排除 $a$ 本身在的这棵子树, 因为 $p$ 是 $a$ 和 $x$ 的 LCA 的祖先, 若 $x$ 和 $a$ 在 $p$ 的同一棵子树里, 答案一定更劣.

再考虑点对. 对于一个询问 $(a, b)$, 把路径拆成 $x \rightarrow p \rightarrow a$ 和 $y \rightarrow q \rightarrow b$. 设 $f(p, q)$ 表示与 $f(p)$ 类似的含义, 预处理出来我们就可以回答询问了. 但是 $\mathcal O(n^2)$ 的空间并不能承受, 于是把询问挂载在 $a$ 到根的路径上, 求 $f_p(q)$ 即可.

说着简单, 代码还是有点难度的, 尤其我对点分树完全不熟悉...

D. 花之舞 (dance)

不会.


/ 即使我们分隔 在遥远的两端 /

/ 我会始终凝望 你存在的方向 /

/ 很感谢你曾经赋予给我的最特殊的意义 /

/ 为我描画翅膀 去向蔚蓝之上 /

/ 闪烁最动人的星光 /

—— Evalia - 星辰 ft. 星尘