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

分类 笔记 下的文章


date: '2023-10-15'
title: 'Solution -「JOISC 2022」コピー & ペースト 3'


Desc.

Link.

你有两个字符串 $S$ 和 $T$, 初始为空. 每次你可以进行以下三种操作中的一种:

  1. 选定小写字母 $c$, 令 $S := S+c$;
  2. 令 $T := S$, 令 $S:=\texttt{""}$;
  3. 令 $S := S+T$.

三种操作分别花费 $A, B, C$, 现要求你将 $S$ 转化为目标串 $X$, 求最小花费.

$1\leqslant |X| \leqslant 2.5\times 10^{3}$, $\Sigma = \{\texttt{a}, \dots, \texttt{z}\}$.

Sol.

今天真调了一天 RE, 人麻了. 😅

考虑区间 DP, 设 $f_{l, r}$ 表示达成 $X[l, r)$ 的最小花费. 操作 1 的转移很简单:

$$ f_{l, r}+A \rightarrow f_{l-1, r} \\ f_{l, r}+A \rightarrow f_{l, r+1} $$

其中 $\rightarrow$ 表示更新, 即取最小值. 可以发现操作 2 比较有性质, 因为每次执行操作 2 都可以「刷新」当前字符串的状态. 设当前这次操作 2 后, $T$ 变成了 $Y$, 那么后面若干次操作中, 我们会使用操作 2&3, 来使 $S$ 变成类似 $\texttt{...}Y\texttt{...}YY\texttt{...}Y\texttt{...}$ 的形状, 其中省略号代表操作 1 插入的字母. 所以如果考虑从 $f_{l, r}$ 转移到 $f_{x, y}$ ($x \leqslant l <r \leqslant y$), 那么有 $X[x, x+r-l)=X_[y-r+l, y) = X[l, r)$. 并且这三个部分无交. 我们可以这样钦定 $X[x, y)$ 的两端一定等于 $X[l, r)$ 的原因是, 上述形式两边多出来的字母可以通过第一种操作转移到.

具体转移的路径是从 $f_{l, r}$ 转移到 $f_{i, r}$, 其中 $X[i, i+r-l) = X[l, r]$ 且 $i+r-l \leqslant l$. 令 $cnt$ 表示 $X[i, r)$ 中最多可选出的子串个数, 使得这些子串都等于 $X[l, r)$ 且互相不交, 则转移的方程可以写为:

$$ f_{l, r} + B + cnt \times C + (r - i - cnt \times (r - l)) \times A \rightarrow f_{i, r} $$

这样的复杂度最坏可以达到 $\mathcal O(n^3)$. 接下来引出一个优化复杂度的重要性质:

  • 若存在两个下标 $i$ 和 $j$, 满足可以从 $f_{l, r}$ 转移到 $f_{i, r}$ 和 $f_{j, r}$, 并且 $i+r-l > j$ (即有交), 那么我们只需要转移到 $j$ 即可!

为什么? 因为 $f_{i, r}$ 可以被 $f_{j, r}$ 以同样的代价更新 (注意从 $f_{j, r}$ 到 $f_{i, r}$ 的更新能且仅能使用操作 1)! 比如 $X = \texttt{ababaccaba}$, 取最右边的 $\texttt{aba}$ 作为当前的模式串, 花费 $\texttt{ba}$ 到 $\texttt{aba}$ 和从 $\texttt{aba}$ 花费 $\texttt{ab}$ 都可以以同样的代价到达 $\texttt{aba}$.

那么我们每次转移就只剩下 $\mathcal O(\frac{l}{r-l})$ 次了, 加起来就是调和级数. 预处理 $p_{l, r}$ 表示 $X[0, l)$ 中最后出现的 $X[l, r)$ 位置即可.

