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

标签 graph theory 下的文章

首先有两个前置技巧:1) 两点间的最短距离就是直接连接两点的边的长度;2) 遍历一个子图的最小花费是最小生成树的边权之和乘二。原问题让我们找出一条最短且必经过钦定边的 $( s, i )$ 路径,那么我们先将 $\lang s , i \rang$ 连上,问题就变成了找出一条最短且必经过钦定边 $( s, i)$ 的回路。虽然每条边不一定恰好经过一次,但是对于正确性的判断,每个点的度数为偶数依然是一个必要条件,再加上连通性的限制,一条回路的正确性就可以由这两条必要条件充要表示。

其次来考虑如何修复每个点度数的奇偶性,一个贪心策略是把一条 $(s, i)$ 拆成 $(s, s+1), (s+1, s+2) \dots (i-1, i)$,由于边权的性质,可以发现这样拆分一定不劣;又考虑连通性修复,类似以上。

const int MN = 2.5e3;
int n, m, s, fa[MN + 100], deg[MN + 100];
int find(int u) {
    while (u != fa[u]) u = fa[u] = fa[fa[u]];
    return u;
}
bool unite(int u, int v) {
    if (find(u) != find(v)) {fa[find(u)] = find(v);return 1;}
    return 0;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m >> s; s --;
    iota(fa, fa+n, 0);
    ll res = 0;
    rep(m) {
        int u, v;cin >> u >> v;u--;v--;
        deg[u] ++ , deg[v] ++;
        res += abs(u - v);
        unite(u, v);
    }
    vi tmp(fa, fa+n);
    rep(n) {
        deg[s] ++ , deg[i] ++;
        unite(s, i);
        vi id;
        rep(j,n) if (deg[j]%2) id.pb(j);
        ll sum = 0;
        rep(j,intsz(id)-1) {
            rep(k,id[j],id[j+1]) unite(k, k+1);
            sum += id[j+1]-id[j];
            j++;
        }
        vi from, to; vi().swap(id);
        rep(j,n) if (deg[j] > 0) id.pb(j);
        rep(j,intsz(id)-1) from.pb(id[j]), to.pb(id[j+1]);
        vi(intsz(to)).swap(id);
        iota(id.begin(), id.end(), 0);
        sort(id.begin(), id.end(), [&](int x, int y) {
            return to[x]-from[x]<to[y]-from[y];
        });
        for (int j : id) {
            if (unite(from[j], to[j])) sum += 2*(to[j]-from[j]);
        }
        cout<< res + sum << " \n"[i==n-1];
        deg[s] -- , deg[i] --;
        rep(j,n) fa[j] = tmp[j];
    }
}
/*
急
|
|
速
|
|
地
|
|
下
|
|
坠
|
|
*/

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. 星尘


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-04'
title: 'Solution -「ARC 106E」Medals'


Desc.

Link.

你有 $n$ 个朋友,他们会来你家玩,第 $i$ 个人 $1...A_i$ 天来玩,然后 $A_i+1...2A_i$ 天不来,然后 $2A_i+1...3A_i$
又会来,以此类推;

每天你会选一个来玩的人,给他颁个奖,如果没人来玩,你就不颁奖。

你要给每个人都颁 $K$ 个奖,问至少需要多少天。

Sol.

前面的部分很套路了,主要看看建出二分图后我们应该做什么。首先根据 Hall's marriage theorem,我们要做的是判断任意子集的邻域大小是否大于等于该子集的大小。把 $n$ 个人拆成 $n\times k$ 个点,可以发现进行判断时不需要把同一类点(由同一个工人拆出来的 $k$ 个点)分开。设 $f(S)$ 为满足存在集合 $S$ 中任一工人会来打工的天数,则有解的一个充要条件为 $\forall S\subseteq 2^{\{1,\dots,n\}},m-f(U\setminus S) \geqslant |S|\times k$。

const int N = 18, K = 1e5;
int n, k, a[N + 5], f[2 * N * K + 5], g[1 << N];
bool check(int upp) {
    const int LIM = 1 << n, U = LIM - 1;
    for (int i=0;i<LIM;++i) g[i] = 0;
    rep (i,1,upp) g[f[i]]++;
    for (int j=0;j<n;++j) for (int i=0;i<LIM;++i) if (!(i&(1<<j))) g[i|(1<<j)] += g[i];
    for (int i=0;i<LIM;++i) if (upp - g[U^i] < __builtin_popcount(i) * k) return false;
    return true;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    rd(n, k); rds0i(a, n);
    for (int i=0;i<n;++i) {
        for (int t=0;;t+=2) for (int j=t*a[i]+1;j<(t+1)*a[i]+1;++j) {
            if (j > 2 * n * k) goto label;
            f[j] |= 1<<i;
        }
        label:;
    }
    int l = 0, r = 2 * n * k, res = -1;
    while (l <= r) {
        if (int mid = (l + r) / 2; check(mid)) r = mid - 1, res = mid;
        else l = mid + 1;
    }
    cout << res << "\n";
}