做题乱记

P3177

tags:树形 dp

对于每个子树,维护的是该子树内的所有边对答案做出的贡献。

\(f_{v,q}\) 转移到 \(f_{u,p}\),那么这条边权为 \(w\) 被经过的次数就是 \(q\times (k-q)+(siz_v-q)\times(n-k-(siz_v-q))\),乘上 \(w\) 进行转移即可。

记得上下界优化以及合法性的检测。

void dfs (int u, int fa) {
    siz[u] = 1;
    for (auto [v, w] : gra[u]) {
        if (v == fa || siz[v]) continue;
        dfs (v, u); siz[u] += siz[v];
        for (int j = min (siz[u], k); ~j; j --) 
            for (int p = max (0, j - siz[u] + siz[v]); p <= min (siz[v], j); p ++) 
                f[u][j] = max (f[u][j], f[u][j - p] 
                                    + f[v][p] + 
                                    w * 1ll * ( 1ll * (siz[v] - p) * (n - k - siz[v] + p) + p * 1ll * (k - p) ) );  
    }
}

CF486D Vaild Sets

tags: 计数、树形dp

一眼秒了。

考虑一个计数顺序:枚举每个点作为最小值,计算大于等于这个值的方案。当一个点存在集合中且点权与最小值相同时,当且仅当其编号大于初始点。

void dfs (int u, int fa, int root) {
    f[u] = 1;
    for (auto v : g[u]) {
        if (v == fa) continue;
        if ((a[v] > a[root] || a[v] == a[root] && v > root) && a[v] - a[root] <= d) 
            dfs (v, u, root),
            fprintf (stderr, "%d -> %d availible\n", u, v),
            f[u] = f[u] * (f[v] + 1) % p;
    }
    if (u == root) ans = (ans + :: f[u]) % p; 
}
 
int main (void) {
 
    scanf ("%d%d", &d, &n);
    rep (i, 1, n) scanf ("%d", a + i);
    rep (i, 1, n - 1) {
        int u, v; scanf ("%d%d", &u, &v);
        g[u].push_back (v), g[v].push_back (u);
    }
 
    for (int i = 1; i <= n; i ++) dfs (i, -1, i);
 
    printf ("%lld\n", ans);
 
    return 0;
}

P5666 [CSP-S2019] 树的重心

tags: 树的重心、乱搞

Data: \(n\le 3\times10^5\)

考虑每个点作为重心的次数 \(f_i\)。不妨以原树的重心 \(g\) 为根,那么一个点在被原树被分割后为重心,分割的边必然不在其子树内。

假设当前点 \(u\not=g\) 的重儿子大小为 \(k\)\(siz_u=s\)。一条符合条件的边连向子节点的子树大小为 \(x\)
那么 \(k\) 必然不大于新树节点个数的一半,即

\[k\le\dfrac{n-x}2\iff x\le n-2k \]

同时,\(u\) 的新子树(即与父节点相连)的节点个数不超过新节点个数的一半,即

\[n-s-x\le\dfrac{n-x}{2}\iff x\ge n-2s \]

于是,一个充要条件就是:

\[n-2s\le x\le n-2k \]

下面讨论 \(u=g\) 的情况。