int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    const ull BASE = 1331;
    int n; cin >> n;
    vector<ull> pw(n+1), hs(n+1);
    pw[0] = 1;
    for (int i=1;i<=n;++i) pw[i] = pw[i-1]*BASE;
    string s; cin >> s;
    ll A, B, C; cin >> A >> B >> C;
    for (int i=0;i<n;++i) hs[i+1] = hs[i]*BASE+s[i]-'a'+1;
    const auto get_hash = [&](int l, int r) { return hs[r]-hs[l]*pw[r-l]; };
    vector pos(n, vi(n+1)); // pos[l][r] = Latest @i such that i+r-l <= l, s[i, i+r-l) == s[l, r)
    unordered_map<ull, int> latest;
    for (int d=1;d<=n;++d) {
        latest.clear();
        for (int r=d;r<=n;++r) {
            int l = r-d;
            if (l-d >= 0) latest[get_hash(l-d, l)] = l-d;
            auto h = get_hash(l, r);
            if (latest.find(h) != latest.end()) pos[l][r] = latest[h];
            else pos[l][r] = -1;
        }
    }
    const ll INF = 1.01e18;
    vector dp(n, vector(n+1, INF));
    for (int i=0;i<n;++i) dp.at(i).at(i+1) = A;
    for (int d=1;d<n;++d) {
        for (int l=0,r=d;r<=n;++l,++r) {
            if (l > 0) chkmin(dp.at(l-1).at(r), dp.at(l).at(r)+A);
            if (r < n) chkmin(dp.at(l).at(r+1), dp.at(l).at(r)+A);
            int p = pos[l][r], cnt = 2;
            while (p >= 0) {
                chkmin(dp.at(p).at(r), dp.at(l).at(r)+B+cnt*C+(r-p-cnt*(r-l))*A);
                cnt++;
                p = pos[p][p+r-l];
            }
        }
    }
    cout << dp.at(0).at(n) << "\n";
}

/ With a handshake /

/ Or an embrace /

/ Or a kiss on the cheek /

/ Possibly, all three /

—— American Football - The Summer Ends


date: '2023-10-14'
title: 'Solution -「JOISC 2022」刑務所'


Desc.

有无根树 $T = (V, E)$, 有 $m$ 个囚犯, 初始分别位于 $s_i$ 节点上. 每一时刻你可以将某个囚犯沿着无向边移动到相邻的节点, 判断是否存在一个方案, 使得所有囚犯分别沿着最短路径被移动到 $t_i$ 节点上, 并且不会出现同一时刻有两个或多个囚犯在同一节点上的情况. 多测.

$T \leqslant 10^3, n \leqslant 1.2 \times 10^5, 2 \leqslant m < n$.

Sol.

Link.

有一条性质:

  • 同一时间图上不会有两个移动中的囚犯.

证明很简单, 钦定两个囚犯, 并讨论两条路径的相对关系即可.

有另外两条比较好看出来, 但应该不太好证的性质:

  • 若囚犯 $j$ 的起点 $s_j$ 位于另外某个囚犯 $i$ 的路径 $(s_j, t_j)$ 上, 那么 $i$ 一定先于 $j$ 出发;
  • 若囚犯 $j$ 的终点 $s_j$ 位于另外某个囚犯 $i$ 的路径 $(s_j, t_j)$ 上, 那么 $i$ 一定晚于 $j$ 出发;

意会一下吧.

根据上述的先后关系, 可以建出关于囚犯的有向图, 若该图存在拓扑序 (即为 DAG), 那么有解, 否则无解. 问题是如何优化连边. 将原树复制为两棵, 分别称为 $S$, $T$, 记原树节点 $i$ 在两棵新树上的对应节点编号为 $S_i$ 和 $T_i$. 将每个囚犯抽象成一个点, 编号记为 $p_i$, 我们来看看如何改写原题的信息. 连边 $\lang p_i, S_{s_i}\rang, \lang T_{t_i}, p_i \rang$, $\lang v \in (S_{s_i}, S_{t_i}) \setminus \{S_{s_i}\}, p_i \rang$, $\lang p_i, v \in (T_{s_i}, T_{t_i}) \setminus \{T_{t_i}\}\rang$.

我们来检验一下这样建图的可行性, 若 $s_i$ 在 $(s_j, t_j)$ 上, 那么存在路径 $p_i \rightarrowtail S_{s_i} \rightarrowtail p_j$, 反之亦然, 则先后关系得到了刻画. 再来考察正确性, 发现我们只是把直接连接 $p_i$ 和 $p_j$ 的边通过拆分 $(s_j, t_j)$ 的方式加「长」了, 因此并不会影响环的存在性.

那么使用重链剖分, 然后线段树优化建图即可.

这个代码写着太自闭了, 先是 Linux 爆栈, 用 Windows 跑比题解快一倍, 但是交上去就是过不了... 服了. 以下代码过不了, 就看个乐呵吧.

