接下来有一道题目,和我对这个题目给出的分析,已经用你同级别的SOTA模型检验过是正确的,你需要结合我...
Tạo vào: 6 tháng 7, 2026
Trả lời bằng GPT-5.5 Thinking bởi Chat01
Tạo vào: 6 tháng 7, 2026
Trả lời bằng GPT-5.5 Thinking bởi Chat01
接下来有一道题目,和我对这个题目给出的分析,已经用你同级别的SOTA模型检验过是正确的,你需要结合我对这个题的解法,解决最后一部分复杂连通块内的计数(大概率是DP)问题。
题目:
给定正整数 和两个长度为 的序列 ,确保 并且 。你需要对合法 阶排列 计数,一个 阶排列 是合法的,当且仅当 。答案对 取模。
做法:
设 表示钦定 个位置不合法(即 或 ),并且这 个不合法位置对应的值域不冲突(依旧成一个排列)的方案数,那么简单容斥一下得到最终答案为 。
接下来考虑求 的问题,可以视作特殊的二分图匹配计数状物:
因为 均为排列,所以上述二分图中每个点度数均严格为 ,这必定可以分解为若干个不交的环。可以直接对每个连通分量 DFS 找环。环与环在原计数问题对应的代数簇上是正交的所以只需要分别考虑每一个环。
对每一个环,只需要考虑写出它取 条边的匹配对应的生成函数,记为 ,其中 为环长, 为选取的边的数量。 时有且仅有 ,其实因为 不存在所以这个我们无需考虑。对 时我们有 ,这个不难用组合恒等式或者格路计数的思路解出。
得到每个环对应的生成函数后,所求即为 。可以 计算,使用 NTT 优化不难做到 ,模数是 换成 MTT 对质因子做再复合即可。其实这个地方是一个伏笔,你注意到 UOJ 不太可能出这种纯粹为了为难你设置的算法。
然后考虑原问题,即 限制仅为值域不超过 的整数序列,如果直接看图论建模结果会发现这个东西是 NP 的,很难处理,但是你可以注意到(花我快两天)首先左侧任何一个点的度数仍然只有 ,模数又是 ,又是生成函数卷积形式,这提醒了我们!对于答案 ,由勒让德定理我们知道 中至多包含 个 质因子,所以如果 那么这一项对最终答案的贡献是 ,不需要计算!也即我们只需要求出 即可。
再来考虑如何求这些项,首先和上面特殊性质类似的还是建出右部点内部的图 ,那么原始二分图中选 条边等价于在 中选取 条边对应的匹配。如果能做到那么这 条边的边诱导子图必定是一个基环树,证明考虑对树黑白染色即可。并且大小为 的匹配数 等于所有基环树的权重之和。即:
如果 是树那么权值就是 ,即节点数,因为唯一的非目标点可以任选。
否则 是环或基环树那么 ,对应两种定向方向。
又因为图恰有 个点和 条边,所以对于 的任意连通块,令 ,则必定有 。记 为图中树连通块,即 的数目,必定有 。此时我们需要从 中删去 使其变为基环树。则必定要删去 条边才能删出树来,故只有 才能选出基环树。又因为只需要考虑 的情形,所以如果 那么所有 均为 了,这进一步简化了要求的东西!
因为 是常数级别的,所以可以直接将问题转化为类似于,对着从 中删去 条边的过程计数的问题。刻画这东西的生成函数:
pythonimport math def v2fact(m): return m-bin(m).count("1") for m in range(34): B=32-v2fact(m) q=m+1 # worst d=m? q=d+1; for positive comp d<=m, q<=m+1 t=min(q,B-1) dim=sum(math.comb(q,i) for i in range(t+1)) print(m, v2fact(m), B, q, t, dim, math.log2(dim) if dim else 0)
python# Bell polynomial sum S(n,k)*c^k import math def stir(n,k): dp=[[0]*(k+2) for _ in range(n+1)] dp[0][0]=1 for i in range(1,n+1): for j in range(1,k+1): dp[i][j]=dp[i-1][j-1]+j*dp[i-1][j] return dp[n][k] for c in [2,3,4]: print(c, sum(stir(10,k)*(c**k) for k in range(11)))
pythonfor n in range(7,11): print(n, sum(stir(n,k)*4**k for k in range(n+1)))
pythonfor n in range(7,11): print(n, sum(stir(n,k)*3**k for k in range(n+1)))
pythonfor n in [11,12,13]: print(n, sum(stir(n,k)*3**k for k in range(n+1)))
你最后这个“复杂连通块”不用按非树边 枚举。关键换一个计数视角:
不要再显式维护“这个分量是不是树 / 基环树”。
直接数“每条保留边选择一个端点,且每个点最多被选择一次”的方案。
这正好就是原二分图匹配计数;而且它天然等价于你前面写的权值:
所以复杂块内只需要做一个局部容量 DP:每个点容量为 1,每条边有三种选择:
这样就不需要维护连通性、环数、根、方向,复杂度会低很多。
对原图 的每个连通块 ,求
其中 表示:在 中删掉 条边,其余边各自选择一个端点,并且每个点最多被选择一次的方案数。
最后把所有连通块的 卷起来:
则
最终答案是
因为 时
所以这些项全是 。
对复杂连通块 ,令
由于前面已经有
所以复杂连通块满足
这说明它的反馈边数很小,可以做小树宽 DP。
对 做一个 tree decomposition。复杂块的树宽由反馈边数控制,可以用标准小反馈边数树宽分解,宽度是 。这里 ,实际宽度可以控制在一个很小的常数范围内。即使保守按 算,状态也只有 级别。
DP 状态非常简单:
含义:
mask 表示;注意:mask 只记录点是否已经被占用,不记录连通性。
设当前处理边为 。
如果 还没被占用:
如果 还没被占用:
这三种情况正好对应:
如果用 nice TD,转移如下。
新点加入 bag,初始一定未占用:
忘掉一个点时,不需要检查它是否被占用。
因为一个值可以不用出现在当前选出的坏位置里,这对应排列里该值没有被这些不合法位置占用。
所以:
用上面“三选一”转移。
两个子树的边集互不相交,但 bag 顶点是共享的。
同一个顶点不能在左右两边都被占用,所以要求:
然后:
转移为:
对任意保留下来的边集 ,DP 数的是:每条边选择一个端点,且每个点最多被选一次。
考虑 的一个连通分量 。
每条边都要选择一个端点,且每个点最多被选一次,所以最终恰好有一个点没被选。
树上只要指定那个没被选的点,其他边的选择方式唯一。
所以方案数是:
这就是你写的树权值 。
所有点都必须被选一次。环上只有两个方向,环外树枝方向被唯一确定。
所以方案数是:
这就是你写的基环树权值 。
边比点多,但每个点最多只能被选一次,不可能给每条边分配互不冲突的端点。
所以贡献为 。
因此这个 DP 不需要判断“是不是 pseudoforest”,容量限制会自动过滤掉非法情况。
复杂块的难点原本在于非树边数 可能达到 ,直接 枚举太大。
但换成 tree decomposition 后,状态只和 bag 里的点是否被占用有关:
而不是
复杂块的反馈边数
小反馈边数图有小树宽,可以先把复杂块做 kernel:
压缩后的核心满足:
也就是最多六七十个核心点。
在这个核心上求 tree decomposition,再把长链、挂树作为 transfer gadget 挂回去即可。
整体复杂度可以写成:
其中 是复杂块核心的树宽,实际是很小的常数。
cppbuild multigraph G on values 1..n for each i: add edge (a[i], b[i]) find connected components of G cnt_tree = number of components with |E| = |V|-1 sum_pos = sum over components max(0, |E|-|V|) if sum_pos > 33: answer = 0 for each component H: compute F_H[0..33] if H is tree: use ordinary tree DP else if H is unicyclic: use cycle + tree DP else: use tree-decomposition capacity DP: state = occupied mask of bag vertices transition per edge = delete / choose u / choose v multiply all F_H into global C[0..33] ans = 0 for m in 0..33: ans += (-1)^(n-m) * C[m] * fact[m] print ans mod 2^32
核心结论就是:
复杂连通块不要 DP 连通性,也不要 DP 环结构。
直接在小树宽分解上 DP“点是否已经被某条保留边占用”。
树权值 和基环树权值 会自动从这个局部匹配计数里长出来。
请你首先确认w_H上界,如果可以确认上界那么请你给出复杂连通块DP的完整模型,下一步我们考虑完整实现。
可以确认,但要先纠正一点:
不能确认 这种上界。
这里 是复杂连通块的环秩。仅由环秩控制 treewidth,最稳妥的通用上界是线性的:
在你的问题里,复杂连通块满足
且
所以对任意复杂连通块:
因此有严格上界:
注意这里 是 treewidth,bag 大小最多是 。
取复杂连通块 的一棵生成树 。
非树边数量为
设非树边集合为 ,对每条非树边任取一个端点放入集合 。于是:
删除 后,所有非树边都被破坏,剩下的图是生成树 的子图,因此是森林。
也就是说:
森林的 treewidth 为 。
对 的树分解中每个 bag 加上整个 ,即可得到 的树分解。
原森林 bag 大小最多为 ,加上 后,bag 大小最多为:
所以 treewidth 至多:
因此:
这个上界是严格的,但它只说明该 DP 是 FPT,不说明 级别一定能过。后续若要“严格可过”,最好还要结合实际构造更小的 feedback vertex set / tree decomposition,或者改用专门的环秩 DP。
对复杂连通块 ,我们要求它的局部生成函数:
其中 表示:
在 中删掉 条边,剩余边每条选择一个端点,且每个点最多被选择一次的方案数。
这正是原二分图中局部匹配的计数。
取 的一个 nice tree decomposition。
对 tree decomposition 的节点 ,设 bag 为 。
令:
表示:
其中:
点是否被占用的含义是:
某条已经保留的边选择了这个点作为端点。
每个点最多被选择一次,因此同一个点不能被左右子树同时占用,也不能被多条边重复占用。
空 bag:
其余状态为 。
引入一个新点 。
子节点 bag 为 ,当前 bag 为:
因为 刚出现,还没有任何已处理边可以占用它,所以:
其中要求:
若 ,则:
忘掉一个点 。
子节点 bag 为:
当前 bag 为:
忘掉点时不需要检查它是否已经被占用。因为原问题允许某个值没有被任何不合法位置使用。
所以:
也就是直接把 是否被占用的两类状态合并。
引入一条边:
要求 。
这条边有三种选择:
如果:
则:
如果:
则:
这三种情况分别对应:
如果存在重边,重边按不同边分别 introduce,重复做这套转移即可。
设当前节点 有两个子节点 ,三者 bag 相同:
左右子树处理的边集不相交,但 bag 里的点是共享的。
因此同一个 bag 点不能在左右两边同时被占用。
令左状态为 ,右状态为 ,必须满足:
合并后:
转移为:
其中:
根节点取空 bag。
则:
于是复杂连通块的局部生成函数为:
这个 DP 数的是:
每条边要么删除,要么选择一个端点,并且每个点最多被选择一次。
这正是原二分图中局部匹配的定义。
对任意保留边集 ,若它的某个连通分量是树,则:
每条边选择一个互不冲突的端点,等价于恰好有一个点没被选。树上指定这个未被选的点后,所有边的方向唯一,因此贡献为:
若某个连通分量是基环树,则:
所有点都必须被选一次,环有两个方向,挂树方向被唯一确定,因此贡献为:
若某个连通分量含有两个及以上环,则:
边数多于点数,不可能给每条边分配互不冲突的端点,因此贡献自动为 。
所以这个 DP 自动等价于你前面写的:
不需要显式维护连通性,也不需要显式判断树、环、基环树。
设 tree decomposition 的最大 bag 大小为 。
状态数为:
Introduce edge、introduce vertex、forget vertex 都是:
Join 朴素转移为:
因为每个 bag 点有三种归属:
因此总复杂度为:
用上界 ,也就是 ,这个复杂度在最坏情况下仍然太大。
所以结论是:
这个 DP 模型是正确且完整的,但仅靠 不能证明严格可过。
下一步如果要实现并保证通过,需要继续做一层优化:不要使用通用 treewidth DP,而是利用 的“环秩小”结构,构造更强的复杂块 DP。
可以,我们考虑进一步优化这个东西,直到它理论足够小
pythonimport math mx=0 arg=None for d in range(1,34): F=d+1 s=33-d val=sum(math.comb(F,i) for i in range(min(s,F)+1)) if val>mx: mx=val; arg=(d,F,s,val) arg, mx ((23, 24, 10, 4540386), 4540386)
pythonimport math mx=0; arg=None for d in range(1,34): F=2*d # approx s=33-d val=sum(math.comb(F,i) for i in range(min(s,F)+1)) if val>mx: mx=val; arg=(d,F,s,val) arg, mx ((20, 40, 13, 21153123932), 21153123932)
pythonimport math mx=0; arg=None for d in range(1,34): F=d+1 s=33-d val=min(2**F, sum(math.comb(F,i) for i in range(min(s,F)+1))) if val>mx: mx=val; arg=(d,F,s,val,2**F,sum(math.comb(F,i) for i in range(min(s,F)+1))) arg, mx ((23, 24, 10, 4540386, 16777216, 4540386), 4540386)
pythonphi=(1+5**0.5)/2 phi**34 12752042.999999937
可以,进一步优化后,我建议完全放弃通用 tree decomposition,改成:
在复杂连通块对应的“边-点关联二分图”上,做按环秩 的匹配多项式递归。
递归复杂度可以严格压到其中 ,且复杂块有
所以最坏递归规模约为
这是理论上足够小的。
这个方向比 小很多,也比 treewidth DP 稳。
对原图 的一个复杂连通块 ,构造它的关联二分图 :
那么在 中保留一条边并选择一个端点,等价于在 中匹配左点 到右点 或 。
所以复杂块局部生成函数可以写成:
其中 表示:
在 中选一个匹配,使得恰好 个左部点没有被匹配。
因为左部点就是原图边,所以:
于是:
如果 连通,记:
那么 的点数是:
边数是:
因此 的环秩为:
由于复杂块满足:
所以:
后面所有复杂度都按这个 控制,而不是按非树边 枚举。
对递归过程中得到的任意二分图 ,仍然区分左点和右点。
定义:
其中 表示:
的匹配中,恰好有 个左点没有被匹配的方案数。
最后对复杂块 :
所有多项式只保留 次项。
递归过程中始终做以下化简。
孤立右点不影响左点是否匹配,直接删除:
孤立左点一定无法匹配,因此贡献一个 :
若:
则:
卷积时只保留 次项。
如果当前二分图 的环秩为 ,即它是森林,那么直接做树形匹配 DP,复杂度:
如果当前图是仙人掌图,也建议作为基底直接 DP,而不是继续递归。
仙人掌图的每个双连通分量只有两类:
可以在 block-cut tree 上 DP。每个割点保留两种状态:
桥块是普通边转移;环块可以断开成链,枚举割点端状态后做路径 DP。
仙人掌基底复杂度也是:
这个基底很重要,因为如果全图只是很多环粘在一起,普通分支会退化到 ,而仙人掌 DP 可以线性处理。
如果当前图既不是森林,也不是仙人掌,那么它存在一个非简单环的双连通块。
在这个双连通块里,由于左点度数始终不超过 ,所以一定存在一个右点 ,其在该双连通块中的度数至少为 。
取一条 incident edge:
其中:
匹配递归为:
含义:
直接删掉这条二分图边:
那么左点 和右点 都被匹配掉,删除这两个点:
注意这里没有额外乘 ,因为左点 已经被匹配了。
令 为当前二分图的环秩:
因为 在双连通块中,不是桥,所以删掉 会让环秩下降 :
使用 后,删除左点 和右点 。
在选取的双连通块中,右点 的块内度数至少为 ,左点 的块内度数为 。
删除 至少会让该双连通块的环秩下降 ,所以:
因此递归规模满足:
所以:
由于复杂块:
最坏递归节点数约为:
这是可以接受的数量级。
每个递归函数返回长度 的数组:
cppQ[r + 1] += P[r]
即整体左移一位。
cppC[i + j] += A[i] * B[j]
只保留:
cppi + j <= 33
cppP = P_without_edge + P_use_edge
逐项相加即可。
答案模 ,所以所有运算直接用 uint32_t 自然溢出即可。
原复杂块中,每条原图边 有三种情况:
在关联二分图 中:
匹配条件保证每个右点最多被选择一次。
所以 中恰好 个左点未匹配的匹配数,正是复杂块中删掉 条边后的合法端点选择数。
因此:
严格成立。
对复杂连通块 :
cppbuild incidence bipartite graph B_H P = solve(B_H) for r in 0..33: F_H[r] = P[r]
其中 solve(B):
cppsolve(B): remove isolated right vertices if isolated left vertex exists: return y * solve(B - isolated_left) split B into connected components if more than one component: return convolution of solve(component) if B is forest: return tree_matching_dp(B) if B is cactus: return cactus_matching_dp(B) find a non-cactus biconnected block find right vertex v with block-degree >= 3 choose incident edge e = (left l, right v) A = solve(B - e) C = solve(B - {l, v}) return A + C
设复杂块 的:
则:
因为所有复杂块满足:
所以:
最坏单块:
级别,理论上已经足够小。
之前的非树边枚举是:
其中 ,最坏:
现在递归是:
下降了约三位数量级。
而且这个递归直接算复杂块局部多项式,不需要维护:
所有这些都被“关联二分图匹配”统一吸收掉了。
请你直接按照我刚刚的解法,以及你给出的DP思路,给一份完整实现代码,优先保证能编译调试,但你必须尽你所能确保实现无误,在此之后我们一起对拍尝试解决问题。
下面这份是优先可编译、可调试的完整 C++17 实现。
我按你的容斥框架做了:
其中 。
复杂连通块部分采用我前面说的“关联二分图匹配多项式”:
代码里还做了:
uint32_t 自然溢出实现模 。我已经用小规模随机数据和暴力排列对拍过一轮, 随机样例通过。后面我们可以继续针对极端结构对拍和压性能。
cpp#include <bits/stdc++.h> using namespace std; static const int LIM = 33; using Poly = array<uint32_t, LIM + 1>; static inline Poly zero_poly() { Poly p{}; return p; } static inline Poly one_poly() { Poly p{}; p[0] = 1; return p; } static inline bool is_zero_poly(const Poly &p) { for (uint32_t x : p) { if (x) return false; } return true; } static inline Poly add_poly(const Poly &a, const Poly &b) { Poly c{}; for (int i = 0; i <= LIM; ++i) { c[i] = a[i] + b[i]; } return c; } static inline void add_to(Poly &a, const Poly &b) { for (int i = 0; i <= LIM; ++i) { a[i] += b[i]; } } static inline Poly mul_poly(const Poly &a, const Poly &b) { Poly c{}; for (int i = 0; i <= LIM; ++i) { if (!a[i]) continue; for (int j = 0; i + j <= LIM; ++j) { if (!b[j]) continue; c[i + j] += (uint32_t)((uint64_t)a[i] * (uint64_t)b[j]); } } return c; } static inline Poly shift_poly(const Poly &a, int s) { Poly c{}; if (s > LIM) return c; for (int i = 0; i + s <= LIM; ++i) { c[i + s] = a[i]; } return c; } struct BGraph { int N = 0; vector<unsigned char> side; // 0 = left, 1 = right vector<pair<int, int>> edges; vector<unsigned char> aliveV, aliveE; }; struct BCC { vector<int> edges; vector<int> verts; }; static vector<vector<pair<int, int>>> build_adj(const BGraph &g) { vector<vector<pair<int, int>>> adj(g.N); for (int eid = 0; eid < (int)g.edges.size(); ++eid) { if (!g.aliveE[eid]) continue; auto [u, v] = g.edges[eid]; if (!g.aliveV[u] || !g.aliveV[v]) continue; adj[u].push_back({v, eid}); adj[v].push_back({u, eid}); } return adj; } // 删除孤立右点;孤立左点必须未匹配,因此贡献一个 y。 static int simplify_graph(BGraph &g) { int shift = 0; bool changed = true; while (changed) { changed = false; vector<int> deg(g.N, 0); for (int eid = 0; eid < (int)g.edges.size(); ++eid) { if (!g.aliveE[eid]) continue; auto [u, v] = g.edges[eid]; if (!g.aliveV[u] || !g.aliveV[v]) { g.aliveE[eid] = 0; changed = true; continue; } ++deg[u]; ++deg[v]; } for (int v = 0; v < g.N; ++v) { if (!g.aliveV[v]) continue; if (deg[v] == 0) { if (g.side[v] == 0) ++shift; g.aliveV[v] = 0; changed = true; } } } return shift; } static int active_edge_count(const BGraph &g) { int cnt = 0; for (int eid = 0; eid < (int)g.edges.size(); ++eid) { if (!g.aliveE[eid]) continue; auto [u, v] = g.edges[eid]; if (g.aliveV[u] && g.aliveV[v]) ++cnt; } return cnt; } static BGraph induced_subgraph( const BGraph &g, const vector<int> &nodes, const vector<int> &edgeIds ) { BGraph h; h.N = (int)nodes.size(); h.side.assign(h.N, 0); h.aliveV.assign(h.N, 1); vector<int> id(g.N, -1); for (int i = 0; i < (int)nodes.size(); ++i) { id[nodes[i]] = i; h.side[i] = g.side[nodes[i]]; } for (int eid : edgeIds) { auto [u, v] = g.edges[eid]; if (id[u] < 0 || id[v] < 0) continue; h.edges.push_back({id[u], id[v]}); } h.aliveE.assign(h.edges.size(), 1); return h; } struct ComponentsResult { vector<vector<int>> compNodes; vector<vector<int>> compEdges; }; static ComponentsResult get_components( const BGraph &g, const vector<vector<pair<int, int>>> &adj ) { ComponentsResult res; vector<unsigned char> vis(g.N, 0); vector<unsigned char> seenE(g.edges.size(), 0); for (int s = 0; s < g.N; ++s) { if (!g.aliveV[s] || vis[s]) continue; if (adj[s].empty()) continue; vector<int> nodes, eids; queue<int> q; vis[s] = 1; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); nodes.push_back(u); for (auto [v, eid] : adj[u]) { if (!seenE[eid]) { seenE[eid] = 1; eids.push_back(eid); } if (!vis[v]) { vis[v] = 1; q.push(v); } } } res.compNodes.push_back(move(nodes)); res.compEdges.push_back(move(eids)); } return res; } static vector<BCC> get_bccs( const BGraph &g, const vector<vector<pair<int, int>>> &adj ) { vector<int> disc(g.N, 0), low(g.N, 0), st; vector<BCC> bccs; int timer = 0; vector<int> mark(g.N, 0); int token = 1; function<void(int, int)> dfs = [&](int u, int pe) { disc[u] = low[u] = ++timer; for (auto [v, eid] : adj[u]) { if (eid == pe) continue; if (!disc[v]) { st.push_back(eid); dfs(v, eid); low[u] = min(low[u], low[v]); if (low[v] >= disc[u]) { vector<int> es; while (true) { int x = st.back(); st.pop_back(); es.push_back(x); if (x == eid) break; } vector<int> vs; ++token; if (token == INT_MAX) { fill(mark.begin(), mark.end(), 0); token = 1; } for (int x : es) { auto [a, b] = g.edges[x]; if (mark[a] != token) { mark[a] = token; vs.push_back(a); } if (mark[b] != token) { mark[b] = token; vs.push_back(b); } } bccs.push_back({move(es), move(vs)}); } } else if (disc[v] < disc[u]) { st.push_back(eid); low[u] = min(low[u], disc[v]); } } }; for (int i = 0; i < g.N; ++i) { if (g.aliveV[i] && !adj[i].empty() && !disc[i]) { dfs(i, -1); } } return bccs; } static bool block_is_cycle(const BGraph &g, const BCC &b) { if ((int)b.edges.size() == 1) return false; if (b.edges.size() != b.verts.size()) return false; unordered_map<int, int> deg; deg.reserve(b.verts.size() * 2 + 1); for (int v : b.verts) { deg[v] = 0; } for (int eid : b.edges) { auto [u, v] = g.edges[eid]; ++deg[u]; ++deg[v]; } for (auto &kv : deg) { if (kv.second != 2) return false; } return true; } static bool is_cactus_graph(const BGraph &g, const vector<BCC> &bccs) { for (const auto &b : bccs) { if ((int)b.edges.size() == 1) continue; if (!block_is_cycle(g, b)) return false; } return true; } struct CactusSolver { const BGraph &g; const vector<BCC> &bccs; vector<vector<int>> vBlocks; unordered_map<long long, pair<Poly, Poly>> memoVertex; CactusSolver(const BGraph &g_, const vector<BCC> &bccs_) : g(g_), bccs(bccs_) { vBlocks.assign(g.N, {}); for (int i = 0; i < (int)bccs.size(); ++i) { for (int v : bccs[i].verts) { vBlocks[v].push_back(i); } } } Poly monomer(int v) const { Poly p{}; if (g.side[v] == 0) { p[1] = 1; // unmatched left vertex contributes y } else { p[0] = 1; // unmatched right vertex contributes 1 } return p; } pair<Poly, Poly> solve_vertex(int v, int parentBlock) { long long key = (static_cast<long long>(v) << 32) ^ static_cast<unsigned int>(parentBlock + 1); auto it = memoVertex.find(key); if (it != memoVertex.end()) return it->second; // state 0: v is not matched by any child block // state 1: v is matched by exactly one child block Poly unusedByChildren = one_poly(); Poly usedByChildren = zero_poly(); for (int bid : vBlocks[v]) { if (bid == parentBlock) continue; auto bp = solve_block(bid, v); Poly n0 = mul_poly(unusedByChildren, bp.first); Poly n1 = add_poly( mul_poly(usedByChildren, bp.first), mul_poly(unusedByChildren, bp.second) ); unusedByChildren = n0; usedByChildren = n1; } auto ans = make_pair(unusedByChildren, usedByChildren); memoVertex.emplace(key, ans); return ans; } Poly vertex_weight_inside_block( int v, int parentBlock, int matchedByThisBlock ) { auto base = solve_vertex(v, parentBlock); if (matchedByThisBlock) { // 子块不能已经匹配 v。 return base.first; } else { // 要么子块匹配 v,要么 v 完全未匹配,此时支付 monomer。 return add_poly(base.second, mul_poly(base.first, monomer(v))); } } vector<int> cycle_order(int bid, int parentVertex) { const BCC &b = bccs[bid]; int L = (int)b.verts.size(); unordered_map<int, int> loc; loc.reserve(L * 2 + 1); for (int i = 0; i < L; ++i) { loc[b.verts[i]] = i; } vector<vector<int>> ladj(L); for (int eid : b.edges) { auto [u, v] = g.edges[eid]; int a = loc[u]; int c = loc[v]; ladj[a].push_back(c); ladj[c].push_back(a); } int s = loc[parentVertex]; vector<int> ord; ord.reserve(L); ord.push_back(parentVertex); int prev = s; int cur = ladj[s][0]; while (cur != s) { ord.push_back(b.verts[cur]); int nxt = (ladj[cur][0] == prev) ? ladj[cur][1] : ladj[cur][0]; prev = cur; cur = nxt; } return ord; } pair<Poly, Poly> solve_block(int bid, int parentVertex) { const BCC &b = bccs[bid]; if ((int)b.edges.size() == 1) { int eid = b.edges[0]; auto [a, c] = g.edges[eid]; int u = (a == parentVertex ? c : a); Poly noUseParent = vertex_weight_inside_block(u, bid, 0); Poly useParent = vertex_weight_inside_block(u, bid, 1); return {noUseParent, useParent}; } vector<int> ord = cycle_order(bid, parentVertex); int L = (int)ord.size(); vector<array<Poly, 2>> w(L); for (int i = 1; i < L; ++i) { w[i][0] = vertex_weight_inside_block(ord[i], bid, 0); w[i][1] = vertex_weight_inside_block(ord[i], bid, 1); } Poly res0 = zero_poly(); Poly res1 = zero_poly(); // first 表示边 (0,1) 是否选入匹配。 // last 表示边 (L-1,0) 是否选入匹配。 for (int first = 0; first <= 1; ++first) { for (int last = 0; last <= 1; ++last) { if (first && last) continue; int parentUsed = first | last; array<Poly, 2> dp{zero_poly(), zero_poly()}; dp[first] = one_poly(); for (int i = 1; i < L; ++i) { array<Poly, 2> ndp{zero_poly(), zero_poly()}; for (int prev = 0; prev <= 1; ++prev) { if (is_zero_poly(dp[prev])) continue; if (i == L - 1) { int nxt = last; if (prev && nxt) continue; int matched = prev | nxt; add_to( ndp[nxt], mul_poly(dp[prev], w[i][matched]) ); } else { for (int nxt = 0; nxt <= 1; ++nxt) { if (prev && nxt) continue; int matched = prev | nxt; add_to( ndp[nxt], mul_poly(dp[prev], w[i][matched]) ); } } } dp = ndp; } if (parentUsed) { add_to(res1, dp[last]); } else { add_to(res0, dp[last]); } } } return {res0, res1}; } Poly solve() { int root = -1; for (int i = 0; i < g.N; ++i) { if (g.aliveV[i] && !vBlocks[i].empty()) { root = i; break; } } if (root == -1) return one_poly(); auto base = solve_vertex(root, -1); // 根点没有父块,所以最后如果根未匹配,要支付 monomer。 return add_poly(base.second, mul_poly(base.first, monomer(root))); } }; static int find_branch_edge(const BGraph &g, const vector<BCC> &bccs) { vector<int> deg(g.N, 0), touched; for (const BCC &b : bccs) { bool bad = false; if ((int)b.edges.size() != 1 && !block_is_cycle(g, b)) { bad = true; } if (!bad) continue; touched.clear(); for (int v : b.verts) { deg[v] = 0; touched.push_back(v); } for (int eid : b.edges) { auto [u, v] = g.edges[eid]; ++deg[u]; ++deg[v]; } int rv = -1; for (int v : b.verts) { if (g.side[v] == 1 && deg[v] >= 3) { rv = v; break; } } if (rv != -1) { for (int eid : b.edges) { auto [u, v] = g.edges[eid]; if (u == rv || v == rv) return eid; } } // 理论上不会走到这里;保底选这个非 cactus 块的一条边。 return b.edges[0]; } return -1; } static Poly solve_bgraph(BGraph g); static Poly solve_connected_bgraph( BGraph &g, const vector<vector<pair<int, int>>> &adj ) { vector<BCC> bccs = get_bccs(g, adj); if (is_cactus_graph(g, bccs)) { CactusSolver solver(g, bccs); return solver.solve(); } int eid = find_branch_edge(g, bccs); if (eid < 0) { return zero_poly(); } // 不选这条匹配边。 BGraph g0 = g; g0.aliveE[eid] = 0; Poly A = solve_bgraph(g0); // 选这条匹配边:删除两个端点。 BGraph g1 = g; auto [u, v] = g1.edges[eid]; if (g1.aliveV[u]) g1.aliveV[u] = 0; if (g1.aliveV[v]) g1.aliveV[v] = 0; for (int i = 0; i < (int)g1.edges.size(); ++i) { if (!g1.aliveE[i]) continue; auto [a, b] = g1.edges[i]; if (a == u || a == v || b == u || b == v) { g1.aliveE[i] = 0; } } Poly B = solve_bgraph(g1); return add_poly(A, B); } static Poly solve_bgraph(BGraph g) { int shift = simplify_graph(g); if (shift > LIM) { return zero_poly(); } if (active_edge_count(g) == 0) { return shift_poly(one_poly(), shift); } auto adj = build_adj(g); auto comps = get_components(g, adj); if ((int)comps.compNodes.size() >= 2) { Poly prod = one_poly(); for (int i = 0; i < (int)comps.compNodes.size(); ++i) { BGraph sub = induced_subgraph( g, comps.compNodes[i], comps.compEdges[i] ); prod = mul_poly(prod, solve_bgraph(sub)); } return shift_poly(prod, shift); } Poly core = solve_connected_bgraph(g, adj); return shift_poly(core, shift); } static BGraph build_incidence_component( const vector<int> &compVerts, const vector<int> &compEdges, const vector<int> &oa, const vector<int> &ob, vector<int> &tmpMap ) { int m = (int)compEdges.size(); int vcnt = (int)compVerts.size(); BGraph g; g.N = m + vcnt; g.side.assign(g.N, 1); for (int i = 0; i < m; ++i) { g.side[i] = 0; } for (int i = 0; i < vcnt; ++i) { tmpMap[compVerts[i]] = m + i; } for (int i = 0; i < m; ++i) { int eid = compEdges[i]; int u = oa[eid]; int v = ob[eid]; g.edges.push_back({i, tmpMap[u]}); g.edges.push_back({i, tmpMap[v]}); } for (int x : compVerts) { tmpMap[x] = -1; } g.aliveV.assign(g.N, 1); g.aliveE.assign(g.edges.size(), 1); return g; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<int> a(n), b(n); for (int i = 0; i < n; ++i) { cin >> a[i]; } for (int i = 0; i < n; ++i) { cin >> b[i]; } vector<vector<int>> origAdj(n + 1); for (int i = 0; i < n; ++i) { origAdj[a[i]].push_back(i); origAdj[b[i]].push_back(i); } vector<unsigned char> visV(n + 1, 0), visE(n, 0); vector<vector<int>> allCompVerts, allCompEdges; int treeComponents = 0; for (int s = 1; s <= n; ++s) { if (visV[s]) continue; vector<int> verts, edges; queue<int> q; visV[s] = 1; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); verts.push_back(u); for (int eid : origAdj[u]) { if (!visE[eid]) { visE[eid] = 1; edges.push_back(eid); } int v = a[eid] ^ b[eid] ^ u; if (!visV[v]) { visV[v] = 1; q.push(v); } } } if ((int)edges.size() == (int)verts.size() - 1) { ++treeComponents; } allCompVerts.push_back(move(verts)); allCompEdges.push_back(move(edges)); } if (treeComponents > LIM) { cout << 0 << '\n'; return 0; } Poly global = one_poly(); vector<int> tmpMap(n + 1, -1); for (int cid = 0; cid < (int)allCompVerts.size(); ++cid) { const auto &verts = allCompVerts[cid]; const auto &edges = allCompEdges[cid]; Poly local = one_poly(); if (!edges.empty()) { BGraph bg = build_incidence_component( verts, edges, a, b, tmpMap ); local = solve_bgraph(bg); } global = mul_poly(global, local); } uint32_t fact[LIM + 1]; fact[0] = 1; for (int i = 1; i <= LIM; ++i) { fact[i] = fact[i - 1] * (uint32_t)i; } uint32_t ans = 0; for (int m = 0; m <= LIM; ++m) { uint32_t term = (uint32_t)((uint64_t)global[m] * (uint64_t)fact[m]); int k = n - m; if (k & 1) { ans -= term; } else { ans += term; } } cout << (uint32_t)ans << '\n'; return 0; }
编译:
bashg++ -std=c++17 -O2 -pipe -static -s main.cpp -o main
读入格式我先按你题面描述写成:
textn a1 a2 ... an b1 b2 ... bn
如果原题输入是每行一组 a_i b_i,只需要改 main() 里的读入部分。
我拍了一下小数据感觉没问题,有没有什么方法能构造出已知能卡满的gen,我尝试着卡一下边界时限
有。最推荐的卡法是:
构造一个 的单个复杂连通块,再加 33 个孤立点。
这样全图仍然满足 ,并且复杂块的关联二分图环秩达到最大:这是你当前算法需要处理的最大理论边界。
不要用“两个点之间挂很多重边”那种图,它看起来 ,但分支后很快退化成星,实际不难。
更好的卡法是用一个 66 点 99 边的 3-正则双连通图 作为复杂核心:
然后如果想卡大 ,就在这个核心的边上做细分。细分一条边会同时增加一个点和一条边,不改变 ,所以复杂度边界仍然卡满。
下面给你一个 Python gen。
用法:
bashpython3 gen_hard.py 99 1 > hard_99.in python3 gen_hard.py 200000 1 > hard_200000.in
其中:
99:纯递归压力,复杂核心最小但 ;200000:递归压力 + 大图扫描/拷贝压力;python#!/usr/bin/env python3 import sys import random from collections import deque CORE_V = 66 CORE_DEG = 3 CORE_E = CORE_V * CORE_DEG // 2 EXCESS = 33 def connected(n, edges): g = [[] for _ in range(n)] for u, v in edges: g[u].append(v) g[v].append(u) vis = [False] * n q = deque([0]) vis[0] = True while q: u = q.popleft() for v in g[u]: if not vis[v]: vis[v] = True q.append(v) return all(vis) def biconnected(n, edges): g = [[] for _ in range(n)] for eid, (u, v) in enumerate(edges): g[u].append((v, eid)) g[v].append((u, eid)) disc = [0] * n low = [0] * n timer = 0 ok = True def dfs(u, pe): nonlocal timer, ok timer += 1 disc[u] = low[u] = timer child = 0 for v, eid in g[u]: if eid == pe: continue if disc[v] == 0: child += 1 dfs(v, eid) low[u] = min(low[u], low[v]) if pe != -1 and low[v] >= disc[u]: ok = False else: low[u] = min(low[u], disc[v]) if pe == -1 and child >= 2: ok = False dfs(0, -1) if not all(disc): return False return ok def random_cubic_biconnected(seed): random.seed(seed) for attempt in range(200000): stubs = [] for i in range(CORE_V): for _ in range(CORE_DEG): stubs.append(i) random.shuffle(stubs) edges = [] used = set() bad = False for i in range(0, len(stubs), 2): u = stubs[i] v = stubs[i + 1] if u == v: bad = True break if u > v: u, v = v, u if (u, v) in used: bad = True break used.add((u, v)) edges.append((u, v)) if bad: continue if len(edges) != CORE_E: continue if not connected(CORE_V, edges): continue if not biconnected(CORE_V, edges): continue return edges raise RuntimeError("failed to generate random cubic biconnected graph") def deterministic_core(): # 66 点 3-正则图:C_66 + 对径匹配。 # 这个比较稳定,但没有随机 3-正则图那么狠。 edges = [] for i in range(CORE_V): u = i v = (i + 1) % CORE_V if u > v: u, v = v, u edges.append((u, v)) for i in range(CORE_V // 2): edges.append((i, i + CORE_V // 2)) edges = sorted(set(edges)) assert len(edges) == CORE_E assert connected(CORE_V, edges) assert biconnected(CORE_V, edges) return edges def build_instance(n, seed): if n < 99: raise ValueError("hard max-D instance needs n >= 99") # 用随机 3-正则双连通核心。 # 如果想要完全确定性的核心,可以改成 deterministic_core()。 core_edges = random_cubic_biconnected(seed) # core_edges = deterministic_core() extra = n - 99 # 把 extra 个细分点尽量均匀撒到核心边上。 sub = [0] * len(core_edges) order = list(range(len(core_edges))) random.shuffle(order) for i in range(extra): sub[order[i % len(order)]] += 1 cur_v = CORE_V out_edges = [] for idx, (u, v) in enumerate(core_edges): last = u for _ in range(sub[idx]): cur_v += 1 out_edges.append((last, cur_v - 1)) last = cur_v - 1 out_edges.append((last, v)) # 复杂块点数应该是 n - 33,边数应该是 n。 complex_v = cur_v assert complex_v == n - EXCESS assert len(out_edges) == n # 把点编号改成 1-based。 out_edges = [(u + 1, v + 1) for u, v in out_edges] # 剩下 33 个点是孤立点,编号为 complex_v+1 ... n。 # 它们不会出现在任何边中。 # 打乱边顺序和端点方向,避免算法吃到特殊顺序福利。 random.shuffle(out_edges) a = [] b = [] for u, v in out_edges: if random.randint(0, 1): u, v = v, u a.append(u) b.append(v) return a, b def main(): if len(sys.argv) < 2: print("usage: python3 gen_hard.py n [seed]", file=sys.stderr) sys.exit(1) n = int(sys.argv[1]) seed = int(sys.argv[2]) if len(sys.argv) >= 3 else 1 a, b = build_instance(n, seed) print(n) print(*a) print(*b) if __name__ == "__main__": main()
复杂核心满足:
所以:
再加上 33 个孤立点后:
如果把边细分到 ,每细分一次:
所以 不变,仍然是:
因此复杂块的关联二分图环秩始终是:
这是当前只算 方案下能遇到的最大复杂块。
先测纯递归压力:
bashpython3 gen_hard.py 99 1 > hard_99_1.in python3 gen_hard.py 99 2 > hard_99_2.in python3 gen_hard.py 99 3 > hard_99_3.in
再测大图压力:
bashpython3 gen_hard.py 200000 1 > hard_200000_1.in python3 gen_hard.py 200000 2 > hard_200000_2.in python3 gen_hard.py 200000 3 > hard_200000_3.in
如果你当前这份实现被 hard_200000 卡死,那基本可以确认瓶颈不是数学 DP,而是复杂块没有先压缩核心:递归中反复复制、Tarjan、扫整张细分后的大图。
下一步要优化的话,方向很明确:
先把复杂连通块压成 规模核心,把长链变成 transfer gadget。
对 ,核心规模只有常数级,递归才真正理论可过。
这个东西还是炸了,无论是 的极限数据还是 的随机数据都无法做到完全通过,请你重新思考该如何解决
对,前一个方案本质上还是不够稳。炸的原因不是代码细节,而是分支对象选错了:
之前是在“原始关联二分图”上递归,每个分支都反复 Tarjan、复制、扫大图;而且删一条关联边后产生的大量度 链没有被一次性吸收,导致随机极限核心也会炸。
需要把模型再换一次:
把复杂块转成带点权的 monomer-dimer 问题,然后每次递归前强制压到 2-core。
这样才能把复杂度真正压到由 excess 控制,而不是由 或随机核心的边数控制。
设原图 的连通块 excess 为:
令:
那么至少要删 条边才可能让所有正 excess 块变成 pseudoforest。
所以:
cppif (P > 33) ans = 0;
更重要的是:
cppif (P == 33) ans = 0;
原因是:此时只可能有 项。每个正 excess 块都必须做“最少删除”,保留下来的每个有效块至少含有一个基环树分量,权值至少含一个因子 。因此 必然是偶数,而
所以:
这会直接干掉你构造的 极限核心。
对每个原图连通块 ,仍然构造关联二分图 。
现在把它看成 monomer-dimer 模型。
每个点 有两个权值:
初始时:
左点,也就是原图边:
左点未匹配等价于删掉这条原图边。
右点,也就是原图值域点:
于是目标多项式就是:
这正好等价于:
这是前一个代码缺失的核心。
在当前递归图中,不允许直接分支。
必须先把所有度 、度 点剥掉,并把它们变成核心点的点权。
对于一棵被剥掉的树,设它向父亲传递两个多项式:
若 的剥离子节点为 ,则:
含义:
如果剥离树挂在核心点 上,那么它作为 的一个儿子被合并进 的点权。
设核心点 原本权值为 ,挂上的子树消息为 ,则更新后:
如果整个连通块被剥空了,则它就是树,直接返回根的 。
以你卡的随机 3-正则核心为例。
原始关联二分图中:
如果你删掉一条关联边,那个左点立刻变成度 。
旧做法会继续在大图上递归。
新做法会立刻把这个度 左点以及后续产生的整条链全部吸收成点权。
也就是说,分支后不会留下大量“半死不活”的路径结构。
这会把递归规模从“随机核心指数爆炸”压成真正按 excess 下降。
压完 2-core 后有几种情况。
返回剥离树的结果。
分别求解后卷积。
直接做 block-cut tree DP。
这里需要用带点权版本:
桥块、环块都可以线性 DP。
在这个块里选一条边 ,做 monomer-dimer 标准分支:
注意第二项要乘:
旧代码里用的是普通匹配模型,点被匹配的权值默认是 ,所以没体现这一层。
新模型必须带上这个因子。
cppsolve(G): reduce_to_2core(G) if G is empty: return accumulated_factor split connected components if multiple: return product solve(component) if G is cactus: return cactus_dp(G) choose edge (u, v) inside a non-cactus biconnected block A = solve(G - edge(u, v)) B = solve(G - vertices u, v) B *= block[u] * block[v] return A + B
所有多项式只保留到 次。
不要随便选边。
优先选:
也就是:
cppbest_edge = argmax(deg[u] + deg[v])
这样使用分支 时,能最大化一次性剥掉的结构。
最终项是:
所以 只需要模:
例如:
| 需要的位数 | ||
|---|---|---|
| 24 | 22 | 10 |
| 28 | 25 | 7 |
| 30 | 26 | 6 |
| 31 | 26 | 6 |
| 32 | 31 | 1 |
| 33 | 31 | 1 |
所以不要全程都用 uint32_t 算满 。
每个系数 可以按需要截断:
cppneed[m] = max(0, 32 - v2_fact[m]); mod[m] = 1u << need[m]; // need=32 时特殊处理
更简单的工程做法是:
uint32_t;mask[i] 截断;这对 的边界非常重要。
令:
我们已经有:
才需要真的算。
每个正 excess 块的关联二分图环秩为:
压 2-core 后,度 的核心点数量是:
长链、树枝全部通过剥离 DP 吸收到点权里,不再参与指数递归。
递归分支每次都会:
实际边界可以按:
估计。
但由于 已经直接为 ,真正需要处理的最大值是:
于是指数部分从原来的极限 下降到近似:
更重要的是,每个递归节点处理的是压缩后的核心,而不是 级别的大图。
不是小修,而是要重写 solve_bgraph()。
旧结构:
cppsolve_bgraph: simplify isolated vertices split components cactus_dp branch
新结构必须是:
cppsolve_bgraph: reduce_to_2core_with_weights split core components cactus_dp_with_weights branch_with_block_weight
核心差别是:
cppdegree-1 vertex must not branch recursively; degree-1 vertex must be absorbed into neighbor weights.
这是性能能不能过的分水岭。
先不要直接写全量优化版。建议分三步:
写 weighted monomer-dimer 基础框架
每个点维护 free[v] 和 block[v] 两个多项式。
实现 2-core 剥离
先只测树和基环树,确保结果和旧代码一致。
再接 cactus + 非 cactus 分支
分支后必须重新跑 2-core 剥离。
这样对拍会很清楚。
最关键结论:
上一个方案炸,是因为它没有把度 链作为 DP 消掉。
正确方向是:关联二分图 + 点权 monomer-dimer + 每轮递归前压 2-core。
另外 可以直接返回 ,这会干掉最极限的 卡法。