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

分类 笔记 下的文章

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";
}

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

—— 梁实秋 - 谈友谊

Desc.

Link.

给出 $n$ 个最高位不超过 $m$ 二进制数, 对于每个 $i \in [1, n]$, 找到一个 $j \neq i$, 使得删去 $i$ 和 $j$ 后, 满足票数超过 $\lfloor \frac n2 \rfloor$ 的数位数最大. 某个二进制数的某一位为 $1$ 就相当于给这一位投票.

$3 \leqslant n \leqslant 3\times 10^5$, $1 \leqslant m \leqslant 20$.

Sol.

这篇题解写得很玄学, 不好理解, 建议不看.

考虑如下暴力:

int n, m, s[300100], cnt[30];
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i=0,x;i<n;++i)
        for (int j=0;j<m;++j) {
            cin >> x;
            s[i] |= x<<j;
            cnt[j] += x;
        }
    for (int i=0;i<n;++i) { // enumerating chairman
        for (int j=0;j<m;++j) cnt[j] -= s[i]>>j&1;
        int ans = 0;
        for (int j=0;j<m;++j) ans += cnt[j] >= n/2;
        int sw = 0; // set waiting to be determined
        for (int j=0;j<m;++j)
            sw |= (cnt[j] == n/2)<<j;
        int ext = 233;
        for (int j=0;j<n;++j)
            if (j != i) chkmin(ext, __builtin_popcount(s[j]&sw));
        cout << ans-ext << "\n";
        for (int j=0;j<m;++j) cnt[j] += s[i]>>j&1;
    }
}

那么问题转化为, 对于 $S$, 求 $\min\{\mid S \cup T_i \mid\}$, 将 $T_i$ 取补集, 则求 $\max\{\mid S\cup T_i\mid\}$. 令 $g_S$ 表示与 $S$ 的交集最大的 $T_i$ 的 $i$. 由于交集一定是 $S$ 的子集, 因此可以从子集转移过来. 很难说, 只能意会, DJ 给我讲了半天才懂.

大概是, 首先求出 $S$ 的某个超集的编号, 这个可以直接用超集和. 然后以这个为初值, 求解上述的 DP. 当然, 正确的思考顺序是为了上面的 DP 构造初值, 这才是我们的 Motivation.

int n, m, s[300100], cnt[30];
int f[1<<20], g[1<<20];
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m;
    memset(f, -1, sizeof f);
    memset(g, -1, sizeof g);
    for (int i=0;i<n;++i) {
        for (int j=0,x;j<m;++j) {
            cin >> x;
            cnt[j] += x;
            s[i] |= (x^1)<<j;
        }
        if (f[s[i]] == -1) f[s[i]] = i;
        else g[s[i]] = i;
    }
    // == computing f[i], such that @i \in @s[f[i]]
    for (int i=(1<<m)-1;i>=0;--i) {
        for (int j=0;j<m;++j) {
            if (!(i&(1<<j))) {
                int ni = i|(1<<j);
                if (f[i] == -1) {
                    f[i] = f[ni];
                    if (g[ni] != f[i] && g[i] == -1) g[i] = g[ni];
                } else {
                    if (f[i] != f[ni] && g[i] == -1) g[i] = f[ni];
                    else if (f[i] != g[ni] && g[i] == -1) g[i] = g[ni];
                }
            }
        }
    }
    // == end computing f[i]
    // == computing f[i], such that |i&s[f[i]]| is max
    for (int i=0;i<1<<m;++i) {
        for (int j=0;j<m;++j) {
            if (i&(1<<j)) {
                int ni = i^(1<<j);
                vi v;
                for (int x : {f[i], g[i], f[ni], g[ni]})
                    if (x > -1) v.pb(x);
                if (v.empty()) continue;
                sort(v.begin(), v.end(), [&](int x, int y) {
                    return __builtin_popcount(s[x]&i) > __builtin_popcount(s[y]&i);
                });
                v.erase(unique(v.begin(), v.end()), v.end());
                f[i] = v[0];
                if ((int) v.size() > 1) g[i] = v[1];
            }
        }
    }
    // == end computing f[i]
    for (int i=0;i<n;++i) {
        for (int j=0;j<m;++j) cnt[j] -= !(s[i]>>j&1);
        int ans = 0, cur = 0;
        for (int j=0;j<m;++j) ans += cnt[j] > n/2;
        for (int j=0;j<m;++j) cur |= (cnt[j] == n/2)<<j;
        if (i == f[cur]) ans += __builtin_popcount(cur&s[g[cur]]);
        else ans += __builtin_popcount(cur&s[f[cur]]);
        cout << ans << "\n";
        for (int j=0;j<m;++j) cnt[j] += !(s[i]>>j&1);
    }
}