const int N = 1.2e5;
int n, dfn[N + 5], rev[N + 5], fa[20][N + 5], dep[N + 5], son[N + 5], top[N + 5], times;
vvi grp, grp2;
vi deg;
void dfs(int u, int Fu) {
    rev[dfn[u] = times++] = u, dep[u] = dep[Fu]+1, fa[0][u] = Fu;
    for (int i=1;i<20;++i) fa[i][u] = fa[i-1][fa[i-1][u]];
    for (int v : grp[u]) if (v != Fu) dfs(v, u);
}
void dfs2(int u, int Fu, int t) {
    top[u] = t;
    if (son[u] < n) dfs2(son[u], u, t);
    for (int v : grp[u]) if (v != Fu && v != son[u]) dfs2(v, u, v);
}
void safe_add(int u, int v) {
    if ((unsigned)u >= grp2.size()) grp2.resize(u+1);
    if ((unsigned)v >= grp2.size()) grp2.resize(v+1);
    grp2[u].pb(v);
    if ((unsigned)v >= deg.size()) deg.resize(v+1);
    deg[v]++;
}
int tot; // Ids distribution.
int tr1[N + 5], tr2[N + 5], id1[N * 4 + 5], id2[N * 4 + 5];
void build(int u, int l, int r) {
    id1[u] = tot++, id2[u] = tot++;
    if (l == r) {
        safe_add(id1[u], tr1[rev[l]]);
        safe_add(tr2[rev[l]], id2[u]);
    } else {
        int mid = (l+r)/2;
        build(u*2, l, mid); build(u*2+1, mid+1, r);
        safe_add(id1[u], id1[u*2]);
        safe_add(id1[u], id1[u*2+1]);
        safe_add(id2[u], id2[u*2]);
        safe_add(id2[u], id2[u*2+1]);
    }
}
void lnk(int fr, int ba, int rt, bool flag, int u, int l, int r) {
    if (l > ba || r < fr) return;
    else if (l >= fr && r <= ba) {
        if (flag == 0) safe_add(rt, id1[u]);
        else safe_add(id2[u], rt);
    }
    else {
        int mid = (l+r)/2;
        lnk(fr, ba, rt, flag, u*2, l, mid), lnk(fr, ba, rt, flag, u*2+1, mid+1, r);
    }
}
int jump(int u, int d) {
    for (int i=0;i<20;++i) if (d&(1<<i)) u = fa[i][u];
    return u;
}
int getLca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    u = jump(u, dep[u]-dep[v]);
    if (u == v) return u;
    for (int i=19;i>=0;--i)
        if (fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v];
    return fa[0][u];
}
void cover(int u, int v, int rt, bool flag) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        lnk(dfn[top[u]], dfn[u], rt, flag, 1, 0, n-1);
        u = fa[0][top[u]];
    }
    if (dfn[u] > dfn[v]) swap(u, v);
    lnk(dfn[u], dfn[v], rt, flag, 1, 0, n-1);
}
bool isdag() {
    queue<int> que;
    for (int i=0;i<tot;++i) if (!deg[i]) que.push(i);
    while (!que.empty()) {
        int u = que.front(); que.pop();
        for (int v : grp2[u]) if (!--deg[v]) que.push(v);
    }
    return !count_if(allu(deg), [&](int x) { return x > 0; });
}
void solve() {
    cin >> n;
    // TODO: clear.
    fill(son, son+n, n);
    vvi(n).swap(grp);
    vvi().swap(grp2);
    vi().swap(deg);
    tot = times = 0;
    for (int i=0;i<n;++i) tr1[i] = tot++;
    for (int i=0;i<n;++i) tr2[i] = tot++;
    for (int i=1,u,v;i<n;++i) {
        cin >> u >> v; u--; v--;
        grp[u].pb(v), grp[v].pb(u);
    }
    for (int i=0;i<=n;++i) {
        for (int j=0;j<20;++j) fa[j][i] = n;
    }
    dep[n] = 0;
    dfs(0, n);
    return;
    dfs2(0, n, 0);
    build(1, 0, n-1);
    int m; cin >> m;
    for (int s,t,i=0;i<m;++i) {
        cin >> s >> t; s--; t--;
        int p = tot++;
        int fs, ft, lca = getLca(s, t);
        if (s == lca) fs = jump(t, dep[t]-dep[s]-1);
        else fs = fa[0][s];
        if (t == lca) ft = jump(s, dep[s]-dep[t]-1);
        else ft = fa[0][t];
        cover(s, ft, p, 0);
        cover(fs, t, p, 1);
        safe_add(tr1[t], p);
        safe_add(p, tr2[s]);
    }
    cerr << tot << ' ';
    cout << (isdag() ? "Yes\n" : "No\n");
}
int main()
{
    freopen("in", "r", stdin);
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    int t; cin >> t; while (t--) solve();
}

