agc022F - Checkers 题解
每次做这种 atc 风格 DP 都是生不如死……
先扯一些关系不大的东西。
我们知道 个元素合并 次可以用一棵 个点的二叉树表示。每次合并都新建节点,并连向两个集合的树的根,得到一棵新树。这样可以描述每次合并的两个集合的无序对的集合,但并不能知道合并的顺序(也不会有题目需要知道顺序),但起码需要符合这棵树的拓扑序。如果 A 合并到 B、B 合并到 A 不同(可以理解为 PK 的胜负,负者淘汰),那么此时这种方法并不能记录每次的输赢,但只需要把胜者放在左儿子、负者放在右儿子即可。
但还有另一种我以前不知道的描述合并关系的方法。这种方法重在描述每次合并的胜负,每次合并设 A 胜 B 负,则令 B 目前的胜者直接指向 A 目前的胜者。这样也可以得到一棵(内向)树,只不过恰有 个节点,且是多叉树(相当于把二叉树的左儿子缩上去)。但普通情况并不能描述每次合并的两个集合的无序对的集合,然而依然可以稍作改进做到。对于某个节点 ,记其儿子序列为 ,则必然恰好存在一个排列 ,使得 参与的合并是与 合并的,那就将儿子序列按照这个排列排序即可。这种方法依然不能知道合并顺序,但可以肯定合并是按照树的拓扑序进行的。所以这和二叉树方法是等价的。
再讲一个大冤种:如果每次是「将 所在集合合并」,那么用无向边连接 ,会形成一棵无根树。每种断边 / 加边顺序就对应一种合并过程(当然不是双射)。
开始讲这个题。可以认为一开始是 个向量 ,使得 。每次 PK 是合并为 或 。注意到,这里涉及胜负,而多叉树描述法擅长描述这个。我之前只知道二叉树描述法,推了半天毫无头绪/yun
考虑其多叉树。每次胜者的集合里所有 对最终向量的第 分量贡献了 的系数,负者贡献 。设第 分量为 ,尝试将 在树上表示出来。 显然就是 的深度。而 比较复杂,好在我们只关心其奇偶性。
在 往上爬的过程中,每爬一次看它在当前节点的儿子序列中排第 个,则说明所在集合胜了 次(这里排列 与之前的描述是相反的/cy),对 有 的贡献。而它胜儿子们还有额外 的贡献,其中 是 的儿子树。这样的表达太难受了,考虑写一个用父亲的 值表达的递推式。容易写出 ,其中 往上爬一次的 。如果再将 的含义 reverse 一次,表达式则简单很多,为 (当然是模 意义下的)。
又注意到:设最终能得到某一个向量 ,其元素的可重集为 ,则任意可重集为 的向量都能达到(只要置换一下元素的地位即可)。这意味着我们可以从可重集的角度出发去计数,但要乘上 ,这要求我们的 DP 是以一种一种的 为阶段的。
下面正式开始 DP。设 为有 个节点、至多 层的所有多叉树的所有可能的可重集的可重集排列数之和。由于以层为阶段,每层的 相等,只有两种可能的 ,极易做到转移时立刻决定 并以其为系数。再仔细想想,其实根本不需要记录 ,直接当作在考虑目前这棵树的最后一层即可,毕竟各层无本质区别(第一层除外,必须恰有一个点,所以要特判 )。
考虑枚举最后一层的 的个数 ,以及 的个数 。考察递推式 ,最后一层 值为 ,所以仅考虑 和 。那么对于倒数第二层的某个节点 ,如果 是偶数,则它的儿子们的 固定, 却有一半是 一半是 ,所以儿子们的 值一定是一半 一半 。一直这样下去的话必有 ,需要 为奇数来调节。类似推理,可以知道这样 的儿子们中 的会比 的多一个。
于是一种方案就是倒数第二层有 个 为奇数的 ,且这些 的 值都要等于 ,偶数点无所谓,但要保证奇偶总共至少有一个节点。只要是存在对应多叉树满足这个条件的向量元素可重集,便可将 个 、 个 接在下面,于是直接转移到对应状态即可。但是有那么一种可能,就是倒数第二层 为奇数的 的个数还可以是大于 且与之奇偶性相同的数 ,并且 的比 的多 个。把这些情况的 DP 值都加在一起吗?对不起,可能有重复。一种理想情况是,存在某一种状态的所有可能可重集完全包含其它状态,那就可以只算这个状态。幸运的是,可以证明 个奇数的状态就是完全包含其它的。因为若超过 ,则必存在两个奇数点的 值不同,则将其中一个的儿子给另一个,该层 值可重集不变,且奇数点减少了两个。
好的,现在将 DP 定义扩展至 表示 个数,最后一层 ,且这 个的 值必须都等于 。重新考虑转移,可以发现相比于之前多了 的影响,之前都是 的。那么设 ,即可跟之前差不多。枚举 分别表示 为偶数的点中 的数量,则根据 容易算出 分别表示所有点中 的数量,以及 分别表示所有点中 的数量。那么贡献为 。
这样时间复杂度是 ,可以过 agc 原题,但过不了模拟赛(魔鬼笑)。
先放个四方代码
constexpr int N = 510;
int n;
int iv[N], fc[N], ifc[N];
int dp[N][N][2];
void mian() {
n = read(), P = read();
iv[1] = 1; REP(i, 2, n) iv[i] = (ll)iv[P % i] * (P - P / i) % P;
fc[0] = ifc[0] = 1; REP(i, 1, n) fc[i] = (ll)fc[i - 1] * i % P, ifc[i] = (ll)ifc[i - 1] * iv[i] % P;
dp[1][0][0] = dp[1][0][1] = dp[1][1][1] = 1;
REP(i, 2, n) REP(j, 0, i - 1) REP(k, 0, 1) {
REP(x, 0, i - 1 - j) REP(y, 0, i - 1 - j - x) if(j || x || y) {
int x0 = x + (k == 1) * j, y0 = y + (k == 0) * j;
int x00 = x + (k == 0) * j, y00 = y + (k == 1) * j;
int ad = x0 < y0 ? dp[i - j - x - y][y0 - x0][0] : dp[i - j - x - y][x0 - y0][1];
addto(dp[i][j][k], (ll)ifc[x00] * ifc[y00] % P * ad % P);
}
}
int ans = dp[n][0][0];
ans = (ll)fc[n] * ans % P;
prt(ans), pc('\n');
}
考虑优化到 。
观察代码,枚举 ,考虑分成四元组 和 来查看。
总贡献为 (这里 虽然不在求和枚举里,但也是被枚举的变量)。如果能 知道后面这个 的值,便达到了三方。注意到该 仅与 j || x
、、x + (k ? j : -j)
、 有关,其中 j || x
是 bool 值其复杂度可以忽略。这样还是涉及三个 的量,有 种 的值(flag),计算每种的话加上枚举 还是四方的复杂度。
但是注意到最后一项 ,如果 就不需要记录了,那么就只有 种,对每种暴力计算复杂度就是三方了。那 呢?很简单,交换 地位即可。
略微卡常,用了 18 次乘法取一次模的优化才过/qd
code
constexpr int N = 510;
int n;
int iv[N], fc[N], ifc[N];
int dp[N][N][2];
ull f[N][3 * N][2][2];
int vis[N][3 * N][2];
void mian() {
n = read();
iv[1] = 1; REP(i, 2, n) iv[i] = (ll)iv[P % i] * (P - P / i) % P;
fc[0] = ifc[0] = 1; REP(i, 1, n) fc[i] = (ll)fc[i - 1] * i % P, ifc[i] = (ll)ifc[i - 1] * iv[i] % P;
dp[1][0][0] = dp[1][0][1] = dp[1][1][1] = 1;
memset(f, -1, sizeof(f));
REP(i, 2, n) REP(j, 0, i - 1) REP(k, 0, 1) { // 这优化,多是一件美逝啊……
if(k == 0) {
REP(x, 0, i - 1 - j) {
ull &F = f[i - j - x][x + (k ? j : -j) + n][k][j || x];
if(!~F) {
F = 0; int c = 0;
REP(y, 0, i - 1 - j - x) if(j || x || y) {
int x0 = x + (k == 1) * j, y0 = y + (k == 0) * j;
int y00 = y + (k == 1) * j;
int ad = x0 < y0 ? dp[i - j - x - y][y0 - x0][0] : dp[i - j - x - y][x0 - y0][1];
F += (ll)ifc[y00] * ad;
if(++c == 18) F %= P, c = 0;
} F %= P;
}
int x00 = x + (k == 0) * j;
addto(dp[i][j][k], ifc[x00] * F % P);
}
} else {
REP(y, 0, i - 1 - j) {
ull &F = f[i - j - y][y + (!k ? j : -j) + n][k][j || y];
if(!~F) {
F = 0; int c = 0;
REP(x, 0, i - 1 - j - y) if(j || x || y) {
int x0 = x + (k == 1) * j, y0 = y + (k == 0) * j;
int x00 = x + (k == 0) * j;
int ad = x0 < y0 ? dp[i - j - x - y][y0 - x0][0] : dp[i - j - x - y][x0 - y0][1];
F += (ll)ifc[x00] * ad;
if(++c == 18) F %= P, c = 0;
} F %= P;
}
int y00 = y + (k == 1) * j;
addto(dp[i][j][k], ifc[y00] * F % P);
}
}
}
int ans = dp[n][0][0];
ans = (ll)fc[n] * ans % P;
prt(ans), pc('\n');
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
2020-04-05 CodeForces 1080E - Sonya and Matrix Beauty
2020-04-05 Manacher算法