\(g\) 的重儿子大小为 \(S_g\),次重儿子(并不严格)大小为 \(S_p\),割掉的边失去的子树大小为 \(q\)
如果这条边在重儿子上,那么 \(S_g-q\)\(S_p\) 需要小于新树的大小 \(n'=n-q\) 的一半,即:

\[S_g-q\le\dfrac{n-q}2\iff q\ge 2S_g-n\\S_p\le\dfrac{n-q}2\iff q\le n-2S_p \]

也就是:

\[2S_g-n\le q\le n-2S_p \]

如果不在重儿子上,那么只需使新树大小的一半不小于 \(S_g\),即:

\[S_g\le \dfrac{n-q}2\iff q\le n-2S_g \]

综上,用树状数组维护即可。

由于我比较唐,不会什么精妙的维护方法,索性就分类讨论。

  • 对于在 \(g\to u\) 路径上的点 \(v\)\(siz\)\(n-siz_v\),这个部分可以直接 dfs 跑一遍维护,设这个值为 \(c_{1,v}\)
  • 对于其他点(不在子树内,不在 \(g\to u\) 的路径上),\(siz\)\(siz_v\),记这个值为 \(c_{2,v}\)

对于 \(u\),作为重心次数就是 \(c_{1,u}+c_{2,u}\)
但是第二个不好直接维护啊!
那就用容斥的思想!

我们先把所有点的 \(siz=siz_v\) 求出当作初始系数。减掉子树内这样的点,这是好维护的,将计算子树后减去子树之前的查询结果就行。这样还会多算 \(g\to u\)\(siz=siz_v\),和第一个方法一样减去就好。

void dfs1 (int u, int fa) {
    siz[u] = 1;
    for (auto v : gra[u]) {
        if (v == fa) continue;
        dfs1 (v, u); siz[u] += siz[v];
        mxsiz[u] = max (mxsiz[u], siz[v]);
    }
    mxsiz[u] = max (mxsiz[u], n - siz[u]);
    if (mxsiz[u] < mxsiz[g]) g = u;
}

void dfs (int u) {
    mxsiz[u] = 0; siz[u] = 1;
    dfn[u] = ++ tim;
    for (auto v : gra[u]) {
        if (v == fa[u]) continue;
        fa[v] = u;
        dfs (v); siz[u] += siz[v];
        if (siz[v] >= mxsiz[u]) {
            pmxsiz[u] = mxsiz[u];
            mxsiz[u] = siz[v];
            son[u] = v;
        } else pmxsiz[u] = max (pmxsiz[u], siz[v]);
    }
}

long long ans = 0;

void del_subtree (int u) {
    for (auto v : gra[u]) {
        if (v == fa[u]) continue ;
        coef[v] = t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
        del_subtree (v);
        coef[v] -= t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
        t.add (siz[v], 1);
    }
}

void add_rtt (int u) {
    for (auto v : gra[u]) {
        if (v == fa[u]) continue;
        t.add (n - siz[v], 1);
        coef[v] += t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
        add_rtt (v);
        t.add (n - siz[v], -1);
    }
}

void del_vrtt (int u) {
    for (auto v : gra[u]) {
        if (v == fa[u]) continue;
        t.add (siz[v], 1);
        coef[v] -= t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
        del_vrtt (v);
        t.add (siz[v], -1);
    }
}

void add_all (int u) {
    for (auto v : gra[u]) {
        if (v == fa[u]) continue;
        t.add (siz[v], 1);
        add_all (v);
    }
}

void get_son (int u) {
    if (u == g) {
        int v = son[u];
        if (siz[v] <= n - 2 * pmxsiz[g]) ans += g;
        get_son (v);
        return ;
    }
    for (auto v : gra[u]) {
        if (v == fa[u]) continue;
        if (siz[v] <= n - 2 * pmxsiz[g]) ans += g;
        get_son (v);
    }
}

void get_other (int u) {
    for (auto v : gra[u]) {
        if (v == fa[u] || v == son[g]) continue;
        if (siz[v] <= n - 2 * mxsiz[g]) ans += g;
        get_other (v);
    }
}

void work () {
    init (); ans = 0;
    scanf ("%d", &n);
    for (int i = 1; i < n; i ++) {
        int u, v; scanf ("%d %d", &u, &v);
        gra[u].push_back (v), gra[v].push_back (u);
    }
    dfs1 (1, 0); 
    dfs (g); t.init (n);
    del_subtree (g); t.init (n); add_rtt (g);
    del_vrtt (g); 
    add_all (g);
    rep (i, 1, n) {
        if (i == g) continue;
        coef[i] += t.sum (n - 2 * siz[i], n - 2 * mxsiz[i]);
        ans += coef[i] * 1ll * i;
    }
    get_son (g); 
    get_other (g);
    
    printf ("%lld\n", ans);
}   


P4219 [BJOI2014] 大融合

tags: 树剖、离线思想

把问答离线下来,然后就能把树的形态刻画出来。
跑一遍 dfs,就能确定祖孙关系。
对于询问而言,实际上就是动态维护每个点子树大小。把询问逆向,每次加边相当于在树中删边,这是可以用树剖维护的。答案就是一端的 \(siz\) 乘上另一端的 \(siz\)

再加一下细节。
要维护每个点当前的根,但是修改的时候发现修改子树会导致已经不在当前树内的还被修改。
注意一点:删边必然会导致当前子树根的深度增大,因此对于根的修改取一下最大值即可。

时间复杂度感觉还行,因为你查询根之类是 \(\mathcal O(\log n)\) 级别的,一次查询调用次数比较少。瓶颈还是在树剖上,所以复杂度是 \(\mathcal O(n\log^2n)\)

#include <array>
#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int n, q;
vector <int> g[N];
vector < array <int, 3> > qry;
int siz[N], fa[N], son[N], top[N], dep[N], id[N], tim[N], Time, rt[N];

void dfs (int u) {
    siz[u] = 1; 
    dep[u] = dep[ fa[u] ] + 1;
    for (auto v : g[u]) {
        if (fa[u] == v) continue;
        fa[v] = u; rt[v] = rt[u];
        dfs (v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]]) son[u] = v;
    }
}