/ 恍然间 我已经 安然地 来到你身旁 /

/ 如暗夜灯塔 指引流星归家 /

/ 孤单的小船 在漫长漂泊后 终于靠岸 /

—— Ddickky - 最终祈愿 ft. 星尘


date: '2023-10-14'
title: 'Solution -「ARC 112E」Cigar Box'


Desc.

Link.

给定目标排列 $Q = (p_1, \dots, p_n)$ 和初始排列 $P = (1, \dots, n)$. 你可以对初始排列进行如下操作:

  • 选择一个下标 $i \in [1, n]$, 将其置于排列的开头或结尾.

问通过 $m$ 次操作将初始排列变化为目标排列的方案数, 对 $\textbf{998244353}$ 取模.

$n, m \leqslant 3000$.

Sol.

发现对于每一个数, 我们只关心对它的最后一次操作. 枚举 $L$ 和 $R$ 分别表示将某个数移动到开头的重要操作数和将某个数移动到结尾的重要操作数, 那么考虑从 $P$ 到 $Q$ 的操作过程, 最终留在 $[L+1, N-R]$ 的数的相对顺序并没有改变, 因此 $Q$ 中的 $[L+1, N-R]$ 部分一定单调递增.

设 $f_{i, L, R}$ 表示已经填了操作序列的 $[i, m]$ 部分 1 的, 前 $L$ 个数和后 $R$ 个数已经还原的方案数. 有如下转移:

$$ f_{i + 1, L, R} += f_{i, L, R} \times 2 \times (L + R) \\ f_{i + 1, L + 1, R} += f_{i, L, R} \\ f_{i + 1, L, R + 1} += f_{i, L, R} $$

[TODO /阐释转移方程/]

后面还似懂非懂, 就不放出来误人子弟了...

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    const int MOD = 998244353;
    using mint = modint<MOD>;
    int n, m; cin >> n >> m;
    vi a(n); rds(a);
    vector comb(n+1, vector<mint>(n+1));
    for (int i=0;i<=n;++i) {
        comb[i][0] = 1;
        for (int j=1;j<=i;++j) comb[i][j] = comb[i-1][j]+comb[i-1][j-1];
    }
    vector f(m+1, vector<mint>(n+1));
    f[0][0] = 1;
    for (int i=0;i<m;++i) {
        for (int j=0;j<=n;++j) {
            f[i+1][j] += f[i][j]*2*j;
            if (j < n) f[i+1][j+1] += f[i][j];
        }
    }
    mint ans = 0;
    for (int l=0;l<=n;++l) {
        for (int r=n-l;r>=0;--r) {
            if (n-l-r >= 2 && a[n-r-2] > a[n-r-1]) break;
            ans += f[m][l+r]*comb[l+r][l];
        }
    }
    cout << ans << "\n";
}

从未发觉 白昼已到尽头

何时 我未留意的腐朽

在暮雨之中尝试绽放

—— COP - hello&bye, days ft. 洛&乐


  1. 为什么倒序进行 DP? 因为这样我们才能获得「当前操作是否重要」的信息.


date: '2023-10-13'
title: 'Solution Set -「Nhk R2」'


A

观察数据范围, 我们可以发现 $n \leq 3000, k \leq 10^9$ 指向的是时间复杂度大概率是 $O(n^2)$ 的。 又由于是回文串, 和回文串相关的 $O(n^2)$ 算法最容易想到的就是枚举对称点暴力扩展。

接着再考虑如何处理操作, 如果我们单独有一个回文串, 想要扩展它的长度, 最优的方法就是在对称点进行操作, 这样每次操作后序列依旧回文, 如 $\texttt{bab}$ 可以操作为 $\texttt{baab}$ , $\texttt{bcbbcb}$ 可以操作为 $\texttt{bcbbbcb}$ 。

但是如果不是整个序列为回文串的话, 我们直接找最长回文子串然后在对称点进行操作不一定最优, 所以需要考虑在两侧进行操作(因为不在回文串两侧一定不是最优), 这时候有两种情况, 一种是可以通过在一侧进行操作后就能继续匹配, 如 $\texttt{bbabc}$ 可以对右侧的 $\texttt{b}$ 操作后扩展回文串长度, 回文串由 $\texttt{bab}$ 变为 $\texttt{bbabb}$ 这个时候我们执行这个操作, 因为这次操作和在对称点进行操作比一定不劣, 如果两端加数都不能继续匹配, 如 $\texttt{cbabd}$ , 就停止两侧的扩展, 因为此时想要在两侧操作后能扩展, 当且仅当同时对两边进行相同次数的操作, 这样和在对称点操作比起来一定不优。