/ 板斜尿流急,坑深屎落迟。/

—— [梁实秋 - 忆清华 [上]](https://s.bailushuyuan.org/novel/traditional/chapters/123805)

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$ 个区间 $[l_i, r_i)$, 问是否能选出恰好 $k$ 区间, 使得两两无交, 并构造一组字典序最小的解.

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

Sol.

一道套着信息学奥赛外壳的 HOMO 题, 撅了好久, やりますね.

考虑如何最小化字典序. 已知前 $i-1$ 个数的选择情况 $S \in 2^{\{1, \dots i-1\}}$, 现在对 $i$ 进行决策. 只需要判断后 $n-i$ 个元素中是否能选出 $k - |S| - 1$ 个无交元素即可, 若能则将 $i$ 加入. 维护 $f(L, R)$ 表示在数轴 $[L, R]$ 上能选出的最大不相交区间数, 于是加入 $i$ 造成的变化就是 $f(L, l-1)+f(r+1, R)-f(l, r)+1$.

于是问题转化成了维护 $f(L, R)$, 由于区间无交, 我们贪心地选择当前能加入的区间中右端点最小的, 于是可以倍增. 令 $f[i][j]$ 为从数轴上 $j$ 出发, 选择 $2^i$ 个无交区间, 能到达地最小右端点. 直接转移即可.

注意 $f$ 的初值, 由于同一个 $l_i$ 可以对应多个 $r_j$, 因此要取 $\min$.

struct Interval {
    int l, r;
    Interval(int _l, int _r) : l(_l), r(_r) {}
    bool operator<(const Interval& rhs) const { return r < rhs.l; }
};
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    vi l(n), r(n), uni;
    for (int i=0;i<n;++i) {
        cin >> l[i] >> r[i];
        uni.pb(l[i]), uni.pb(r[i]);
    }
    sort(allu(uni));
    uni.erase(unique(allu(uni)), end(uni));
    for (int i=0;i<n;++i) {
        l[i] = lower_bound(allu(uni), l[i])-begin(uni);
        r[i] = lower_bound(allu(uni), r[i])-begin(uni)-1;
    }
    const int m = uni.size();
    const int t = __lg(k)+1;
    vvi f(t, vi(m, m));
    for (int i=0;i<n;++i) chkmin(f[0][l[i]], r[i]);
    for (int i=m-2;i>=0;--i) chkmin(f[0][i], f[0][i+1]);
    for (int i=1;i<t;++i)
        for (int j=m-1;j>=0;--j) {
            if (j+1 < m) f[i][j] = f[i][j+1];
            if (f[i-1][j]+1 < m) chkmin(f[i][j], f[i-1][f[i-1][j]+1]);
        }
    /// @returns: the maximum number of disjoint segments in [@cl, @cr]
    const auto calc = [&](int cl, int cr) {
        if (cl > cr) return 0;
        int res = 0;
        for (int i=t-1;i>=0;--i)
            if (f[i][cl] <= cr) cl = f[i][cl]+1, res += 1<<i;
        return res;
    };
    int tot = calc(0, m-1), cnt = 0;
    if (tot < k) {
        cout << "-1\n";
        return 0;
    }
    set<Interval> s;
    s.emplace(0, m-1);
    for (int i=0;i<n;++i) {
        if (cnt == k) break;
        auto it = s.lower_bound(Interval(l[i], r[i]));
        if (it == s.end()) continue;
        const auto [cl, cr] = *it;
        if (cl > l[i] || cr < r[i]) continue;
        int dlt = calc(cl, l[i]-1)+calc(r[i]+1, cr)-calc(cl, cr)+1;
        if (tot+dlt >= k) {
            cnt++;
            tot += dlt;
            s.erase(it);
            if (cl < l[i]) s.emplace(cl, l[i]-1);
            if (cr > r[i]) s.emplace(r[i]+1, cr);
            cout << i+1 << "\n";
        }
    }
}