void dfs (int u, int tp) {
    tim[u] = ++ Time; id[Time] = u;
    top[u] = tp;
    if (son[u]) dfs (son[u], tp);

    for (auto v : g[u]) if (!tim[v]) dfs (v, v);
}

#define ls (u << 1)
#define rs (u << 1 | 1)
#define mid (l + r >> 1)

int sum[N << 2], root[N << 2], tag[N << 2], rtag[N << 2];

inline int comp (int u, int v) { if (dep[u] > dep[v]) return u; return v; }
void upd (int u, int k) { sum[u] += k; tag[u] += k;  }
void updrt (int u, int rt) { root[u] = comp (root[u], rt); rtag[u] = root[u];  }
void pushdown (int u) { int &v = tag[u]; int &rt = rtag[u]; upd (ls, v), upd (rs, v); updrt (ls, rt), updrt (rs, rt); v = rt = 0;  }

void modify (int l, int r, int ql, int qr, int k, int u = 1) {
    if (ql > r || l > qr) return ;
    if (l >= ql && r <= qr) return upd (u, k);
    pushdown (u);
    modify (l, mid, ql, qr, k, ls); 
    modify (mid + 1, r, ql, qr, k, rs);
}

void rt_modify (int ql, int qr, int k, int l = 1, int r = n, int u = 1) {
    if (ql > r || l > qr) return ;
    if (l >= ql && r <= qr) return updrt (u, k);
    pushdown (u);
    rt_modify (ql, qr, k, l, mid, ls);
    rt_modify (ql, qr, k, 1 + mid, r, rs);
}

int query (int l, int r, int q, int u = 1) {
    if (l == r) return sum[u];
    pushdown (u);
    if (q <= mid) return query (l, mid, q, ls);
    else          return query (mid + 1, r, q, rs);
}

int getRt (int q, int l = 1, int r = n, int u = 1) {
    if (l == r) return root[u]; 
    pushdown (u);
    if (q <= mid) return getRt (q, l, mid, ls);
    return getRt (q, mid + 1, r, rs);
}

void path_modify (int u, int v, int k) {
    while (top[u] != top[v]) {
        if (dep[ top[u] ] < dep[ top[v] ]) swap (u, v);
        modify (1, n, tim[ top[u] ], tim[u], k);
        u = fa[ top[u] ];
    }
    if (dep[u] > dep[v]) swap (u, v);
    modify (1, n, tim[u], tim[v], k);
}

void test_Output () {
    for (int i = 1; i <= n; i ++) {
        fprintf (stderr, "%d: siz = %d, root = %d\n", i, query (1, n, tim[i]), getRt (tim[i]));
    }
}

int main (void) { dep[0] = -1;

    scanf ("%d%d\n", &n, &q);
    for (int i = 1; i <= q; i ++) {
        char opt[10]; int a, b;
        scanf ("%s%d%d\n", opt + 1, &a, &b);
        qry.push_back ({opt[1] == 'A', a, b});
        if (opt[1] == 'A') g[a].push_back (b), g[b].push_back (a);
    }   

    for (int i = 1; i <= n; i ++) if (!fa[i]) { rt[i] = i; dfs (i); dfs (i, i);}
    for (int i = 1; i <= n; i ++) modify (1, n, tim[i], tim[i], siz[i]), rt_modify (tim[i], tim[i], rt[i]); 


    reverse (qry.begin (), qry.end ()); vector <long long> ans;
    for (auto [typ, u, v] : qry) {
        if (typ == 1) {
            if (dep[u] > dep[v]) swap (u, v);
            path_modify (getRt (tim[v]), u, -query (1, n, tim[v]));
            rt_modify (tim[v], tim[v] + siz[v] - 1, v);
        } else {
            if (dep[u] > dep[v]) swap (u, v);
            int rt = getRt (tim[u]), 
                d1 = query (1, n, tim[rt]) - query (1, n, tim[v]),
                d2 = query (1, n, tim[v]);
            ans.push_back (d1 * 1ll * d2);
        }
    }   

    reverse (ans.begin (), ans.end ());
    for (auto as : ans) printf ("%lld\n", as);

    return 0;
}