最后, 如果我们有一段到了序列的首或尾, 我们直接选择在中间操作, 一直操作到长度等于 $k$ 。

B

从题目最强的限制入手,考虑如何使得每个点恰好处于一个环上?

这是一个很典的 trick,对于任何在环上的点 $p$,出入度都为 $1$,于是将它拆成两个点,记为 $p_{in}, p_{out}$。原图上的一条边 $(u, v)$ 在拆点后的图上就表示为 $(u_{out}, v_{in})$,如果要满足限制,当且仅当该图存在完美匹配。

现在加入双边权的限制,这是 0/1 分数规划 的模型,具体地,我们二分答案 $mid$,如果有解,则满足 $\large\frac{\sum_{i \in E} a_i }{\sum_{i \in E} b_i } \ge mid$,化一下式子变成 $\sum_{i \in E} a_i - mid \cdot {\sum_{i \in E} b_i } \ge 0$,于是重新对每一条边赋权为 $c_i = a_i - mid \cdot b_i$,检验是否有解直接跑 KM 即可。

复杂度取决于 KM 的实现方式,较优秀的为 $\Theta(n^3) \log V$,其中 $V$ 是 $a_i, b_i$ 的值域。

C

让我们来挖掘一些性质。不妨设 $n=2$,它的 $0\sim 2^n-1$ 的序列用二进制表示即为:

$$ \begin{matrix} 00 \\ 01 \\ 10 \\ 11 \\ \end{matrix} $$

那么当 $n=3$ 时,就相当于是在这个序列前加了一位,这一位可能是 $1$ 也可能是 $0$,列出来即为:

$$ \begin{matrix} 0\quad 00 \\ 0\quad 01 \\ 0\quad 10 \\ 0\quad 11 \\ 1\quad 00 \\ 1\quad 01 \\ 1\quad 10 \\ 1\quad 11 \\ \end{matrix} $$

所以可以发现:

  1. 异或值是对称的;
  2. 其最中间的数值是 $2^n-1$。

我们再对比上面 $n=2$ 与 $n=3$ 的区别,可以发现,题目的问题可以递推求解。

我们设第一个问题是的答案是 $f$,第二个问题是 $g$。那么有以下式子:

$$ \begin{cases} f_i=2\times f_{i-1}+\binom{n}{i} \\ \\ g_i=2\times g_{i-1}+i\binom{n}{i} \end{cases} $$

我们把上面第一个式子化为求和式,即为 $\displaystyle\sum_{i=1}^n2^{n-i}\binom{n}{i}$,又因为 $\displaystyle\binom{n}{i}=\binom{n}{n-i}$,所以 $\displaystyle=\sum_{i=1}^n2^{n-i}\binom{n}{n-i}\displaystyle=-2^n\times\binom{n}{n}+\sum_{i=0}^n2^{n-i}\binom{n}{n-i}$,换一下元,即 $\displaystyle-2^n\times\binom{n}{n}+\sum_{i=0}^n2^{i}\binom{n}{i}$,又通过二项式定理,得到原式即 $(2+1)^n-2^n=3^n-2^n$,用快速幂求解即可。

我们把上面第二个式子化为求和式,即为 $\displaystyle\sum_{i=1}^ni\times2^{n-i}(2^i-1)\binom{n}{i}$ 因为加上 $i=0$ 其值不变故可写为 $\displaystyle\sum_{i=0}^ni\times2^{n-i}(2^i-1)\binom{n}{i}$,将括号拆开 $\displaystyle\sum_{i=0}^ni2^{n}\binom{n}{i}-\sum_{i=0}^ni2^{n-i}\binom{n}{i}$ 我们将其看作左右两个式子求解,先看左边,即为 $\displaystyle\sum_{i=0}^ni2^{n}\binom{n}{i}$,将 $2^n$ 提出,即 $\displaystyle2^{n}\sum_{i=0}^ni\binom{n}{i}=2^{n-1}(\sum_{i=0}^ni\binom{n}{i}+\sum_{i=0}^ni\binom{n}{i})$ 再将式子括号右边的求和式通过 $\binom{n}{i}=\binom{n}{n-i}$ 变换一下,即 $\displaystyle 2^{n-1}(\sum_{i=0}^ni\binom{n}{i}+\sum_{i=0}^ni\binom{n}{n-i})$ 再换元,即 $\displaystyle2^{n-1}(\sum_{i=0}^ni\binom{n}{i}+\sum_{i=0}^n(n-i)\binom{n}{i})$ 再将括号中的数加起来,即 $=2^{n-1}\sum_{i=0}^nn\binom{n}{i}$,再将 $n$ 提出,即 $\displaystyle n2^{n-1}\sum_{i=0}^n\binom{n}{i}$ 再用二项式定理,可化为:$n2^{n-1}2^n=n2^{2n-1}$。

