Solution Set - “我将它捣成美梦愿你梦里无忧”
- 0.「NOI Simu.」掌分治
- 1.「集训队互测 2019」「LOJ #3077」绝目编诗 ⭐
- 2.「ICPC 2019 WF」「洛谷 P6256」Directing Rainfall ⭐
- 3.「CTSC 2010」「洛谷 P4191」性能优化 ⭐
- 4.「NOI Simu.」摸底测试 ⭐
- 5.「NOI Simu.」树上游戏 ⭐
- 6.「NOI Simu.」土地划分 ⭐
- 7.「NOI Simu.」算法考试 ⭐
- 8.「CF 1693E」Outermost Maximums ⭐
- 9.「CTSC 2016」「洛谷 P5417」萨菲克斯·阿瑞 ✡️
- 10.「IMO 2023」忍者路径 ⭐
- 11.「SCOI 2016」「洛谷 P3290」围棋
- 12.「NOI 2017」「洛谷 P3827」分身术 ⭐
- 13.「HEOI 2014」「洛谷 P4106」逻辑翻译
- 14.「NOI Simu.」简单计数 ⭐
- 15.「NOI Simu.」队伍划分 ⭐
我草快去听 Orange Killer 啊!!!
对了, 这是第 17 篇 sol set, 正在伤脑经怎么在 final review 里编号. (
0.「NOI Simu.」掌分治
- Private link & Submission.
- 「A.数学-组合计数」
"连通块大小" 仍然拆成点对贡献, 正好一个连通块产生贡献时会被删掉一个点, 我们就可把贡献挂在它的上面, 也即使 "点被删除时与它连通的点的数量".
枚举被删除点 \(r\) 和与钦定删除时与它连通的点 \(u\). 若 \(r\) 和 \(u\) 在同一个环上, 那么只需要保留环上的 \(a\) 个或者 \(b\) 个点就可以让 \(r,u\) 连通, 这可以容斥作 "保留 \(a\) 个" + "保留 \(b\) 个" - "保留两侧 \(a+b-1\) 个", 其余情况把若干个环的保留数量加起来统一算概率就行, 简单 DP \(\mathcal O(n^2)\) 算所有 \(u\) 的答案, 最终复杂度 \(\mathcal O(n^3)\).
1.「集训队互测 2019」「LOJ #3077」绝目编诗 ⭐
- Link & Submission.
- 「C.性质/结论」
麻了, 猜到类似结论还是不会做.
可以感知到 \(m\) 应当在 \(\mathcal O(n)\) 级别, 否则必然有解. 先想想如何求答案. 我们只需要快速找到 \(n\) 个环就能完成判断, 所以可以直接搜索剪枝: 枚举环的第一条边, 先将它删掉, 然后大力搜索下一步, 每走一步就判断能否得到至少一个新环. \(\mathcal O(n)\) 次找环, 每次走 \(\mathcal O(n)\) 步, 每步 \(\mathcal O(m)\) 检查, 总复杂度 \(\mathcal O(n^3)\).
接下来, 我们进一步缩紧需要判断的 \(m\) 的上界. 设 \(m\ge n\), 我们在环上随机删掉 \(\sqrt n\) 条边, 那么每条边被删除的概率 \(\le 1/\sqrt n\). 假设图上存在大小为 \(3\sim n\) 的环各一个, 那么删除后图上环的期望个数就有
也就是说, 存在一种删除 \(\sqrt n\) 条边的方法, 是的剩下环的数量 \(<\sqrt n\). 因此得到一个阈值 \(m\le n+2\sqrt n\). 超过阈值的 \(m\) 一定有解 (不符合 "各一个环", 则存在题目所求环对). 现在, 生成树上取出 \(\mathcal O(\sqrt n)\) 条非树边的端点建立虚树, 我们可以得到一个点边规模 \(\mathcal O(\sqrt n)\) 的带权图. 运行上文算法暴力求解, 复杂度 \(\mathcal O(n\sqrt n)\).
这题又名掘墓鞭尸?
2.「ICPC 2019 WF」「洛谷 P6256」Directing Rainfall ⭐
- Link & Submission.
- 「A.DP-数据结构优化 DP」
显然雨棚之间的雨水滴落存在一个拓扑关系, 我们可以用 这篇 #10 的扫描线做法建出这个拓扑关系. 沿着拓扑关系从上到下枚举雨棚, 维护雨点当前横坐标关于最小代价的序列, 每个雨棚对序列的变化形如:
- 令 \([l,r]\) 内的数\({}+1\).
- 令 \([l,r]\) 内的数变为区间内的前缀 (后缀) \(\min\).
(注意这里省略了边界的 \(\pm1\).) 维护差分, 以左高右低的雨棚为例, 前缀取 \(\min\) 相当于把区间内 \(>0\) 的差分值于紧邻的 \(<0\) 的差分值抵消, 如果抵消不了就全部推到 \(r+1\) 上. 用两个 std::map
维护正负差分值就行. 不难看出复杂度 \(\mathcal O(n\log n)\).
为什么题解区两篇题解实现的时候, 雨都是从地面返回天上? Vertin 感到好奇.jpg
3.「CTSC 2010」「洛谷 P4191」性能优化 ⭐
- Link & Submission.
- 「A.数学-多项式」
先来复习一下 Chirp-Z 变换, 设 \(\hat a_i=(\omega^{ij})_{1\times n}\times(a_j)_{n\times 1}\), \(\omega\) 为 \(n+1\) 的原根, 那么:
于是只需要进行一次差卷积. 对 \(j\) 处的下标变换是 \(j\to n-1-j\), 因而卷积出来的下标变换是 \(i+j\to n-1+i\), 我们只需要 \(n-1\sim 2n-2\) 内的数据不被污染, 所以做长度不小于 \(2n\) 的 DFT 就行.
当然这里还需要求逆变换. 注意到 \((\hat a_i)_{n\times 1}=(\omega^{ij})_{n\times n}\times(a_i)_{n\times 1}\), 中间矩阵自乘得到:
因此只需要对 \(\hat a\) 再做一次 Chirp-Z, 所有位置\({}\div n\), reverse \(\hat a_{1..n-1}\) 即得逆变换. 这和我们 FFT 的常见写法是一样的. 本题的 FFT 可以用 NTT 模 \(>(5\times10^5)^2\) 的素数实现, 不需要任意模数.
有没有巧妙一点的算法?
设 \(A(z)=\dots\), 我们仍然想插值求出 \(A(\omega^i)\). 回到最初, 我们来学习 FFT 吧!
分开奇数项与偶数项 取出 \(n\) 的任意一个素因子 \(d\), 设 \(A_r(z)\) 表示仅保留 \(i\bmod d=r\) 的系数得到的多项式, 那么:
设 \(\cancel{n=2m}\) \(n=dm\), 尝试代入一个 \(\omega_n^k\):
分治即可. 复杂度和 \(n\) 的素因子有关, 这题恰好保证了素因子都很小, 直接分治就行.
4.「NOI Simu.」摸底测试 ⭐
- Private link & Submission.
- 「B.Tricks」
考虑如何求 \(g(S)\), 对于静态的 \(S\) 这个问题其实很容易. 分子串贡献, 子串 \(s\) 可贡献的区间是 "\(s\) 第一次出现的有端点" 到 "\(s\) 最后一次出现的左端点" 这个左闭右开的区间. 对于 SAM 上同一个等价类中的子串同时考虑贡献的话, 贡献是一段区间加和一段等差数列加, 很好维护.
然后是一个重磅 trick, 相见恨晚呐!
合法区间长度具有单调性的序列划分问题中, 有总共检查区间长度 \(\mathcal O(n\log n)\) 的算法. 对于左端点 \(l\), 倍增增量长度 \(1,2,\dots,2^k\) 增大 \(r\), 不可增大时立即退出, 然后倍增增量长度 \(2^k,\dots,2,1\) 继续增大 \(r\). 这样, 我们找出极长 \([l,r]\) 只需要检查 \(\mathcal O((r-l)\log(r-l))\) 长度的区间.
/* Example */
inline bool check(const LL lim) {
int l = 1, cnt = 0;
while (l <= n) {
if (++cnt > m) return false;
int r = l - 1, k = 1;
while (r + k <= n && calc(l, r + k) <= lim) r += k, k <<= 1;
while (k >>= 1) if (r + k <= n && calc(l, r + k) <= lim) r += k;
l = r + 1;
}
return true;
}
5.「NOI Simu.」树上游戏 ⭐
- Private link & Submission.
- 「C.性质/结论」
深究 "为什么二者计数对象那么奇怪, 一点都不对称" 是应该快点得到的 motivation.
小 P 的计数对象可以容斥为 \(\binom{|V_P|}{2}\) 减去 \(\sum_{u,v\in V_P}[u\in\anc(v)][w_u{\color{red}{\le}}w_v]\). 我们本来期望中间的 \(\le\) 是 \(<\), 这样小 P 小 Q 的贡献形式完全一致, 问题很好解决: 从 \(V_Q=V\) 起, 每次选一个 \(u\in V_Q\) 丢到 \(V_P\) 里. 对于一个 \(w_u<w_v\) 的祖孙对 \((u,v)\), 先被丢到 \(V_P\) 的会让答案\({}-1\) (来自小 Q), 后被丢到 \(V_P\) 的还是会让答案\({}-1\) (来自小 P), 因此每个点对答案的贡献是一定的, 贪心选择就行.
那 \(\le\) 怎么办? 考虑 \(=\), 当 \(u\) 被丢到 \(V_P\) 时, 会让祖先中和子孙中所有 \(w_v=w_u\) 的 \(v\) 的贡献增加 \(1\). 我会数据结构! 树剖+线段树 (为每种 \(w\) 动态开点) 维护每种 \(w\) 的最小点, 再用堆汇总全局最小点. \(\mathcal O(n\log^2n)\), 其实也能过.
别急, 想想结论? 不难发现, 若 \(w_u=w_v\) 时, 若 \(v\) 同时还是 \(u\) 的祖先, 则 \(v\) 必然比 \(u\) 先选. 这是显然的 (有个傻瓜快速否定了这个结论去冲树剖, 乐). 这样每个点的贡献又是固定的了, \(\mathcal O(n\log n)\) 贪心.
6.「NOI Simu.」土地划分 ⭐
- Private link & Submission.
- 「A.扫描线」「A.数学-容斥计数」
核心是容斥的设计. 设无向图 \(G\) 描述了 \(n\) 个矩形是否相交的关系. 则我们需要数 \((u,v,w)\) 间无连边的三元组. 构造 (式中 \(u,v,w\) 不取相同值):
其中 \(B,C\) 通过 \(u\) 的度数就能计算, \(D\) 稍微麻烦一点. 我们分别两个部分求解.
对于 \(B,C\), 扫描线求每个矩形与其他多少个矩形有交. 可以把有交的矩形分为与 \(u\) 的左边界相交和不与 \(u\) 的左边界相交两部分, 前者正常扫描线, 后者不需要去除被完全扫过的矩形的区间贡献, 在 \(u\) 的左右边界处做差分. 这里是 \(\mathcal O(n\log n)\) 的.
对于 \(D\), 考虑在 \(u,v,w\) 交出的矩形的左下角统计一次贡献. 做二维差分后得到类似 "一个点被三个矩形包含", "挨着两个点被三个矩形包含", "一团四个点被三个矩形包含" 的情况, 线段树维护 \(\sum \binom{\cdot}{3}\), 做两次扫描线 (在扫描的坐标方向上情况相同的问题可以一起算) 即可求出答案. 最终复杂度 \(\mathcal O(n\log n)\).
7.「NOI Simu.」算法考试 ⭐
- Private link & Submission.
- 「A.DP-树上 DP」「A.数学-Lagrange 插值」
中位数经典转化, 枚举中位数 \(X\), 令 \(a_i\gets\sgn(a_i-X)\), 设 \(f(u,x,y,-1/0/1)\) 表示在递归树的 \(u\) 结点处, 子树内有 \(x\) 个 \(-1\), \(y\) 个 \(+1\), 子树返回值为 \(-1/0/1\) 的方案数. 大力背包转移, 我们得到了一个极其粗犷的暴力, 考虑优化.
首先, \(-1\) 很少, 没有 \(-1\) 的区间的状态是一定的. 于是每次只有 \(\mathcal O(k\log n)\) 个状态需要求解.
此外, \(x,y\) 只需要记录区间中的 \(-1\) 的决策情况, 于是单个 \(u\) 的状态量变为 \(\mathcal O(k^2)\).
状态转移, 暴力的做法是枚举三个儿子的所有信息, 但显然可以先枚举两个求出中间状态, 再和剩下的一个合并. 这样转移的总复杂度为 \(\mathcal O(k^4)\).
到此, 对于固定的 \(X\), 我们可以 \(\mathcal O(k^4+kn)\) 地求出结果.
接下来是对 \(X\) 的枚举的优化. 显然总共只有 \(\mathcal O(k)\) 个本质不同的值域区间, 每个区间内可以 Lagrange 插值求出总贡献. 最终复杂度 \(\mathcal O(k^5+k^2n)\).
8.「CF 1693E」Outermost Maximums ⭐
- Link & Submission.
- 「C.思维」
好题, 不会捏.
\(\mathcal O(n^2)\) 暴力很容易: 模拟消除过程, 每次把所有最大值位置赋成左右最大值的 \(\min\), 接下来的工作是优化这个模拟过程.
把 "赋值" 作为懒标记, 在处理完 \(\ge x\) 的数后, 我们可以维护出一个区间 \([l,r]\), 表示 \([1,l)\) 内 \(\ge x\) 数需要被赋为前缀 \(\max\), \([l,r]\) 内 \(\ge x\) 的数不确定赋值方向, \((r,n]\) 内 \(\ge x\) 的数需要被赋为后缀 \(\max\). 考虑加入 \(x\), 设 \(x\) 的最左出现位置是 \(p\), 最右出现位置是 \(q\), 那么:
- \([p,q]\) 内所有 \(>x\) 的数一定会被赋为 \(x\), 贡献答案.
- 如果 \(r<p\), \((r,p)\) 内所有 \(>x\) 的数需要被赋为后缀最大值, 也就是 \(x\), 贡献答案. 此外, 我们得知了 \([l,r]\) 内 \(>x\) 的数赋前缀 \(\max\), 因而 \([l,r]\gets[r+1,q]\).
- 否则若 \(l>q\), 类似地, \((q,l)\) 贡献答案, \([l,r]\gets[p,l-1]\).
- 否则, 没有其他可以确定的赋值, \([l,r]\gets[p,q]\), 没有答案贡献.
最终只需要支持查询 "区间内有多少个 \(>x\) 的数", BIT 就行. \(\mathcal O(n\log n)\).
9.「CTSC 2016」「洛谷 P5417」萨菲克斯·阿瑞 ✡️
- Link & Submission.
- 「A.数学-组合计数」「C.思维」
关键点在于计数对象的转化. 对于一个 suffix array \(\{p_n\}\) 和与之一一对应的 rank array \(\{r_n\}\), 它们可以导出字符之间的不等关系:
- \(\forall i\in[1,n)\), 若 \(r_{p_i+1}<r_{p_{i+1}+1}\), 则 \(s_{p_i}\le s_{p_{i+1}}\), 否则 \(s_{p_i}<s_{p_{i+1}}\).
据此, 我们可以得到一个由 \(\le\) 和 \(<\) 组成的字符之间的不等式链.
这有什么用? — 你就要问了, 毕竟这个映射可能不单也不满, 顶多算是对 SA 进行了一个很粗的分类. 接下来对于一个特定的分类 (也即是一个特定的不等式链), 我们又反过来考虑有多少 SA 属于这个分类.
我们先把字符 (连同它的下标) 按照不等式链排列, 此时 \(<\) 和 \(\le\) 可以分别生成 rank 的 \(>\) 和 \(<\) 关系. 为了避免歧义, 我们用 \(<_r,>_r\) 描述 rank 链上的符号, 用 \(<_c,\le_c\) 描述字符链上的符号.
这里, 可以联想到不等关系一题, 与其讨论 \(\le_c\) 和 \(<_c\), 我们不如套用不等关系的容斥做法, 将 \(>_r\) 翻转容斥, 同时也对应地将一些 \(<_c\) "翻转" 成 \(\le_c\). 容斥之后, 相邻两个的 \(>_r\) 之间存在形如 \(>_r\cdot<_r\cdot<_r\cdot<_r\cdot>_r\) 的子链, 对应着字符链上两个 \(<_c\) 之间的形如 \(<_c\cdot\le_c\cdot\le_c\cdot\le_c\cdot<_c\) 的子链. 我们现在需要对小圆点分配 rank, 显然 rank 子链上是升序的, 我们可以在 \(n\) 个 rank 中无序地选出 \(\ell\) (\(\ell\) 为小圆点个数) 个值, 分配给其中的 rank. 因此, 设容斥前总共有 \(m\) 个 \(>_r\) (即 \(m\) 个 \(<_c\)), 未被容斥的 \(>_r\) 将链划分为长度为 \(\ell_1,\ell_2,\cdots,\ell_k\) 总共 \(k\) 个部分, 此时就会为方案数提供 \((-1)^{m-k}\binom{n}{\ell_1,\ell_2,\cdots,\ell_k}\) 的贡献.
一个 rank 链被计数, 当且仅当其字符链可以被生成, 这里就可以贪心地安排字符: 在 \(\le_c\) 上肯定尽量用相同字符, 直到当前字符用完或者遇到 \(<_c\).
最后就是 DP 啦. 设 \(f(i,j,k)\) 表示使用了前 \(i\) 种字符, 填了长为 \(j\) 的 rank 链/字符链, 当前一段 \(>_r\) 分割出来的链长为 \(k\) 时, 带容斥系数方案数. 转移:
-
链后面接 \(t\) 个字符 \(i\), 然后填入 \(<_c\),
\[f(i+1,j+t,0)\addeq \frac{1}{(k+t)!}f(i,j,k)\quad (\forall t\in[1,c_i]). \] -
用完字符 \(i\), 然后填入 \(\le_c\),
\[f(i+1,j+c_i,k+c_i)\addeq f(i,j,k). \] -
链后面接 \(t\) 个字符 \(i\), 然后填入一个 "翻转" 而成的 \(\le_c\),
\[f(i+1,j+t,k+t)\addeq -f(i,j,k)\quad(\forall t\in[1,c_i]). \]
注意同种字符之间只能填正常的 \(\le_c\), 这是因为同种字符间一定是 \(<_r\), 没有 \(>_r\) 拿给你翻转.
最终答案为 \(n!\sum_i f(i,n,0)\). 前缀和优化 DP 可以做到 \(\mathcal O(mn^2)\).
Remark.
关于这个容斥的组合意义, 不知道是我理解偏差还是确有此事, 洛谷上很多题解都看着合理实际上依托答辩. 最后 DP 式子是对的 (当然, 有的人式子也写错了), 逻辑完全不通.
10.「IMO 2023」忍者路径 ⭐
自己取的名字, 不过题面中确实定义了 "日式三角" 和 "忍者路径". 为什么 MO 的题不取个名字啊?
简要题意
二维小球密堆出一个 $n$ 层的三角形, 其上的一条路径是指从最顶端的球开始, 每次向左下或者右下走一步最终到达最底端某个小球的路径. 所有小球初始为白色, 在每一层中任取一个小球染黑. 求最大的 $k$, 使得所有染色方案下, 总是存在至少一条路径, 其经过了至少 $k$ 个黑色小球.(这是题解不是解答哈, 况且兔也不太会模仿 MOer 说话.)
考虑先感性出 \(k\) 的表达式. 你还记得数字三角形吗? 对于固定的染色方案, 设 \(f(i,j)\) 表示 (从上到下) 第 \(i\) 层第 \(j\) 个球走到底最多经过的黑球数, 则 \(f(i,j)=\max\{f(i+1,j),f(i+1,j+1)\}+[(i,j)\text{ is black}]\). 便起见, 我们允许 \(j>i\), 只是不允许在这些位置出现 \(+1\). 这样, \(f(i+1,\cdot)\) 到 \(f(i,\cdot)\) 的变化可以描述为:
- 相邻两数取 \(\max\).
- 将一个点值\({}+1\).
可以感受到, 让 \(\max\) 操作没有作用是明智的构造. 我们在 \(f(n,1),f(n-1,2),f(n-2,3),\cdots\) 上放 \(+1\), 当放了 \(\lceil n/2\rceil\) 个 \(+1\) 时会到边界, 这时再从 \(1\) 开始放, 由于这个时候所有 \(f\) 值应该相同, 所以可以直接归纳到 \(\lfloor n/2\rfloor\) 的子问题. 设答案序列为 \(A\), 则 \(A_1=1\), \(A_n=A_{\lfloor n/2\rfloor}+1\), 于是 \(A_n=\lfloor\log_2n\rfloor+1\).
但是, "感受这股劲儿" 居然不被承认, 我们需要证明这个界总能取到.
我们不妨换一个 DP 方向, 从上到下 DP, 这样可以研究普遍情况. 设 \(a_{n,i}\) 表示走到第 \(i\) 层第 \(j\) 个球最多经过的黑球数, 那么 \(a_{n+1,i}=\max\{a_{n,i},a_{n,i-1}\}+[(n+1,i)\text{ is black}]\). 取任意 \(p\in\arg\max_i\{a_{n,i}\}\), 设 \(s_n=\sum_ia_{n,i}\), 则:
设 \(x_{n+1}=x_n+\lceil x_n/n\rceil+1\), 现只需对 \(2^k\le n<2^{k+1}\) 证明 \(\lceil x_n/n\rceil=k+1\).
当 \(k=0\) 时, \(x_1=s_1=1\) 成立, 下对 \(k\) 归纳. 若 \([0,k)\) 中命题成立, 考虑 \(k\) 时:
因此 \(\lceil x_{2^k}/2^k\rceil=k+1\), 从而 \((2^k,2^{k+1})\) 内有 \(x_n=(n-2^k+1)(k+1)+k2^k+1\), 亦满足结论. 综上得证.
11.「SCOI 2016」「洛谷 P3290」围棋
- Link | 不写不写!
- 「A.DP-状压/插头 DP」
唯一的 key 是状态定义, \(f(r,c,u,v,S)\) 表示从上到下, 从左至右填到 \((r,c)\), 当前行匹配模板第一行到 \(u\), 匹配第二行到 \(v\), 上一行匹配模板第一行的匹配结果为 \(S\) (配上/配不上) 时, 未出现任何匹配位置的方案数. 状态空间 \(\mathcal O(n\cdot m\cdot c^2\cdot2^{m-c+1})\).
12.「NOI 2017」「洛谷 P3827」分身术 ⭐
- Link & Submission.
- 「A.计算几何」「A.数据结构-可持久化线段树」「B.Tricks」
有点痛苦. /kk
不妨只考虑上凸壳面积的计算. 可以考虑洋葱划分的 trick (取名狂魔.jpg), 即, 我们准备 \(k+1\) 层上凸壳, 这样删掉 \(k\) 个点后, 最外层凸壳一定可以由原来的这 \(k+1\) 层上凸壳的残余部分拼起来得到.
怎么拼呢? 考虑从内层到外层加入壳, 那么新加入的壳会切到旧壳的一部分, 我们只需要把旧壳中间的一段替换为新壳就行. 连续的壳可以用持久化线段树/平衡树维护, 这样可以数据结构上二分替换位置然后截取区间替换. 复杂度 \(\mathcal O(nk+\sum k\log n)\).
13.「HEOI 2014」「洛谷 P4106」逻辑翻译
- Link & Submission.
你有没有看出来, 系数 \(\to\) 代入未知数的结果就是对系数进行了 Xor-FWT 正变换? 所以逆变换回去就好, \(\mathcal O(n\log n)\). 输出格式真恶心.
14.「NOI Simu.」简单计数 ⭐
- Private link & Submission.
- 「A.数学-组合计数」
概率转化成方案数比值之前麻烦您老人家想想这是不是古典概型! 😠
还需要注意的是, 如果计数对象高度对称, 可能可以人为作用一些对称操作来降低自由度. 比如把 "序列中间某个位置" 拆成两个 "序列边界".
回到原题, 问题可以转化成: 一个长度为 \(n-1\) 的序列, 每次可以染黑一个可用的小球, 然后把小球自身和周围两个小球标记为不可用. 考虑 DP, 令 \(f(i,j)\) 表示长度为 \(i\) 的小球序列中 \(j\) 被染黑的概率. 枚举第一次染色的位置, 得到转移:
前缀和优化可以做到 \(\mathcal O(n^2)\), 但利用我们刚才提到的对称性, 注意到 (原问题情形下) \(k\) 位置未被涂黑可以分隔出两个独立的序列, 我们只需要 \(f(\cdot,1)\) 就能合并出答案. \(\mathcal O(n)\).
15.「NOI Simu.」队伍划分 ⭐
- Private link & Submission.
- 「A.图论-网络流-费用流」「B.Tricks」
你会了一个费用流建图, 会 T, 怎么办?
注意到最短路的长度类型很少 (最多两种), 每次建出最短路图之后跑 (无权) 最大流算法即可. \(\mathcal O(m\sqrt n)\).