P4216 [SCOI2015] 情报传递

tags: 树剖、离线思想、主席树

在线:实际上是动态查询一段上值小于 \(C\) 的个数,在主席树上维护就做完了!

离线:另一种想法是,把询问离线下来,那么每个询问都有一个时间限制,这个时间限制实质上就是形如 \(c\) 时刻前开始的都可以被统计。我们按照这个方法排序,然后就发现可以直接做了。随便什么简单的数据结构维护一下就好。

正常做是树剖,\(\mathcal O(n\log^2n)\)。但可以维护这样的:\(s[i]\) 表示 \(i\) 到根有多少需要统计的点。修改的话就是对 \(u\) 的整个子树加,查询就是做一次差分。可以做到 \(\mathcal O(n\log n)\)

你应该在九月八日在 15min 以内完成这道题。

int main (void) {

    scanf ("%d", &n);
    for (int i = 1; i <= n; i ++) {
        scanf ("%d", &par[i][0]);
        if (!par[i][0]) root = i;
        else g[ par[i][0] ].push_back (i);
    } dfs (root);

    int q; scanf ("%d", &q);
    for (int i = 1; i <= q; i ++) {
        int k; scanf ("%d", &k); ans[i] = -1;
        int x, y, c;
        if (k == 1) scanf ("%d%d%d", &x, &y, &c), qry.push_back ({i - c, x, y, i});
        else        scanf ("%d", &x), qry.push_back ({i, x, 0, i});
    } t.init (n);

    sort (qry.begin (), qry.end (), [&](auto x, auto y){ return x[0] == y[0] ? x[2] > y[2] : x[0] < y[0]; });

    for (auto [c, a, b, id] : qry) {
        if (b) {
            int _lca = lca (a, b);
            ans[id] = t.sum (dfn[a]) + t.sum (dfn[b]) - t.sum (dfn[ par[_lca][0] ]) - t.sum (dfn[_lca]);
            ans1[id] = dep[a] + dep[b] - dep[_lca] - dep[ par[_lca][0] ];
        } else t.add (dfn[a], 1), t.add (dfn[a] + siz[a], -1);
    }   

    for (int i = 1; i <= q; i ++)
        if (ans[i] != -1) printf ("%d %d\n", ans1[i], ans[i]);

    return 0;
}

P5773 [JSOI2016] 轻重路径

tags: 树的重心、倍增

更加详实

神仙题啊!

一次修改只会影响到当前叶子节点到根的这一条链。

考虑这条链上可能被修改的点。假设当前子树 \(rt\) 的大小为 \(siz_{rt}\),那么 \(rt\) 的后代 \(u\) 可能变为轻儿子必须满足 \(siz_{u}\le \dfrac{siz_{rt}}2\)。倍增找 \(siz_u\)\(u\),判断是否改变。

由于每次 \(siz\) 至少减半,因此计算了至多 \(\log n\) 次。

void dfs (int u) {
    dep[u] = dep[ par[u][0] ] + 1;
    L[u] = dfn[u] = ++ tim;
    id[tim] = u;
    siz[u] = 1;
    for (int i = 1; i < 19; i ++) par[u][i] = par[ par[u][i - 1] ][i - 1];
    for (auto v : son[u]) 
        if (v) par[v][0] = u, dfs (v), siz[u] += siz[v];
    if (siz[ son[u][1] ] > siz[ son[u][0] ]) wson[u] = son[u][1];
    else                                     wson[u] = son[u][0];     
    ans += wson[u];              
    R[u] = tim;
}

int Size (int u) {
    if (!u) return 0;
    return t.sum (dfn[u], dfn[u] + siz[u] - 1);
}