我们再考虑右边的式子 $\displaystyle-\sum_{i=0}^ni2^{n-i}\binom{n}{i}$,给其加上一个 $\displaystyle n\sum_{i=0}^{n}2^{n-i}\binom{n}{i}$,再减去,即 $\displaystyle\sum_{i=0}^{n}(n-i)2^{n-i}\binom{n}{i}-n\sum_{i=0}^{n}2^{n-i}\binom{n}{i}$ 右边式子的结果在上面求解第一个问题已经算过,为 $n*3^n$,化简即为 $\displaystyle\sum_{i=0}^{n}(n-i)2^{n-i}\binom{n}{i}-n*3^n$ 再换元,即为 $\displaystyle\sum_{i=0}^{n}i2^i\binom{n}{i}-n*3^n$ 再通过组合恒等式$\displaystyle\binom{n}{m}=n/m*\binom{n-1}{m-1}$,可化为 $\displaystyle\sum_{i=0}^{n}n2^i\binom{n-1}{i-1}-n*3^n$,提出 $2*n$,即为 $\displaystyle 2n\sum_{i=0}^{n}2^{i-1}\binom{n-1}{i-1}-n*3^n$ 再换元,即为 $\displaystyle 2n\sum_{i=0}^{n-1}2^{i}\binom{n-1}{i}-n*3^n$ 再用二项式定理,可化为 $\displaystyle 2n*3^{n-1}-n*3^n=-n*3^{n-1}$。

综上所述,第二问答案为 $n2^{2n-1}-n*3^{n-1}$,可用快速幂求解。

D

一个结论是任意无向图度数为奇数的点有偶数个。因为一条边会使得度数总和增加 $2$,度数总和永远是偶数。

所以如果建一个虚点向所有度数为奇数的点连边,则这个新图存在欧拉回路。

考虑初始图为二分图。然后加上虚点。

考虑从虚点出发,每走一条边,就染成跟上一条边不相同的颜色,即黑白间隔染色。则不被虚点连向的点(度数为偶数),每次进入这个点就会出这个点,边能一一对应。设初始图中 $a_x$ 表示 $x$ 点为端点的边有 $a_x$ 条被染成白色,$b_x$ 表示以 $x$ 为端点的边有 $b_x$ 条被染成黑色,则对于度数为偶数的点 $|a_x-b_x|=0$。对于度数为奇数的点,加上虚点连向的边,一样可以做到一进一出一一对应。由于虚点连向的边不产生贡献,所以 $|a_x-b_x|=1$。

则二分图 $\sum_x |a_x-b_x|$ 的答案为图中度数为奇数的点的个数。

考虑本题。驿站发件处看做左部点,收件处看做右部点,是一个二分图。所以可以用上述结论。

对于一个询问新加入的点,会对其右上的点的度数产生影响,而新加入点的度数为左下的点数。

首先二维数点求出初始答案,记为 $ans1$。然后记录每个点的度数奇偶性。

对于新加入的点分为三个二维数点。

第一个算新加入点的度数,记为 $deg1$。

第二个算新加入点右上角的点数,记为 $cnt1$。

第三个算新加入点右上角度数为偶数的点数,记为 $cnt2$

新答案为 $ans1+[deg1\%2=1]+cnt2-(cnt1-cnt2)$。

E

前三种操作维护起来比较简单,这里就略过了,第四种操作只需要注意到「最多操作 $\log_2 n$ 次」的性质即可。

主要来看询问部分。

整个图的形状就是一根「主链」上挂了若干根「副链」,由此可以发现这个图中的最长简单链的长相一定类似如下的图示:

$$ \cdots{\color{black}\bullet\ \bullet\ \bullet\ }\color{black}\bullet\ {\color{black}\bullet\ }{\color{red}{\bullet\ \bullet\ \bullet\ \bullet\ \bullet\ }}{\color{black}\bullet\ }\color{black}\bullet\ {\color{black}\bullet\ \bullet\ \bullet}\cdots\ \\ \cdots{\color{black}\bullet\ \color{red}{\bullet\ }\color{black}\bullet\ \bullet\ \bullet\ \color{red}{\bullet\ }}\color{black}\bullet\cdots \\ \cdots{\color{black}\bullet\ \color{red}{\bullet\ }\color{black}\bullet\ \bullet\ \bullet\ \color{red}{\bullet\ }}\color{black}\bullet\cdots \\ \cdots{\color{black}\bullet\ \color{red}{\bullet\ }\color{black}\bullet\ \bullet\ \bullet\ \color{red}{\bullet\ }}\color{black}\bullet\cdots \\ \vdots $$

其中红色的点是图中的最长链。那么我们考虑暴力怎么做。我们处理出两个东西,一个是上面那个图的第一层的权值前缀和 $a_i$,以及不包含第一层元素的每一条单链的权值和 $b_i$,那么答案即 $\max\limits_{1\leqslant i\leqslant j\leqslant n}\{a_j-a_{i-1}+b_i+b_j\}$。此时就可以考虑平衡树套线段树了,平衡树维护第一层的结点,线段树维护每条链除第一层结点外的部分,维护方法参见最大子段和。

F

记所有有颜色的边的属性集合 $S$ 。

首先在外层容斥,枚举 $S\in [0,2^w)$,计算被覆盖的的边中不包含 $S$ 中属性,并且没有被覆盖的边的数目恰好为 $i$ 的配对方案数。

暴力的 DP 做法是记录子树内还没有被匹配的点的数目,复杂度$O(n^5)$,不能通过。出题人特意卡了这种做法。

如果一条边没有覆盖,那么所有点对间的路径都不能经过这条边,这样我们可以把一个连通块分成两个子联通块进行求解。但是这样就要记录连通块里面所有的点,无法通过

考虑二项式反演。记 $g(i)$ 表示钦定断了 $i$ 条边(即$i$条边没有被覆盖)的方案数,$f(i)$ 表示恰好断了 $i$ 条边的方案数,注意这里的下标 $i$ 不包含一定不被覆盖的边。那么有:

$g(i)=\sum _{j=i}^{n-1}\binom{j}{i}f(j)\Rightarrow f(i)=\sum_{j=i}^{n-1}(-1)^{j-i}\binom{j}{i}g_j$

而 $g(i)$ 是好算的,也就是剩下的每个连通块内部任意连边的方案数的乘积

记 $h(n)$ 表示大小为 $n$ 的连通块任意连边的方案数,如果 $n$ 为奇数那么答案是 $0$,如果 $n$ 为偶数那么答案是 $(n-1)\times (n-3)\times ...\times 1$

考虑 DP。设 $dp(u, i, j)$ 表示以 $u$ 为根的子树,已经断了 $i$ 条边,连通块大小为 $j$ 的方案数。对于一条边 $(u,v,w)$ 转移式子如下:

$1.1$ $dp(u, i, j) \times dp(v, i_2, j_2) \times h(j_2) \to dp(u, i + i_2+1, j)$

$1.2$ 如果 $w\notin S$,$dp(u,i,j)\times dp(v,i_2,j_2)\to dp(u,i+i_2,j+j_2)$

这个 DP 的时间复杂度上界是 $O(n^4)$ 的,因此总复杂度 $O(2^wn^4)$ 。

但是注意到每个连通块大小都是必须偶数,因此常数极小,实测单次 DP 计算量在 $10^6$ 左右,链的情况可以卡满。注意要把 DP 值为 0 的状态跳过,否则无法通过

