[AHOI2022] 山河重整

之前打过一场 HA 的 ICPC 萌新赛,了解了这个问题的一个结论:若对于 \(\forall k\in [1,n]\)\(k\) 都能被表示出,那么满足 \(\sum\limits_{j\in S,j\le k}j\ge k\)。这个条件是充要的。并且能被表示出来的数一定会形成一个形如 \([1,x]\) 的区间。

考虑 DP:设 \(f(i,j)\) 表示前 \(i\) 个数选若干个,能表示出 \([1,j]\) 的方案数。\(\mathcal O(n^2),\tt 60pts\)。感谢良心 EI 送这么多分。

然后发现 能表示出 \([1,j]\) 这个条件有点烦,这种 DP 的上限就在 \(\mathcal O(n^2)\) 了,很难优化。

转换视角,既然限制很强,考虑正难则反,容斥一下,算无法表示出 \([1,n]\)\(S\) 个数。

这样有一个相当优秀的性质:对于第一个不能被 \(S\) 表示出的 \(k\),必然有 \(\sum\limits_{j\in S,j\le k}j=k-1\),并且 \(k\) 没有选入 \(S\)

如果我们可以计算出 \(i\) 是第一个不能被表示出的数的方案数,就能快速解决这个问题。根据推导,这种方式成功的可能性很大。

把限制写出来\(\sum\limits_{j\in S,j\le i}j=i\),且 \(i+1\)\(S\) 中第一个满足这个条件的数(此处改为 \(i+1\) 是为了方便)。

拆分限制,第二个限制可以用比 \(i\) 小的数容斥去重,具体方法后面再说,我们先考虑第一个限制。

定义 \(f(i)\) 表示 \(1\sim i\) 中选若干个数表示出 \(i\) 的方案数。

\(j\le i\) 的限制是假的,没什么用,这个东西本质就是 \(i\) 的自然数拆分方案,并且不能重复,那么显然至多拆出来 \(\sqrt i\) 个数。可以类比普通的自然数拆分问题,做到 \(\mathcal O(n\sqrt n)\) 计算。具体的计算方法在 这篇题解 里面写的相当清楚,我感觉我也讲不好,就不说了 QAQ。

然后我们考虑容斥掉第二个限制,当我们将 \(j\lt i\)\(f\) 值已经不再有重复,先用 \(f_j\) 进行暴力容斥找找思路:\(f_i\gets f_i-f_j\times A(j,i)\),其中 \(A(j,i)\) 表示容斥系数。

具体地,\(A(j,i)\) 表示 \([j+2,i]\) 中的数凑出 \(i-j\) 的方案数。因为 \(j+2\sim i\) 这部分的空缺需要补上,否则 \(i\) 不满足第一条限制。

考虑计算 \(A(j,i)\),假设选了 \(k(k\ge 1)\) 个数,将所有选中的数减去 \(j+2\),总和减去 \(i\times (j+2)\),又成了原来的问题。

然后会发现,\(j\ge \frac{i}{2}\) 的时候不合法,那么将 \(1\sim i\) 分割为 \(1\sim \frac{i}{2},\frac{i}{2}+1\sim i\),递归求解左半段,计算左半段对右半段的贡献即可。因为右半段自身内部不会产生影响。

类半在线卷积,分治计算即可,过程中用一个辅助数组 \(g\) 处理容斥系数,因为容斥系数也是一个拆分数的形式,所以计算方式和 \(f\) 大同小异。

因为分治只需递归一半,所以复杂度仍为 \(\mathcal O(n\sqrt n)\),使用神奇多项式科技好像可以做到 \(\mathcal O(n^{4/3}\log^{2/3}n)\)。EI 好强。

#include <bits/stdc++.h>

const int maxn = 5e5 + 5;

int n,p,pw[maxn],f[maxn],g[maxn];

void add(int& x,int y) {
	x += y;
	if(x >= p)
		x -= p;
	return ;
}

void sub(int& x,int y) {
	x -= y;
	if(x < 0)
		x += p;
	return ;
}

void solve(int n) {
	if(n <= 1)
		return ;
	solve(n >> 1);
	int s = std::sqrt(n * 2);
	for(int i = 0;i <= n;++ i)
		g[i] = 0;
	for(int i = s;i;-- i) {
		for(int j = n;j >= i;-- j)
			g[j] = g[j - i];
		for(int j = 0;j + (j + 2) * i <= n;++ j)
			add(g[j + (j + 2) * i] , f[j]);
		for(int j = i;j <= n;++ j)
			add(g[j] , g[j - i]);
	}
	for(int i = (n >> 1) + 1;i <= n;++ i)
		sub(f[i] , g[i]);
	for(int i = 0;i <= n;++ i)
		g[i] = 0;
	return ;
}

int main() {
	scanf("%d %d",&n,&p);
	pw[0] = 1;
	for(int i = 1;i <= n;++ i)
		pw[i] = pw[i - 1] * 2 % p;
	int s = std::sqrt(2 * n);
	for(int i = s;i;-- i) {
		for(int j = n;j >= i;-- j)
			f[j] = f[j - i];
		add(f[i] , 1);
		for(int j = i;j <= n;++ j)
			add(f[j] , f[j - i]);
	}
	f[0] = 1;
	solve(n);
	int ans = 0;
	for(int i = 0;i < n;++ i)
		add(ans , 1ll * f[i] * pw[n - i - 1] % p);
	sub(pw[n] , ans);
	printf("%d\n",pw[n]);
	return 0;
}
posted @ 2023-03-08 16:14  ImALAS  阅读(50)  评论(0编辑  收藏  举报