int brot (int u) {
    int f = par[u][0];
    return son[f][!cha[u]];
}

int main (void) {

    scanf ("%d", &n); t.init (n); memset (cha, -1, sizeof cha);
    for (int i = 1; i <= n; i ++) scanf ("%d%d", &son[i][0], &son[i][1]), cha[ son[i][0] ] = 0, cha[ son[i][1] ] = 1, t.add (i, 1);
    dfs (1); printf ("%lld\n", ans);
    
    scanf ("%d", &q); 
    while (q --) {
        int u, rt = 1; scanf ("%d", &u);
        t.add (dfn[u], -1); 
        int l = u;
        while (rt != l) {
            u = l;
            for (int i = 18; ~i; i --)
                if (dep[ par[u][i] ] > dep[rt] && Size (par[u][i]) <= Size (rt) / 2) u = par[u][i];
            int bro = brot (u), fa = par[u][0];
            if (wson[fa] == u) {
                int siu = Size (u), sib = Size (bro);
                if (siu < sib) wson[fa] = bro, ans += bro - u;
                if (!siu && !sib) ans -= u, wson[fa] = 0;
            } 
            rt = u;
        }
        printf ("%lld\n", ans);
    }

    return 0;
}

CF1528C Trees of Tranquillity

tags: dfs 序,set 应用,贪心

神仙题啊!

由于 \(i>fa_i\),所以可以将 Keshi 树映射到 dfs 序上。这样,点 \(u\) 管辖的范围 \(rg_u\)\([dfn_u,dfn_u+siz_u-1]\)。对于 Soroush 树上一条链上的两点 \(i<j\),若 \(i,j\) 可以同时选,当且仅当 \(rg_i\)\(rg_j\) 无交。在 dfs Soroush 树时,可以直接从父节点继承选了的点集,进行分类讨论:

  • 若当前点覆盖了已有的点,当前点不选。
  • 若有点覆盖了当前点,删除那个点。
    这样保证了单调性,且管辖区间范围逐渐缩小,所以贪心是正确的。

具体实现可以用 set。

void dfs (const vector <int>* g, int u) {
    l[u] = ++ tim;
    for (auto v : g[u]) dfs (g, v);
    r[u] = tim;
}
 
set < pair <int, int> > s;
 
void dfs2 (const vector <int>* g, int u) {
    auto it = s.lower_bound ({l[u], r[u]});
    bool flag = false;
    pair <int, int> del = {0, 0};
    if (it == s.end ()) {
        if (s.empty ())
            s.insert ({l[u], r[u]}), flag = true;
        else {
            it --;
            if (it -> second < l[u]) s.insert ({l[u], r[u]}), flag = true;
            else if (it -> second >= r[u]) {
                del = *it; s.erase (del); flag = true;
                s.insert ({l[u], r[u]});
            }
        }
    }
    else {
        if (it == s.begin ()) {
            if (r[u] < it -> first)
                s.insert ({l[u], r[u]}),
                flag = true;
        } else {
            auto p = it; p --;
            if (p -> second < l[u]) s.insert ({l[u], r[u]}), flag = true;
            else if (p -> second >= r[u]) {
                del = *p; s.erase (del);
                flag = true;
                s.insert ({l[u], r[u]});
            }
        }
    }
    ans = max (ans, (int)s.size ());
    for (auto v : g[u]) dfs2 (g, v);
    if (flag) s.erase ({l[u], r[u]});
    if (del.first != 0) s.insert (del);
}
 
void work () {
    init ();
    scanf ("%d", &n);
    for (int i = 2; i <= n; i ++) scanf ("%d", f1 + i), g1[ f1[i] ].push_back (i);
    for (int i = 2; i <= n; i ++) scanf ("%d", f2 + i), g2[ f2[i] ].push_back (i);
    dfs (g2, 1);
    dfs2 (g1, 1);
    printf ("%d\n", ans);
}
 

CF2063E Triangle Tree

tags:推式子、LCA

假设两边长度为 \(a,b\ \ (a\le b)\),那么 \(x\in(b-a,a+b)\),也就是有 \(2a-1\) 个选择。