数据里面造了一些几条链并起来的情况,暴力要跑 4s 以上,std 能稳定在 0.5s 内出解。随机数据下基本卡不了。如果有人暴力冲过去了或者正解被卡常了,出题人在这里谢罪:(

考虑到打这场比赛的大佬肯定还是比较多的,如果场切这道题的大佬们有更精确的分析复杂度的方式欢迎赛后分享。


date: '2023-10-13'
title: 'Solution -「GYM 104128E」Color the Tree'


Desc.

Link.

给出树 $T=(V,E)$, 一个代价数组 $a_0,\dots, a_{n-1}$. 现欲对该树进行黑白染色, 所有节点初始皆为白色, 每次你可以令节点 $u$ 的子树中距离 $u$ 为 $i$ 的节点以 $a_i$ 的代价染为黑色. 问将整棵树染为黑色的最小代价.

$n \leqslant 10^5$.

Sol.

朴素做法即, 设 $f_{u, d}$ 为将子树 $u$ 中距离 $u$ 为 $d$ 的节点染为黑色的最小代价, 直接转移即可. $\mathcal O(n^2)$. 以下是朴素部分的代码:

void dfs2(int u) {
    for (int v : grp[u]) dfs2(v);
    for (int i=0;i<=m-dep[u];++i) dp[u][i] = a[i];
    static ll sum[N + 5];
    memset(sum, 0, (m-dep[u])*8);
    for (int v : grp[u]) {
        for (int i=0;i<=m-dep[v];++i) sum[i] += dp[v][i];
    }
    for (int i=0;i<m-dep[u];++i) chkmin(dp[u][i+1], sum[i]);
}

然后可以发现, 我们对于不同深度染色最小代价的计算是独立的, 于是枚举 $d$, 我们求解将原树中深度为 $d$ 的节点染黑的最小代价. 后面的做法就很暴力了, 直接将深度为 $d$ 的节点拉出来建虚树, 跑朴素 DP 即可. $\mathcal O(n\log_2 n)$ / $\mathcal O(n)$, 根据具体实现复杂度不同.

void solve() {
    int n; cin >> n;
    vi a(n); rds(a);
    const int LG = __lg(n)+1;
    vvi mn(LG);
    mn[0] = a;
    for (int i=1;i<LG;++i) {
        mn[i].resize(n-(1<<i)+1);
        for (int j=0;j<n-(1<<i)+1;++j) mn[i][j] = min(mn[i-1][j], mn[i-1][j+(1<<i-1)]);
    }
    auto getMin = [&](int l, int r) {
        assert(0 <= l && l <= r && r < n);
        int k = __lg(r-l+1);
        return min(mn[k][l], mn[k][r-(1<<k)+1]);
    };
    vvi grp(n);
    for (int i=1,u,v;i<n;++i) {
        cin >> u >> v; u--; v--;
        grp[u].pb(v);
    }
    vvi fa(LG, vi(n+1, n));
    function<void(int)> dfs2 = [&](int u) {
        for (int i=1;i<LG;++i) fa[i][u] = fa[i-1][fa[i-1][u]];
        for (int v : grp[u]) fa[0][v] = u, dfs2(v);
    };
    dfs2(0);
    vi dep(n), dfn(n);
    int timeStamp = 0;
    function<void(int)> dfs = [&](int u) {
        dfn[u] = timeStamp++;
        for (int v : grp[u]) dep[v] = dep[u]+1, dfs(v);
    }; dep[0] = 1; dfs(0);
    auto getLca = [&](int u, int v) {
        if (dep[u] < dep[v]) swap(u, v);
        for (int i=LG-1;i>=0;--i)
            if (fa[i][u] < n && dep[fa[i][u]] >= dep[v]) u = fa[i][u];
        if (u == v) return u;
        for (int i=LG-1;i>=0;--i)
            if (fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v];
        return fa[0][u];
    };
    int m = *max_element(allu(dep));
    vvi idep(m+1);
    for (int i=0;i<n;++i) idep[dep[i]].pb(i);
    ll ans = 0;
    vvi virt(n);
    auto createVirtualTree = [&](vi h) {
        sort(allu(h), [&](int x, int y) {
            return dfn[x] < dfn[y];
        });
        for (int i=0,sz=h.size();i<sz-1;++i) h.pb(getLca(h[i], h[i+1]));
        sort(allu(h), [&](int x, int y) {
            return dfn[x] < dfn[y];
        });
        h.erase(unique(allu(h)), h.end());
        for (int i=0,sz=h.size();i<sz-1;++i) virt[getLca(h[i], h[i+1])].pb(h[i+1]);
        return h[0];
    };
    for (int i=m;i>=1;--i) {
        int rt = createVirtualTree(idep[i]);
        function<ll(int)> dfs3 = [&](int u) {
            if (!virt[u].empty()) {
                ll res = 0;
                for (int v : virt[u]) res += dfs3(v);
                virt[u].clear();
                return min(res, (ll)getMin(i-dep[u], i-1));
            } else return (ll)getMin(0, i-1);
        };
        ans += dfs3(rt);
    }
    cout << ans << "\n";
}

/ 看着蔚蓝的天空被电线分割了 /

/ 天空下的你和我擦肩过 /

/ 就像风起风落 /

/ 曾有一张白纸在面前铺陈着 /

/ 提起笔勾勒出了轮廓 /

—— 孤寂code - 平行 ft. 洛天依