agc022F - Checkers 题解

每次做这种 atc 风格 DP 都是生不如死……


先扯一些关系不大的东西。

我们知道 n 个元素合并 n1 次可以用一棵 2n1 个点的二叉树表示。每次合并都新建节点,并连向两个集合的树的根,得到一棵新树。这样可以描述每次合并的两个集合的无序对的集合,但并不能知道合并的顺序(也不会有题目需要知道顺序),但起码需要符合这棵树的拓扑序。如果 A 合并到 B、B 合并到 A 不同(可以理解为 PK 的胜负,负者淘汰),那么此时这种方法并不能记录每次的输赢,但只需要把胜者放在左儿子、负者放在右儿子即可。

但还有另一种我以前不知道的描述合并关系的方法。这种方法重在描述每次合并的胜负,每次合并设 A 胜 B 负,则令 B 目前的胜者直接指向 A 目前的胜者。这样也可以得到一棵(内向)树,只不过恰有 n 个节点,且是多叉树(相当于把二叉树的左儿子缩上去)。但普通情况并不能描述每次合并的两个集合的无序对的集合,然而依然可以稍作改进做到。对于某个节点 x,记其儿子序列为 A1m,则必然恰好存在一个排列 p,使得 Api 参与的合并是与 {x}j=1i1Apj 合并的,那就将儿子序列按照这个排列排序即可。这种方法依然不能知道合并顺序,但可以肯定合并是按照树的拓扑序进行的。所以这和二叉树方法是等价的。

再讲一个大冤种:如果每次是「将 x,y 所在集合合并」,那么用无向边连接 x,y,会形成一棵无根树。每种断边 / 加边顺序就对应一种合并过程(当然不是双射)。


开始讲这个题。可以认为一开始是 n 个向量 aai,使得 ai,j=[i=j]。每次 uu,vv PK 是合并为 2uuvv2vvuu。注意到,这里涉及胜负,而多叉树描述法擅长描述这个。我之前只知道二叉树描述法,推了半天毫无头绪/yun

考虑其多叉树。每次胜者的集合里所有 i 对最终向量的第 i 分量贡献了 1 的系数,负者贡献 2。设第 i 分量为 (1)ci2di,尝试将 ci,di 在树上表示出来。di 显然就是 i 的深度。而 ci 比较复杂,好在我们只关心其奇偶性。

i 往上爬的过程中,每爬一次看它在当前节点的儿子序列中排第 π 个,则说明所在集合胜了 π1 次(这里排列 p 与之前的描述是相反的/cy),对 ciπ1 的贡献。而它胜儿子们还有额外 si 的贡献,其中 sii 的儿子树。这样的表达太难受了,考虑写一个用父亲的 c 值表达的递推式。容易写出 ci=cfaxsfax+(πf1)+sx,其中 πf 往上爬一次的 π。如果再将 p 的含义 reverse 一次,表达式则简单很多,为 ci=cfax+πf+sx(当然是模 2 意义下的)。

又注意到:设最终能得到某一个向量 vv,其元素的可重集为 S,则任意可重集为 S 的向量都能达到(只要置换一下元素的地位即可)。这意味着我们可以从可重集的角度出发去计数,但要乘上 n!cntx!,这要求我们的 DP 是以一种一种的 (cmod2,d) 为阶段的。

下面正式开始 DP。设 dpi,d 为有 i 个节点、至多 d 层的所有多叉树的所有可能的可重集的可重集排列数之和。由于以层为阶段,每层的 d 相等,只有两种可能的 cmod2,极易做到转移时立刻决定 1cntx! 并以其为系数。再仔细想想,其实根本不需要记录 d,直接当作在考虑目前这棵树的最后一层即可,毕竟各层无本质区别(第一层除外,必须恰有一个点,所以要特判 i=1)。

考虑枚举最后一层的 c=0 的个数 x,以及 c=1 的个数 y。考察递推式 ci=cfax+πf+sx,最后一层 s 值为 0,所以仅考虑 cfaxπf。那么对于倒数第二层的某个节点 u,如果 su 是偶数,则它的儿子们的 cfax 固定,πf 却有一半是 0 一半是 1,所以儿子们的 c 值一定是一半 0 一半 1。一直这样下去的话必有 x=y,需要 su 为奇数来调节。类似推理,可以知道这样 u 的儿子们中 c=¬cfax 的会比 c=cfax 的多一个。

于是一种方案就是倒数第二层有 |xy|su 为奇数的 u,且这些 uc 值都要等于 [x>y],偶数点无所谓,但要保证奇偶总共至少有一个节点。只要是存在对应多叉树满足这个条件的向量元素可重集,便可将 x0y1 接在下面,于是直接转移到对应状态即可。但是有那么一种可能,就是倒数第二层 su 为奇数的 u 的个数还可以是大于 |xy| 且与之奇偶性相同的数 m,并且 c=[x>y] 的比 c=¬[x>y] 的多 |xy| 个。把这些情况的 DP 值都加在一起吗?对不起,可能有重复。一种理想情况是,存在某一种状态的所有可能可重集完全包含其它状态,那就可以只算这个状态。幸运的是,可以证明 |xy| 个奇数的状态就是完全包含其它的。因为若超过 |xy|,则必存在两个奇数点的 c 值不同,则将其中一个的儿子给另一个,该层 c 值可重集不变,且奇数点减少了两个。

好的,现在将 DP 定义扩展至 dpi,j,k=0/1 表示 i 个数,最后一层 [2su]=j,且这 j 个的 c 值必须都等于 k。重新考虑转移,可以发现相比于之前多了 su 的影响,之前都是 0 的。那么设 cu=cusu,即可跟之前差不多。枚举 x,y 分别表示 su 为偶数的点中 c=0/1 的数量,则根据 j,k 容易算出 x,y 分别表示所有点中 c=0/1 的数量,以及 x,y 分别表示所有点中 c=0/1 的数量。那么贡献为 1x!y!dpijxy,|xy|,[x>y]

这样时间复杂度是 O(n4),可以过 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');
}

考虑优化到 O(n3)

观察代码,枚举 i,j,k,x,y,考虑分成四元组 (i,j,k,x)y 来查看。

总贡献为 x1(x+(1k)j)!y1(y+kj)!f(i,j,k,x,y)(这里 i,j,k 虽然不在求和枚举里,但也是被枚举的变量)。如果能 O(1) 知道后面这个 的值,便达到了三方。注意到该 仅与 j || xijxx + (k ? j : -j)kj 有关,其中 j || x 是 bool 值其复杂度可以忽略。这样还是涉及三个 O(n) 的量,有 O(n3) 的值(flag),计算每种的话加上枚举 y 还是四方的复杂度。

但是注意到最后一项 kj,如果 k=0 就不需要记录了,那么就只有 O(n2) 种,对每种暴力计算复杂度就是三方了。那 k=1 呢?很简单,交换 x,y 地位即可。

略微卡常,用了 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');
}
posted @   ycx060617  阅读(151)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
历史上的今天:
2020-04-05 CodeForces 1080E - Sonya and Matrix Beauty
2020-04-05 Manacher算法
点击右上角即可分享
微信分享提示