把这个东西放在树上,假设 \(u,v\) 两点 LCA 为 \(l\)。那么就是有 \(2\min(dep_u,dep_{v})-2dep_l-1\) 个选择。考虑每个点 \(u\) 作为 LCA 的贡献,对于在同一个子节点内的子树的重复统计,我们额外记录总共有多少个这样的对。将 \(dep\) 从小到大排序,假设子树大小为 \(n\),使 \(dep_i\) 被计算的有 \(n-i\) 个。

好像明白了。继续拆贡献,由于 \(2\min(a,b)=a+b-\lvert a-b \rvert\),于是对于两点 \(u,v\) 的贡献 \(f(u,v)=dep_u+dep_v-\lvert dep_u-dep_v \rvert-2dep_{\operatorname{lca}(u,v)}-1\)。发现这个是十分好处理的。对于 \(dep_u+dep_v-2dep_{\operatorname{lca}(u,v)}\),把每个点当作 LCA 的贡献算一下,维护每个子树的 \(dep\)\(s_u\),那么贡献就是 \(\dfrac12\sum\limits_{v\in son_u}s_v\times(siz_u-siz_v)\)。对于 \(\lvert dep_u-dep_v\rvert\),这也是好算的。在求出每个点的深度之后,将整个数组从小到大排序,维护后缀和。对于 \(-1\),和第一个是一样的。

然后就做完了。

void dfs (int u) {
    siz[u] = 1; ++ :: cnt[dep[u] = dep[ fa[u] ] + 1];
    ll cnt = 0;
    ans += (n - 1) * 1ll * dep[u];
    for (auto v : g[u]) {
        if (v == fa[u]) continue;
        fa[v] = u;
        dfs (v); 
        cnt += siz[v] * 1ll * (siz[u] - 1);
        siz[u] += siz[v];
    }
    ans -= 2 * (cnt + siz[u] - 1) * dep[u],
    ans += siz[u] - 1;
}
 
void work () {
    init ();
    scanf ("%d", &n);
    for (int i = 1; i < n; i ++) {
        int u, v; scanf ("%d%d", &u, &v);
        g[u].push_back (v); g[v].push_back (u);
    }
    dfs (1);
    ll suf = 0;
    for (int i = 1; i <= n; suf += cnt[i ++]) ans -= i * 1ll * cnt[i] * (suf - n + cnt[i] + suf); 
    printf ("%lld\n", ans - n * 1ll * (n - 1) / 2);
}

[ARC088F] Christmas Tree

tags:树形dp、二分、set 的应用

考虑 A 怎么做。

\(f(x)\) 表示以 \(u\) 为根的子树的答案,且向 \(fa_x\) 连了一条边。若有偶数个子节点,那么 \(f(x)=\sum\limits_{v\in son_x}f(v)-\dfrac{\lvert son_x\rvert}2+1\);若有奇数个子节点,那么 \(f(x)=\sum\limits_{v\in son_x}f(v)-\lfloor\dfrac{\lvert son_x\rvert}{2}\rfloor\),对于根节点特判一下即可。

接下来考虑 B 怎么算。注意到答案单调,因此考虑二分。观察 A 的做法,类似的进行判断即可。

复杂度的瓶颈是在算 B,为 \(\mathcal O(n\log^2 n)\)

P6327 区间加区间 sin 和

tags:基本数据结构

思维题做多了,来点好玩的 DS。

由于 \(\sin (x+v)=\sin x\cos v+\cos x\sin v\)\(\cos(x+v)=\cos x\cos v-\sin x\sin y\),因此维护区间 \(\sin x\) 以及 \(\cos x\) 和就做完了!

P8511 [Ynoi Easy Round 2021] TEST_68

tags: 01Trie,dfs 序

由于 dfs 序的性质,在枚举到一个点时其子树必然未被枚举。因此维护一个全局变量 \(x\) 表示答案,每搜到一个点就加入并在 Trie 上找与其匹配的最大值,更新答案。但这样只能算出 \(j\le i\) 的所有点与其匹配的答案,考虑修正这一答案。

发现一个性质:设全局最大的点对是 \((x,y)\),那么被影响到的只有 \(1\to x\) 以及 \(1\to y\),其余子树答案都为 \(a_x\oplus a_y\)。考虑如何计算 \(1\to x\) 以及 \(1\to y\) 上的答案。