/ 寒星坠地 白鸟飞去 /

/ 千宵灯火转眼便览尽 /

—— COP - 光与影的对白 ft. 洛天依

Desc.

Link.

给出一棵大小为 $n$ 的树, 记 $f(S)$ 表示, 令点集 $S$ 中的点的点权为 $1$, 其余为 $0$, 树的带权重心数量. 求 $\max_{|S| = i}$.

Sol.

由带权重心的性质, 所有重心处于树上的同一条路径. 且该路径上不存在带权点 (就本题而言). 则路径的两端点的带权子树大小相等且等于 $\frac{|S|}2$.

那么使用点分治, 对于当前分治子树的后继节点的子树, 求出 $w_s$ 表示该后继节点子树中子树所有大小为 $s$ 的节点中到当前分治重心的距离的最大值, $pre_s$ 表示 $w_s$ 在处理过的子树中的前大值. 但注意, 带权点并没有确定, 因此我们并不能直接用 $w$ 和 $pre$ 来求答案.

上述问题的解决方法其实很简单, 只要对 $w_s$ 做个后缀 $\max$ 即可, 即在 $\geqslant \frac{|S|}2$ 的子树中找最优解.

int n, sz[200100], w[200100], pre[200100], ans[200100];
bool rm[200100];
vi grp[200100];
int root(int u, int Fu, int all) {
    int r = n, ok = sz[u] = 1;
    for (int v : grp[u])
        if (v != Fu && !rm[v]) {
            int rs = root(v, u, all);
            if (rs < n) r = rs;
            if (sz[v]*2 > all) ok = 0;
            sz[u] += sz[v];
        }
    return ok && ((all-sz[u])*2 <= all) ? u : r;
}
void dfs(int u, int Fu, int d) {
    sz[u] = 1;
    for (int v : grp[u])
        if (v != Fu && !rm[v]) dfs(v, u, d+1), sz[u] += sz[v];
    chkmax(w[sz[u]], d);
}
void solve(int u) {
    rm[u] = 1;
    int h = 0;
    reverse(allu(grp[u]));
    for (int v : grp[u])
        if (!rm[v]) {
            dfs(v, u, 1);
            for (int i=sz[v];i>=1;--i) chkmax(w[i], w[i+1]), chkmax(ans[i*2], w[i]+pre[i]);
            for (int i=1;i<=sz[v];++i) chkmax(pre[i], w[i]), w[i] = 0;
            chkmax(h, sz[v]);
        }
    for (int i=1;i<=h;++i) pre[i] = 0;
    for (int v : grp[u])
        if (!rm[v]) solve(root(v, n, sz[v]));
}
int main() {
    ios::sync_with_stdio(0);
    IN.tie(nullptr);
    IN > n;
    for (int i=1,u,v;i<n;++i) {
        IN > u > v; u--; v--;
        grp[u].pb(v), grp[v].pb(u);
    }
    solve(root(0, n, n));
    transform(ans+1, ans+n+1, ans+1, [&](int x) { return x+1; });
    copy_n(ans+1, n, ostream_iterator<int>{cout, "\n"});
}

/ 我們這些沙沙作響的葉子有回應風暴的聲音,而你是誰,如此安靜? /

/ 我不過是一朵花。 /

/ "We, the rustling leaves, have a voice that answers the storms, but who are you so silent?" /

/ "I am a mere flower." /

—— Rabindranath Tagore - Stray Birds