这是很容易实现的,类比我们求只包含 \(j\le i\) 点的方法,将链之外的所有点权插入 Trie,从 \(1\)\(x\) 依次插入点权并查询。这样就可以 \(\mathcal O(n\log n)\) 地做完这道题。

P3592 [POI 2015] MYJ

tags: 区间 DP

\(f(l,r,x)\) 为包含于 \([l,r]\) 中的所有顾客,且区间中最小价格不小于 \(x\) 的最大总和,那么枚举间断点 \(k\in(l,r)\),以及权值 \(y\)。这里注意到所有商店的值取顾客的 \(c_i\) 是最优的,因此最多只有 \(4000\) 个不同的值。于是有转移式:

\[f(l,r,x)=\max\limits_{k} \{f(l,k-1,x)+f(k+1,r,x)+x\cdot cnt(l,r,x)-x\cdot cnt(l,k-1,x)-x\cdot cnt(k+1,r,x)\} \]

注意到这个是可以从大往小继承的,因此再与 \(f(l,r,x+1)\)\(\max\) 即可。

\(cnt(l,r,x)\) 表示 \([l,r]\)\(c_i\) 值不小于 \(x\) 的顾客个数,注意我们只需计算过 \(k\) 的部分,因此要减掉不过 \(k\) 的答案。

时间复杂度 \(\mathcal O(n^3m)\)

P5336 [THUSC 2016] 成绩单

tags: 区间 DP

\(f(l,r,mn,mx)\) 区间 \([l,r]\) 内,没选的最大值为 \(mx\),最小值为 \(mn\) 的答案。设 \(g(l,r)\)\([l,r]\) 全选完的答案。考虑如何从这个状态推出其他状态。

位置在 \(l\) 上的成绩单可能被取走,或者没有被取走,根据这一点进行分类讨论:

  • \(l\) 未被取走,那么 \(f(l,r,\min(mn,a_l),\max(mx,a_l))\gets f(l+1,r,mn,mx)\)
  • \(l\) 被取走,那么可以将区间划分为全部被取走的 \([l,k]\) 部分以及剩下没有取走均在 \([k+1,r]\) 的部分。
    • 因此,有转移式:\(f(l,r,mn,mx)=\min\limits_{k\in[l,r)}\{g(l,k)+f(k+1,r,mn,mx)\}\)

\(g\) 的转移式也是显然的,即 \(g(l,r)=\min\{f(l,r,mn,mx)+a+b\times(mx-mn)^2\}\)

对于不合法情况,应该设为 \(+\infty\)

P6764 [APIO2020] 粉刷墙壁

tags: dp, greedy

题面实在太长/tuu!

一开始读错了题,以为是任意一个请求都会接受,遇到无效情况不刷即可。但发现实际上不是这样。

看一个性质:\(\sum f(x)^2\le 4\times 10^5\)
那么由 Cauthy 不等式有:

\[(\sum f(x)^2)(\sum 1)\ge (\sum f(x))^2 \]

因此 \((\sum f(x))^2\le 4\times 10^5\times 10^5\iff \sum f(x)\le 2\times 10^5\),也就是说开发商喜欢颜色个数之和最多也只有 \(2\times 10^5\)

那么我们的任务就是对于墙壁的每一段 \(i\),是否能向后(包含 \(i\))刷 \(M\) 段。这一步考虑 DP。设 \(f(i,j)\) 为第 \(j\) 个承包商从 \(i\) 开始刷,能刷的最大距离。有转移式:

\[f(i,j)=\begin{cases}f(i+1,j+1)+1,&i\text{ like }col_j.\\0,&i\text{ doesn't like }col_j.\end{cases} \]

只要 \(f(i,j)\ge M\),那么就可以从 \(i\) 刷。

接下来是一个贪心,从 \(N-M+1\) 作为起始点,然后一直往最远的合法点跳即可。

为什么不要环形处理?
假设答案存在刷 \(j\in (i-M+1,n]\) 的,那么如果存在可行解,必然前一线段 \(r>i-M+1\)。而每个开发商的后继是固定的,因此必然在 \(k\le i-M+1\) 包含了 \(j\) 的情况。

CF1111D Destroy the Colony

tags: combinatorics, math, dp

题意简述:给出长度为 \(2n\) 的有重复元素的字符串 \(s\),当一种字符都在 \([1,n]\) 或者都在 \([n+1,2n]\) 位置上时称其为合法,你可以对 \(s\) 重排列。\(Q\le 10^5\) 次询问,每次询问给出 \(x,y\),问 \(s_x\)\(s_y\) 的字符在同一侧时,合法排列数。

实际上是对集合的划分。设字符集为 \(U\),字符 \(x\) 出现的次数为 \(c_x\)。那么当其合法,必然存在至少一个集合 \(S\subset U\),使:

\[\sum\limits_{x\in S}c_x=n \]

考虑一个合法子集 \(S\) 对答案的贡献,也就是存在相同元素的排列问题。先将所有元素视为不同,再固定除 \(x\) 字符不同的元素不动,将 \(x\) 字符重排列,这部分也就是多算的,需要用除法原理去重。即:

\[\dfrac{n!}{\prod\limits_{x\in S} c_x!} \]

那么剩下的字符集 \(T=U-S\),和是 \(n\)。这一部分对答案的贡献是:

\[\dfrac{n!}{\prod\limits_{x\in T} c_x!} \]

二者相互独立,因此总方案为:

\[\dfrac{(n!)^2}{\prod\limits_{x\in S}c_x!} \]

所以任意一个合法子集 \(S\subset U\),对答案的贡献都是相同的。于是我们只需用背包统计满足上述条件的集合 \(S\) 个数,再乘上上述的系数和 \(2\)(这是因为可以颠倒),即为没有限制下合法排列总数。

对于 \(Q\) 次询问,实际上有效的仅有 \(52^2\) 个。考虑一对 \(x,y\),我们实际上就是固定这两个数。换句话说,这等价于在删掉 \(x,y\) 的集合 \(U'\) 中,合法排列的个数。枚举每一个 \(x\),强制不选它。设 \(f(j)\) 为容量为 \(j\) 的背包,不选 \(i\) 的方案数。那么对于 \(x=y\) 的答案,即为 \(f(n)\)。对于 \(x\not=y\) 的答案,可以考虑退背包的做法。总的复杂度是 \(\mathcal O(k^2n)\)\(k\) 为字符集的大小。



    int up = 1e5;
    fac[0] = 1; for (int i = 1; i <= up; i ++) fac[i] = fac[i - 1] * 1ll * i % p;
    ifac[up] = qpow (fac[up], p - 2); for (int i = up - 1; ~i; i --) ifac[i] = ifac[i + 1] * 1ll * (i + 1) % p;

    scanf ("%s", s + 1); n = strlen (s + 1); n >>= 1; coef = fac[n] * 1ll * fac[n] % p;
    for (int i = 1; i <= (n << 1); i ++) cnt[ getNum (s[i]) ] ++;
    for (int i = 1; i <= 52; i ++) {
        coef = coef * 1ll * ifac[ cnt[i] ] % p;
        // if (cnt[i]) fprintf (stderr, "cnt[%d] = %d\n", i, cnt[i]);
    }
    // fprintf (stderr, "initial = %d\n", coef);
    for (int i = 1; i <= 52; i ++) {
        if (!cnt[i]) continue;
        f[i][0] = 1;
        for (int j = 1; j <= 52; j ++) {
            if (j == i || !cnt[j]) continue;
            for (int k = n; k >= cnt[j]; k --)
                add (f[i][k], f[i][k - cnt[j]]);
        }
        ans[i][i] = f[i][n];
        // fprintf (stderr, "Answer without %d = %d\n", i, f[i][n]);
        g[0] = 1;
        for (int j = i + 1; j <= 52; j ++) {
            if (!cnt[j]) continue;
            for (int k = 1; k <= n; k ++)
                if (k >= cnt[j]) g[k] = minuss (f[i][k], g[k - cnt[j]]);
                else             g[k] = f[i][k];
            ans[j][i] = ans[i][j] = g[n];
            // fprintf (stderr, "(%d, %d) : %d\n", i, j, g[n]);
        }
    }

    scanf ("%d", &q);
    while (q --) {
        int x, y; scanf ("%d%d", &x, &y);
        printf ("%d\n", ans[getNum (s[x])][getNum (s[y])] * 1ll * coef * 2 % p);
    }
posted @ 2025-09-08 18:08  Toorean  阅读(5)  评论(0)    收藏  举报