计数(2):题目

一些碎碎念

计数本身的做题流程应当是:

  • 首先,找一个东西刻画结构。

    • 如图,树等,在结构上进行计数。

    • 特别的刻画方法有笛卡尔树,直方图等。

    • 当然有的时候在结构刻画上没有什么可以发掘的地方,没必要“这个东西结构刻画不够简洁所以必须要找到一个好的结构”

  • 其次,刻画合法条件。

    • 结构是为了辅助这一条的。

    • 特别的,如果合法条件难以满足,我们可以考虑钦定部分满足/满足部分/部分不满足/不满足部分,其它任意,然后任意。

  • 最后计数。

接下来是一些关于入手点的一些问题。

  • 直接做很难?正难则反 CF1556F

  • 好难刻画?多规定点东西!多规定点,直到可以容易的刻画,然后再一点一点脱掉 P10004

  • 好难刻画?少规定点东西!别考虑太细致的过程,从宏观处着手,比如从起点和终点。P7736

  • 根据加法原理,转化统计标准。

  • 贡献均摊。

  • 判定序列合法,变为构造生成合法序列。(建立双射)

  • 利用组合意义,如 \(a^2\) 可以是 \(a\) 里面允许重复选两个的方案数。

  • 考虑极端的情况,比如分析条件上最难以删除的元素被删除的情况等,AGC043D

计数技巧有很多,从计数的原理上:

  • 加法,乘法原理,非常基础。
  • 容斥原理,博大精深。本质是子集反演来描述的事情。
    • 容斥中,从钦定的角度来说,可以是钦定一个“至少范围”,也可以是钦定出一个“至多范围”。
    • 从容斥对象来说,可以是容斥所要求的对象,也可以是对对象的约束计数。
  • 概率期望的角度出发

更加和题目模型相结合的技巧上:

  • 对于直方图可以刻画的,将 \(\le x\)\(>x\) 分别看作 \(0, 1\) 再计数。也可以看作一种差分 agc049_e P12251

  • 将邻接矩阵看作图

  • 对于中位数,令小于 \(x\)\(-1\),大于等于 \(x\) 的为 \(1\),那么找到与 \(0\) 的后继就是找到了中位数。P2839

  • 将序列看作一棵树,利用一点点减边容斥的想法,\(点数-边数=联通块数\)。P5825

  • 时刻不忘 \(点数-边数=联通块数\) BZOJ 5473

  • 树的直径中点重合,所以对于树直径计数可以从中点所在的联通块考虑。 XYD19495.热异常/牛客 Haitang and diameter

  • LGV 引理多考虑起点和终点的关系

  • 增加维度。比如把时间看作一个维度。

  • 用折线来刻画前缀和,极差等问题。AT_agc013_d zr3366 CF1924D

  • 对于冒泡排序的技巧有:

    • 一轮单项冒泡排序可以直接刻画:扫到前 \([1, i]\) 个中,最后一个一定是前缀最大值。
    • 转成 \(n\) 个 01 序列。zr3382
    • \(k\) 轮冒泡排序后,\([1, i]\) 里面的值为原序列 \([1, i + k]\) 中最小的 \(i\) 个值。 P12865
  • 对于 \(x+y \ge W\) 类问题,将所有数分成 \(x\le \frac{W}{2}\)\(x > \frac{W}{2}\) 两类。

    • 小于等于 \(\frac{W}{2}\) 称作白点,大于 \(\frac{W}{2}\) 称作黑点。
    • 那么如果按照 \(d, 0, d-1, 1, d-2, 2, \dots\) 排列,那么无论黑点白点,都只能向它之前的黑点连边。这是一个相当有趣的构造。构造的思路也很自然。
  • 对于排列 dp,可以考虑往两端插入的情况,进一步考虑连续段 dp。

一个极为另类的技巧是:

  • 转化成期望问题。利用期望工具解决。

虽然写了这么多,但是计数仍然不是简单的知道如何刻画就可以做出来的。因为一个偏难的计数可能战线会拉的偏长(从刻画再到运用计数原理技巧),所以需要保持对做法的自信与专注。同时大胆运用技巧,不能拘泥于现有的,看上去“可能是对的”的做法。


P6189

tag:构造双射,根号分治。

直接构造其实很困难,于是考虑构造双射。考虑如何生成一个递增序列,要么然复制最后一个数一次,要么然最后一个数 \(+1\),那么设 \(f[i, j]\) 为最后一个数为 \(j\),直接转移就是:\(f[i, j] = f[i - 1, j - 1] + f[i - j, j]\)

这个是 \(O(n^2)\) 的,考虑优化。看到这种求和,很容易想到根号分治。如果 \(j \le \sqrt{n}\) 那么可以直接求,如果 \(j > \sqrt{n}\) 那么有结论是最多不会超过 \(\sqrt{n}\) 个数,我们也可以依据这个来构造双射:保证数字数量的双射手法是,要么然增加一个 \(\sqrt{n}\) 在最后,要么然整体 \(+1\)。这样设 \(g[i, j]\) 那么有 \(g[i, j] = g[i - \sqrt{n}, j - 1] + g[i - j, j]\)

最后合并两个部分卷起来就好了。


P14368

tag:二项式反演
经典的欧拉数问题,询问有多少排列满足 \(\sum\limits_{i = 1}^{n - 1}[P_i > P_{i+1}] = k\)(这里的 \(k\) 是题目中给的 \(k' - 1\))。首先二项式反演,恰好变为钦定有 \(x\) 个位置满足 \([P_i > P_{i+1}]\),相当于给这些连边,于是将 \(n\) 个点划分为 \((n - x)\) 个联通块,每个联通块都是一个下降序列。这个问题等价于 \(n\) 个元素划分为 \((n - x)\) 个非空集合的方案数,非空集合这个“盒子”互不相同,这一点与第二类斯特林数不同。容斥计算,钦定哪些“盒子”不放元素,于是就有 \(\sum\limits_{t = 0}^{n - x}(-1)^t\binom{n - x}{t}(n - x - t)^n\)

最终结果为:

\[\sum\limits_{x = k}^{n - 1}(-1)^{x - k}\binom{x}{k}\sum\limits_{t = 0}^{n - x}(-1)^t\binom{n - x}{t}(n - x - t)^n \]

但是这个需要做两次卷积,为了转化为一次,首先要把 \((-1)\) 的系数进行合并,设 \(i = n - x - t\),合并掉 \((-1)^x\) 这一项。然后把那一车看上去就很范德蒙德卷积状物给丢到后面去,利用二项式恒等式话剪掉后面的一部分。

\[\sum\limits_{i = 0}^{n - k}\sum\limits_{x = k}^{n}(-1)^{x-k}\binom{x}{k}(-1)^{n -i - x}\binom{n - x}{i}i^n\\\implies \sum\limits_{i = 0}^{n - k}\sum\limits_{x = k}^n i^n(-1)^{n - k + i}\binom{x}{k}\binom{n - x}{i}\\ \implies\sum\limits_{i = 0}^{n - k}(-1)^{n - k + i}i^n \sum\limits_{x = k}^n \binom{x}{k}\binom{n - x}{i}\\ \implies \sum\limits_{i = 0}^{n - k}(-1)^{n - k + i}i^n \binom{n+1}{i + k + 1} \]

一些历史遗留问题

  • 这个 \((-1)\) 消去的动机?
  • 二项式系数恒等式。

AGC030D

tag:计数转期望

无比巧妙的一个题目。考虑两个位置变化后提供贡献的概率,设 \(f(i, j)\)\(i\) 位置大于 \(j\) 的概率。那么对于 \(x, y\),可能选择交换也可能选择不交换,于是有:

\[f'(i, x) = f'(i, y) = \dfrac{f(i, x)+ f(i, y)}{2}\\ f'(x, i) = f'(y, i) = \dfrac{f(x, i) + f(y, i)}{2}\\ f'(x, y) = \dfrac{f(x, y) + f(y, x)}{2} \]


upd on 20251120

其实并不一定必须要计数转期望。直接设 \(f(x, y)\) 为方案数行不行?可以的。只不过对于不涉及到的 \((x, y)\) 每一轮要全局乘 \(2\)。于是我们考虑所有方案数除以 \(2^m\)。不难发现等价。


AGC043D

如何分析充要条件?

最大值入手:对于 \(3n\),显然是除了它以外的所有全部消去,然后再消,这时候最多剩下 \(3\) 个元素。然后考虑剩下来的最大值,以此类推。从找最大值这种极限情况入手,于是可以得到:对于前缀 \(\max\),形成的段落长度不超过 \(3\),这是一个必要条件,但并不充分。段落之间还需要进行匹配,使得每个段落长度均为 \(3\)。我们发现这样构造出来的一个初始序列就是合法的。不难得到结论:

  • 划分前缀 \(\max\) 后得到的段落,每个段落长度不超过 \(3\),并且长度为 \(2\) 的段落数量不能超过长度为 \(1\) 的段落数量,这样才能合并。

接下来是对这个东西统计,我是直接组合数计算的,因为想不到 dp 方法。现在介绍 dp 方法。因为总是和“前缀最大值”相关,所以设 \(f[i, j]\) 为插入了 \([1, i]\) 里面的数,其中长度为 \(1\) 的段落减去长度为 \(2\) 得段落有 \(j\) 个得方案数,则:

  • 若拓展 \(1\) 个,显然只有 \(f[i - 1, j - 1] \gets f[i, j]\)
  • 拓展两个,则第一个选择为 \(i\),第二个可以在 \((i - 1)\) 个中任选一个,\(f[i - 2, j + 1](i - 1)\gets f[i, j]\)
  • 拓展三个同理,\((i - 1)(i - 2)f[i - 3, j]\)

总结:

  • 最大难点在于找到充要条件,剩下很简单。
  • 这一步可以通过考虑极端情况出发,最难以删除得位置什么时候删除?
  • 幽默的事情:我自己做的时候,对着 01 序列想了一万年,恰好就忽略了“最大值”这个问题。

CF2146F

tag:冒泡排序,基础组合数
被自己孱弱的基本功创飞了,做了一万年,但是还算是勉强独立战胜了,还是太菜了啊。

对于冒泡排序转成 01 序列刻画,\(\le x\)\(0\)\(>x\)\(1\),排序这个 01 序列需要除了从 \(n\) 开始的 \(1\) 最长后缀外的所有后缀。对于全部情况的 \(x\) 取最大值。不妨直接枚举最后一个 \(0\) 的位置统计前面最多可能会有多少个 \(1\),于是得到 \(b = \max\limits_{i = 1}^n\{\sum\limits_{j = 1}^{i - 1}[P_j > P_i]\}\)

回到题目。很明显,\(k, l, r\) 的约束等价于给每个位置 \(i\) 限定一个 \(b_i\) 的范围 \([l_i, r_i]\)。很容易想到 \(O(n^2)\) 做法,设 \(f[i, j]\) 为长度为 \(i\)排列\(b = j\) 的答案。注意到这里我们记录的是 \(1\sim i\) 的排列而不是从整体 \(1\sim n\) 的排列的前缀出发。每次考虑给最后一位分配什么就非常自由,前面保持相对大小即可。若最后一位分配为 \(x\),那么前面一定会产生 \((i + 1 - x)\)\(1\),则 \(f[i, j]\) 可以转移到 \(f[i + 1, \max(j, i + 1 - x)]\) 处,前缀和优化一下做到 \(O(n^2)\)

当然这并不够,考虑优化。注意到本质不同的区间 \([l_i, r_i]\) 只有 \(O(m)\) 种,那么 \(j\) 可以合并成 \(O(m)\) 个区间段里面的东西。设 \(f[i, j]\) 为前 \(i\) 个区间段,最终的 \(b\)\([u_j, u_{j+1})\) 内的方案数。

令第 \(i\) 个区间范围为 \([l_i, r_i]\)。拆成 \([u_j, u_{j+1})\) 的区间后为 \(l'_i, r'_i\)。对于 \(f[i, j]\) 考虑 \(f[i - 1, k]\) 转移的系数。

  • \(k < l'_i\),那么此时要求接下来第一个位置的值在 \([l_i, u_{j+1})\) 内,接下来要求每一个位置的值都小于 \(u_{j+1}\),同时至少存在一个位置的值大于等于 \(u_j\)。第一个位置单独计算方案数,剩下的问题就是:剩余 \(y\) 个数,要新加 \(x\) 个,每一个位置要求值都不能超过 \(t\),设为 \(G(y, x, t)\),显然 \(G(y, x, t) = \prod\limits_{j= y + 1}^{x + y}\min(j, t + 1)\)。如果要求是这一段中“每个数都要小于等于 \(r\),且至少有一个数大于 \(l\)”,那么方案数就是 \(G(y, x, r) - G(y, x, l)\)。于是不难写出这个情况的系数。
  • \(l'_i \le k < j\),此时不用考虑 \(l_i\) 的约束,但是有 \([u_{j}, u_{j+1})\) 的,直接用 \(G\) 来算。
  • \(k = j\)。只有小于 \(u_{j+1}\) 的约束,直接用 \(G\) 算。

于是时间复杂度 \(O(m^2\log n)\),具体细节见代码。

#include <bits/stdc++.h>
using namespace std;
const int M = 1e6, N = 2000 + 100, Mod = 998244353;
int qpow(int n, int m) {
	int res = 1;
	while(m) {
		if(m & 1) res = 1ll * res * n % Mod;
		n = 1ll * n * n % Mod;
		m >>= 1;
	}
	return res;
}
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int f[N + 3][2 * N + 3], g[N + 3][2 * N + 3], fac[M + 10], invf[M + 10];
int C(int n, int m) {
	if(m > n) return 0;
	return 1ll * fac[n] * invf[n - m] % Mod * invf[m] % Mod;
}
int G(int i, int x, int t) {
	if(!x) return 1;
	if(t < 0) return 0;
	int res = 1;
	if(i + 1 <= min(t + 1, i + x))
		res = 1ll * fac[min(t + 1, i + x)] % Mod * invf[i] % Mod;
	res = 1ll * res * qpow(t + 1, max(0, i + x - max(i + 1, (t + 2)) + 1)) % Mod;
	return res;
}
int GG(int i, int x, int t1, int t2) {//GG 代表之前有 i 个,加入 x 个,第一个在 [t1, t2] 内的方案数,是第一个情况的系数要用到的
	t1 = max(t1, 0);
	int res = max(0, max(0, i + 1 - t1) - max(1, i + 1 - t2) + 1);
	if(x > 1)
		res = 1ll * res * G(i + 1, x - 1, t2) % Mod;
	return res;
}

int cp[N * 4 + 10], n, m, len = 0;
struct node {
	int pos, val, type;
	bool operator < (const node &other) const {
		if(pos != other.pos) return pos < other.pos;
		else {
			return val > other.val;
		}
	}
};
int ql[N + 10], qr[N + 10], ind[N + 10];
void init() {
	cin >> n >> m;
	vector <node> T1, T2;
	T1.push_back((node){1, 0, 0});
	for(int i = 1, k, l, r; i <= m; i++) {
		cin >> k >> l >> r;
		T2.push_back((node){l, k, 1});
		T1.push_back((node){r + 1, k + 1, 0});
	}
	T2.push_back((node){n, n, 1});
	sort(T1.begin(), T1.end());
	sort(T2.begin(), T2.end());
	for(int i = (int)T2.size() - 2; i >= 0; i--)
		T2[i].val = min(T2[i].val, T2[i + 1].val);
	for(int i = 1; i < T1.size(); i++)
		T1[i].val = max(T1[i].val, T1[i - 1].val);
	if(T2[0].pos >= 1) T1.push_back((node){1, T2[0].val, 1});
	for(int i = 1; i < T2.size(); i++) 
		if(T2[i - 1].pos + 1 <= T2[i].pos)
			T1.push_back((node){T2[i - 1].pos + 1, T2[i].val, 1});
	sort(T1.begin(), T1.end());

	int lll = 0, rrr = n, tot = 0;
	for(int i = 0; i < T1.size(); i++) {
		if(!T1[i].type) lll = T1[i].val;
		else rrr = T1[i].val;

		if(T1[i].pos <= n &&
			((i < T1.size() - 1 && T1[i].pos != T1[i + 1].pos) || i == T1.size() - 1))
			++tot, ql[tot] = lll, qr[tot] = rrr, ind[tot] = max(1, T1[i].pos);
	}
	for(int i = 1; i <= tot; i++)
		cp[2 * i - 1] = ql[i], cp[2 * i] = qr[i] + 1;
	sort(cp + 1, cp + 2 * tot + 1);
	int len = unique(cp + 1, cp + 2 * tot + 1) - cp - 1;

	f[0][1] = 1;
	for(int i = 1; i <= tot; i++) {
		int x = ((i == tot) ? (n + 1 - ind[i]) : (ind[i + 1] - ind[i]));
		int qa = lower_bound(cp + 1, cp + len + 1, ql[i]) - cp;
		int qb = lower_bound(cp + 1, cp + len + 1, qr[i] + 1) - cp - 1;

		for(int j = qa; j <= qb; j++) {
			int sum = 0;
			upd(f[i][j], 1ll * g[i - 1][qa - 1] * (GG(ind[i] - 1, x, ql[i], cp[j + 1] - 1) - GG(ind[i] - 1, x, ql[i], cp[j] - 1) + Mod) % Mod);
			upd(f[i][j], 1ll * ((g[i - 1][j - 1] - g[i - 1][qa - 1] + Mod) % Mod) * (G(ind[i] - 1, x, cp[j + 1] - 1) - G(ind[i] - 1, x, cp[j] - 1) + Mod) % Mod);
			upd(f[i][j], 1ll * f[i - 1][j] * G(ind[i] - 1, x, cp[j + 1] - 1) % Mod);
		}
		for(int j = 1; j <= len; j++)
			g[i][j] = g[i][j - 1],
			upd(g[i][j], f[i][j]);
	}
	int sum = 0;
	for(int i = 0; i <= len; i++) upd(sum, f[tot][i]);
	cout << sum << '\n';

	for(int i = 0; i <= tot; i++)
		for(int j = 0; j <= len; j++) f[i][j] = 0;
	for(int i = 1; i <= tot; i++) ql[i] = qr[i] = ind[i] = 0;
	tot = 0;
}

int main() {
	fac[0] = 1; for(int i = 1; i <= M; i++) fac[i] = 1ll * fac[i - 1] * i % Mod;
	invf[M] = qpow(fac[M], Mod - 2); for(int i = M - 1; i >= 0; i--) invf[i] = 1ll * invf[i + 1] * (i + 1) % Mod;

	int T; cin >> T;
	while(T--) init();
}

总结:

  • 刻画充要条件方面,冒泡排序考虑 01 序列。
  • 转移方面,\(O(n^2)\) 做法提示,对于一个排列计数,如果之和相对大小有关,可以直接考虑最后一位填什么,从而确定最后一位的相对大小关系,然后保持之前的大小关系。
  • \(O(n^2)\)\(O(m^2)\) 这一步,其实状态设计和动机都很容易,最重要的一点是保持自信,相信这个东西是基础的组合数学就能算出来的,保持清晰的头脑。我只能这么说。我第一次尝试的时候到最后头脑很不清晰,完全丧失思考能力。第二次甚至第三次才理清思路。

CF156D

tag:prufer 序列,egf

联通块缩点,有 \(k\) 个联通块,大小分别为 \(s_1, s_2, \dots, s_m\)。对于构成的一棵树,若每个点的度数为 \(d_1, d_2, \dots, d_m\),那么显然它的权为 \(\prod\limits_{i = 1}^m s_i^{d_i}\)。于是一棵树的权只和每个点度数有关。接下来我们默认 \(d_i\) 为度数减一,方便写 prufer 序列式子。那么答案为:

\[\sum\limits_{d_1 + d_2 + \dots + d_m = m - 2}\binom{m - 2}{d_1, d_2, \dots, d_m}\prod\limits_{i = 1}^ms_i^{d_i + 1}\\ = (m - 2)!\prod\limits_{i = 1}^m s_i\sum\limits_{d_1 + d_2 + \dots + d_m = m - 2}\prod\limits_{i = 1}^m \dfrac{s_i^{d_i}}{d_i!} \]

前面 \((m - 2)!\prod\limits_{i = 1}^m\) 为常数。对于后半部分,是一个 egf 卷积的形式。设 \(F_i(x)\)\(<s_i^0, s_i^1, s_i^2, \dots>\) 的 egf,那么有 \(F_i(x) = e^{s_i}x\)。于是后半部分等于:

\[[x^{m - 2}]\prod\limits_{i = 1}^m F_i(x)\\ = [x^{m - 2}]\prod\limits_{i = 1}^m e^{s_ix} \\ = [x^{m - 2}]e^{\sum\limits s_ix} = \dfrac{n^{m - 2}}{(m - 2)!} \]

最终答案为 \(n^{m - 2}\prod\limits_{i = 1}^m s_i\)

总结:

  • prufer 序列似乎本身是一件很灵活的事情。不能仅仅把它看成树和数列的双射关系,掌握它的构造也是有必要的。
  • 这个 egf 卷积的结果很漂亮,需要注意。

ARC162E

tag:基础组合数。

我不会基本的组合数了。

\(h_i\) 为大于等于 \(i\) 的位置数量。首先想到简单刻画一下条件,对于每一种数 \(i\) 若有 \(c_i\) 个,则要求它所分布的 \(c_i\) 个位置均有 \(a_i \ge c_i\)。注意到这两部分是相对独立的,于是考虑枚举数集 \(c\),然后把问题分成这样两个部分。

然后我在组合数上面出问题了。直接的想法是:按照 \(c_i\) 从大到小,然后对于每一个 \(c_i\) 计数,比如 \(H_i\) 然后算算。这样会算出来很多重复的,但是重复的只可能是 \(c\) 相同的情况。于是枚举 \(c\) 的数量 \(t\) 然后计算。令比它大的颜色有 \(i\) 种,这些颜色一共占了 \(j\) 个位置,那么给这 \(c\) 个分配颜色是 \(\binom{H_c - i}{t}\),分配位置是 \(\binom{H_c - j}{i,i,\dots, H_c -ji}\),两者相乘。这样算是不重复的,因为 \(\binom{H_c - i}{t}\) 相当于选出来了有序的 \(t\) 个序号,剩下的相当于给序号分配。

\(f[i, j, k]\)\(i, j\) 同上 \(k\) 代表当前的 \(c\) 然后直接转移。\(i\)\(\dfrac{n}{k}\) 级别的,同时 \(t\) 也是 \(\dfrac{n}{k}\) 级别的,所以方案和转移数最后是 \(O(n^3)\) 的。

总结

  • 这道题最大的问题在于我基本的组合数出了错。这太不应该了。也是我计数的问题。我甚至并没有弄清楚多重组合数的定义:有 \(n\)完全相同小球\(m\)互不相同的盒子,这 \(m\) 个盒子每个容量为 \(s_i\),问最终分配方式。

ARC162D

tag:prufer 序列,考虑判定。

简单数数。

为了方便 prufer 序列相关,以下默认对于 \(1\) 点的 \(d_1 \gets d_1 - 1\)

对于点 \(u\) 考虑它贡献次数。于是树分成两部分,一部分是 \(u\) 子树,其中所有点都不小于 \(u\),另一部分是把 \(u\) 变为叶子。令 \(u\) 的子树集合为 \(S\)(不含 \(u\)),那么根据 prufer 序列,子树内部的方案数应该是 \(\dfrac{(|S| - 2)!}{(d_u - 1)!\prod\limits_{x\in S}d_x!}\),外面的方案数应该是 \(\dfrac{(n - |S| - 1)!}{\prod\limits_{x\not\in S, x\not=u}d_x!}\),于是总方案数为 \(\dfrac{(|S| - 2)!(n - |S| - 1)!d_u}{\prod\limits_{x = 1}^n d_x!}\)。当 \(\sum\limits_{x\in S}d_x + d_u - 1 = |S| - 2\) 时此式子才有意义。

注意到,这个式子只和 \(|S|\)\(u\) 相关。很容易想到设 \(f[i, j, k]\) 为只用了 \([i, n]\) 里面的点,选出来一个子集使得该子集大小为 \(j\)\(d\) 之和为 \(k\)。然后枚举 \(u\) 直接算贡献就好了。

特别的,对于 \(|S| = 1\) 的情况要特判。这种情况下能够产生贡献的有且仅有叶子节点,根也要特判。


ARC163D

tag:竞赛图,均摊贡献,基础组合数。

众所周知竞赛图缩点后变成链,要求链总点数,不可能是以一条链为计数对象,所以考虑以“链中间的缝隙”为计数对象。具体而言将 \(n\) 个点划分成两个部分 \(S, (U - S)\),如果这两部分构成了链中间的缝隙,那么显然 \(S\) 内的点均往 \((U - S)\) 连边。

以此我们来考虑这一个“缝隙”贡献的次数。也就是 \(S\)\((U - S)\) 分成两部分后可能合法图数量。显然还要记录 \(u<v\) 的边数量。设 \(g[i, x, c]\)\([1, i]\) 划分成两个集合,第一个集合有 \(x\) 个数,第一个集合向第二个集合连边产生 \(c\)\(u<v\) 的点对,这样集合划分方案。对于集合内部的计数就是一个简单的组合数。于是显然有:

\[ans = \sum\limits_{i = 1}^{n - 1}\sum\limits_{a = 0}^m \sum\limits_{b = 0}^{m - a}\binom{i}{a}\binom{n - i}{b}g[n, i, m - a - b] \]

总结:

  • 对于所有可能的链计数,可以考虑中间缝隙的贡献次数。类似的技巧还有 ARC187B
  • 这个时候一定要想清楚,对于缝隙计数的话,数的是外面的形态数量!我一开始想的很迷糊。

P7213

tag:分析性质,计数转判定;延迟计算贡献

神仙题!根本不会。

首先注意到原来的变化过程过于“动态”,现在我们希望它能够相对“静态”,比如能够从后向前(我自己做的时候是按照值域从大到小,也得到一个充要条件,但是做不了……)做。

考虑如何从后向前确定。首先第 \(n\) 个数可以直接确定,不可能变动。接下来令后 \((k - 1)\) 都直接确定,考虑最后第 \(k\) 个数的变化。若 \(k\) 在之前的某一伦次变小,那么一定可以找到和它相同的最靠后的那一个位置,而那个位置以后永远也不会动。于是我们记录每一个数从后向前第一次出现的位置集合 \(S\)\(k\) 如果在 \(S\) 内出现过,那么就会不断减一,直到没有在 \(S\) 出现过加入 \(S\),或者直接减到 \(0\)

注意到,题目中要求仅仅和“是否会减到 \(0\)”有关,于是我们仅仅记录 \(S\) 中从 \(1\) 开始的极长连续段长度:设 \(f[i, j]\) 为后 \(i\) 个数中,拥有从 \(1\) 开始极长连续段长度为 \(j\) 的方案数。

转移过程中,为了方便我们区分每种颜色的两个值。最后方案数除以 \(2^n\) 即可。

考虑转移:

  • \(i\) 位被删空。
    这个情况下 \(i\) 原值 \(x\) 要满足的条件为:\(x \le j\),同时不能重复使用。设后面 \(c_0\) 个被删到 \(0\)\(c_1\) 个被保留的。在 \(c_0\) 个被删到 \(0\) 的显然都 \(\le j\)。(\(>j\) 的一定会在 \((j+1)\) 位置卡住),保留的 \(c_1\) 个位置中互不相同,那么就只有构成连续段的 \(j\) 个会影响到。于是 \(f[i, j]\)\(f[i - 1, j]\) 转移,转移系数为 \((j - c_0)\)
  • \(i\) 位没有被删空。
    此时显然的 \(i\) 位置最终保留值一定大于 \(j\)
    • \(i\) 最终保留值大于 \((j + 1)\)。这个就很厉害了,我们延迟计算这部分贡献,到后面计算。
    • \(i\) 最终保留值恰好为 \((j + 1)\)
      • 需要注意的是,此时极长连续段长度不一定就是 \((j+ 1)\),因为之前我们保留的那一部分还有可能会产生影响。细致的讨论一下这部分计算,若 \(f[i - 1, j]\) 转移到 \(f[i, k]\),那么 \(j+2\sim k\)\(k - j - 1\) 个数就是新增的加入极长段,它的可能位置有 \(\binom{c_1 - j}{k - j - 1}\),这些位置的原始取值并不好计算,因为可能会有减小,设为一个系数 \(g[k - j - 1]\),然后是 \(i\) 位置的取值数量。对于 \(i\) 位置,取值应当在 \((j + 1)\sim k\),共 \(2(k - j)\) 个数,要减去后面的 \((k - 1)\) 个保留位置,也就是 \((k - j + 1)\) 个位置,于是就是:\(\sum\limits_{j = 0}^{k - 1}f[i - 1, j] \binom{c_1 - j}{k - j - 1}(k - j + 1)g[k - j - 1]\) 转移到 \(f[i, k]\)

\(g[n]\) 的转移为 \(g[n] = \sum\limits_{i = 1}^{n - 1}g[i - 1]g[n - i]\binom{n - 1}{i - 1}(n -i +2)\)


P14568

tag:构造,诈骗。

场后看的。搞笑诈骗题。

观察性质,因为直接判定必然规避不掉求解前缀后缀最值状物,这两个不能增量做,所以考虑构造状物。如果只涉及 0,1 或者只涉及 2,3 那么很容易构造出解,以 0,1 为例,如果是 0 就将前面集体 \(+1\),最后一个设为 \(1\),如果为 \(1\) 就设为 \(i\)。从构造出发我们可以想到如果只有 0,1 答案是唯一的,归纳法即可证明。

称 0,1 为白点,而 2,3 为黑点。黑点白点内部顺序可以直接确定,那么从小到大插入,设 \(f[i, j]\) 为白点插入了 \(i\) 个,黑点插入了 \(j\) 个,大小为 \((i + j + 1)\) 的点一定插入在第 \((i+1)\) 个白点处或者第 \((j + 1)\) 个白点处。是否合法的判定为插入前 \(i\) 个白点和前 \(j\) 个黑点后,是否有完全为空或者完全被填满的一段前缀或后缀,直接预处理即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 5000, Mod = 998244353;
bool st;
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int o[N + 10], n, val[N + 10];
struct node {
	int pos, val;
	bool operator < (const node &other) const {
		return val < other.val;
	}
};
vector <node> sa, sb;
int lpre[N + 3][N + 3], lsuf[N + 3][N + 3];
int al[N + 3][N + 3], ar[N + 3][N + 3], f[N + 3][N + 3];
bool flag[N + 10];
bool ed;
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> o[i];
	int tot = 0;
	for(int i = 1; i <= n; i++) {
		if(o[i] == 0) {
			for(int j = 0; j < i; j++)
				if(o[j] <= 1) val[j]++;
			val[i] = 1;
			++tot;
		} 
		else if(o[i] == 1) val[i] = ++tot;
	}
	tot = 0;
	for(int i = n; i >= 1; i--) {
		if(o[i] == 2) {
			for(int j = i + 1; j <= n; j++)
				if(o[j] > 1) val[j]++;
			val[i] = 1;
			++tot;
		}
		else if(o[i] == 3) val[i] = ++tot;
	}
	for(int i = 1; i <= n; i++)
		if(o[i] <= 1) sa.push_back((node){i, val[i]});
		else sb.push_back((node){i, val[i]});
	sort(sa.begin(), sa.end());
	sort(sb.begin(), sb.end());

	int j1 = 1, j2 = n;
	al[0][0] = n + 1, ar[0][0] = 1, lsuf[0][0] = n + 1;
	for(int j = 0; j < sb.size(); j++) {
		flag[sb[j].pos] = 1;
		al[0][j + 1] = min(al[0][j], sb[j].pos);
		ar[0][j + 1] = max(ar[0][j], sb[j].pos);
		while(j1 <= n && flag[j1]) j1++;
		while(j2 >= 1 && flag[j2]) j2--;
		lpre[0][j + 1] = j1 - 1, lsuf[0][j + 1] = j2 + 1;
	}
	for(int i = 0; i < sa.size(); i++) {
		for(int i = 1; i <= n; i++) flag[i] = 0;
		j1 = 1, j2 = n;
		al[i + 1][0] = n + 1, ar[i + 1][0] = 1;
		for(int j = 0; j <= i; j++) {
			flag[sa[j].pos] = 1;
			al[i + 1][0] = min(al[i + 1][0], sa[j].pos);
			ar[i + 1][0] = max(ar[i + 1][0], sa[j].pos);
			while(j1 <= n && flag[j1]) j1++;
			while(j2 >= 1 && flag[j2]) j2--;
			lpre[i + 1][0] = j1 - 1, lsuf[i + 1][0] = j2 + 1;
		}
		for(int j = 0; j < sb.size(); j++) {
			flag[sb[j].pos] = 1;
			al[i + 1][j + 1] = min(al[i + 1][j], sb[j].pos);
			ar[i + 1][j + 1] = max(ar[i + 1][j], sb[j].pos);
			while(j1 <= n && flag[j1]) j1++;
			while(j2 >= 1 && flag[j2]) j2--;
			lpre[i + 1][j + 1] = j1 - 1, lsuf[i + 1][j + 1] = j2 + 1;
		}
	}

	f[0][0] = 1;
	for(int i = 0; i <= sa.size(); i++) {
		for(int j = 0; j <= sb.size(); j++) {
			int pos;
			if(i < sa.size()) {
				pos = sa[i].pos;
				if(o[pos] == 0) {
					if(al[i][j] > pos)
						upd(f[i + 1][j], f[i][j]);
				} 
				else if(o[pos] == 1) {
					if(lpre[i][j] == pos - 1) 
						upd(f[i + 1][j], f[i][j]);
				}
			}
			if(j < sb.size()) {
				pos = sb[j].pos;
				if(o[pos] == 2) {
					if(ar[i][j] < pos)
						upd(f[i][j + 1], f[i][j]);
				}
				else if(o[pos] == 3) {
					if(lsuf[i][j] == pos + 1)
						upd(f[i][j + 1], f[i][j]);
				}
			}
		}
	}
	cout << f[sa.size()][sb.size()] << '\n';
	// cerr << (&ed - &st) / 1024.0 / 1024.0 << '\n';
}

总结:

  • 遇到一道题目先不要管别人的评价,我一开始因为听说这题很简单所以各种兜圈子急眼
  • 如果难以判定,那么考虑构造。
  • 从简单的部分分入手!

P5999

tag:连续段 dp。

哇奥,是新的 trick 喔!

连续段 dp 的特征是:如果我们将元素按一定顺序插入,如果规定只能插入在序列两端,那么很容易刻画序列形态。对于这道题,从小到大插入,只能插入在序列两端,注意到这个序列只可能在长度不超过 \(3\) 合法。为了规定好,我们强制规定一个连续段的结尾下降,开头上升。这样就使序列在开头结尾插入一定不合法。这样的一个块叫做联通块。注意,刻画一个序列并不需要联通块是极大的。

\(f[i, j]\) 为插入了 \(1\sim i\) 里面的数,形成了 \(j\) 个连续段的方案数。不考虑 \(s, t\),新开一个段有 \(f[i, j] = jf[i - 1, j - 1]\),合并两个段有 \(f[i, j] = jf[i - 1, j+1]\)。现在考虑 \(s, t\),这两个是对称的,只能在开头开新段,或者这个时候是可以合并在第一个段最前面(最后一个段最后面),那么就是 \(f[i, j] = f[i - 1, j - 1] + f[i - 1, j]\)

不考虑 \(s, t\) 时候新开一个段也要考虑能不能再加到开头结尾了。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e3, Mod = 1e9 + 7;
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int f[N + 3][N + 3], n, s, t;

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> s >> t;
	f[0][0] = 1;
	for(int i = 1; i <= n; i++) {
		if(i == s || i == t) {
			for(int j = 1; j <= i; j++)
				upd(f[i][j], f[i - 1][j - 1]),
				upd(f[i][j], f[i - 1][j]);
		}
		else {
			for(int j = 1; j <= i; j++)
				upd(f[i][j], 1ll * j * f[i - 1][j + 1] % Mod),
				upd(f[i][j], 1ll * (j - (i > s) - (i > t)) * f[i - 1][j - 1] % Mod);
		}	
	}
	cout << f[n][1] << '\n';
}

P7967

tobedone


P10547

tobedone


P9197

tobedone


ARC156D

tag:异或,Lucas 定理。

这也太牛了!

注意到这样 xor 可以消除很多重复的和,这启发我们直接统计每个和出现次数,也就是:

\[\operatorname{xor}_{\sum c_i = k}(\binom{k}{c_1, c_2, \dots, c_n}\bmod 2)\sum\limits_{i = 1}^n c_i a_i \]

如果你对 P3773 还有印象,一定会想到利用 Lucas 定理拆开上面的组合数挖掘性质。

\[\binom{k}{c_1, c_2, \dots, c_n} = \prod\limits_{i = 1}^n\binom{k - \sum\limits_{j = 1}^{i - 1}c_j}{c_i} \]

根据 Lucas 定理可以得到 \(\binom{m}{n} \bmod 2 = 1\iff n\subseteq m\),于是可以得到 \(c_1, c_2, \dots, c_n\)\(k\) 的一种不交划分。到了这里就很容易了,直接拆位然后算贡献。因为 \(+, \operatorname{xor}\) 都是只涉及低位向高位的,设 \(f[i, S]\) 为考虑了 \([0, i)\) 这些位的 xor,向 \(i\) 之后进位为 \(S\)。大力转移即可。

总结:主要在于观察到把式子化成那种形式,也许多手玩手玩就会出来了?


ARC156E

首先考虑判定,结论是设 \(S = \sum\limits_{i = 1}^n X_i\),若 \(S\) 为偶数且 \(X_i + X_{i \bmod n + 1} \le \dfrac{S}{2}\) 那么合法。必要性显然,充分性上,若 \(\max(X_i + X_{i \bmod n + 1}) < \dfrac{S}{2}\) 那么显然配对任何一对下一对一定满足,否则,当 \(n = 4\) 时条件显然成立,当 \(n > 4\) 时因为最多只会存在一个 \(i\) 满足 \(X_{i} + X_{i\bmod n + 1} = \dfrac{S}{2}\),所以直接给其中一个和另外任意一个匹配上即可。

现在考虑计数。

很容易注意到满足 \(X_i + X_{i\bmod n + 1} > \dfrac{S}{2}\)\(i\) 最多只有两个,并且一定相邻,于是可以想到钦定一个或者两个位置不合法,然后做容斥。

首先考虑不钦定任何位置的情况,要求 \(S\) 为偶数且 \(S\le K\)。若 \(S = K\),那么就是很经典的容斥问题,钦定哪些位置是超出 \(m\) 的,那么就有:

\[g(n, K) = \sum\limits_{i = 0}^n(-1)^i\binom{n}{i}\binom{K - (m+1)i + n - 1}{n - 1}\\f(n, K) = \sum\limits_{t = 0}^{\frac{K}{2}}g(n, 2t), h(n, K) = \sum\limits_{t = 0}^{K}g(n,t) - f(n, K)\\ N_1 = f(n, K) \]

钦定 \(X_i + X_{i\bmod n + 1} > S/2\),那么剩下的 \((n - 2)\) 个数字只和就应该小于 \(X_i + X_{i\bmod n + 1}\),那么就有方案数为:

\[N_2 = n\sum\limits_{a,b \in [0, m]}([(a+b)\bmod 2 = 0]f(n - 2, \min(k - a - b, a + b - 1)) +[(a+b)\bmod 2 = 1]h(n - 2, \min(k - a - b, a + b - 1))) \]

钦定 \(X_{i - 1}+ X_i > S/2, X_{i} + X_{i\bmod n + 1} > S/2\),记 \(a = X_{i - 1}, b = X_{i}, c = X_{i\bmod n + 1}, T = S - a - b - c\),那么就应该有 \(b - S > |a - c|\),于是:

\[N_3 = n\sum\limits_{a, b, c\in[0, m]} ([(a+b+c)\bmod 2 = 0]f(n - 3, \min(k - a - b - c, b - |a - c| - 1)) + [(a+b+c) \bmod 2 = 1]h(n - 3, \min(k - a - b - c, b - |a - c| - 1))) \]

最终答案为 \((N_1 - N_2 + N_3)\)。现在问题就出在加快运算了。


\(N_2, N_3\) 的计算中,\(f\) 涉及到的第一维只有 \((n - 2), (n - 3)\),第二维的范围是 \(O(m)\) 的,在预处理 \(f\)\(h\) 上容易 \(O(nm)\)。计算 \(N_2\) 时本身就是 \(O(m^2)\) 的,计算 \(N_3\) 时,枚举 \(a, b\),不失一般性,假设 \(c>a\)(对于 \(a = c\) 单独算),分类讨论一下 \(k - a - b - c\)\(b - c + a - 1\) 的大小,然后维护一些前缀和状物即可。

考虑 \(N_1\) 的计算。我是不会告诉你这个地方我想了一年都没想出来 QAQ 呜呜呜怎么这么笨式子重新列出来:

\[\sum\limits_{t = 0}^{\frac{K}{2}}\sum\limits_{i = 0}^n (-1)^i \binom{n}{i}\binom{2t - (m+1)i + n - 1}{n - 1}\\ = \sum\limits_{i = 0}^n(-1)^i \binom{n}{i}\sum\limits_{t = 0}^{\frac{K}{2}}\binom{2t - (m+1)i+n - 1}{n - 1} \]

注意到 \(-(m+1)i+n - 1\) 为常数,预处理前缀和即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 3000 * 3000 + 3000, M = 3000, Mod = 998244353;
int Abs(int x) {
	return ((x < 0) ? (-x) : (x));
}
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int qpow(int n, int m) {
	int res = 1;
	while(m) {
		if(m & 1) res = 1ll * res * n % Mod;
		n = 1ll * n * n % Mod;
		m >>= 1;
	}
	return res;
}
int fac[N + 3], invf[N + 3];
int C(int n, int m) {
	if(m > n) return 0;
	if(n < 0) return 0;
	return 1ll * fac[n] * invf[n - m] % Mod * invf[m] % Mod;
}

int n1, n2, n3, n, m, qk;
int f[M * 2 + 3], g[M * 2 + 3], h[M * 2 + 3];
void prep(int n, int k) {
	for(int K = 0; K <= k; K++) {
		g[K] = 0;
		for(int i = 0; i <= n; i++) {
			int t1 = ((i % 2 == 0) ? 1 : (Mod - 1));
			int t2 = 1ll * C(n, i) * C(K - i * (m + 1) + n - 1, n - 1) % Mod;
			upd(g[K], 1ll * t1 * t2 % Mod);
		}
	}
	f[0] = g[0], h[0] = 0;
	for(int K = 1; K <= k; K++) {
		f[K] = h[K] = 0;
		upd(f[K], f[K - 1]); upd(h[K], h[K - 1]);
		if(K % 2 == 0) upd(f[K], g[K]);
		else upd(h[K], g[K]);
	}
}

int s[N + 10], w[N + 10], sf[N + 10], sh[N + 10];
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	fac[0] = 1; for(int i = 1; i <= N; i++) fac[i] = 1ll * fac[i - 1] * i % Mod;
	invf[N] = qpow(fac[N], Mod - 2); for(int i = N - 1; i >= 0; i--) invf[i] = 1ll * invf[i + 1] * (i + 1) % Mod;

	cin >> n >> m >> qk; qk = (qk / 2) * 2;
	prep(n - 2, 2 * m);
	for(int a = 0; a <= m; a++) {
		for(int b = 0; b <= m && b + a <= qk; b++) {
			if(!a && !b) continue ;
			if((a + b) % 2 == 0)
				upd(n2, f[min(qk - a - b, a + b - 1)]);
			else upd(n2, h[min(qk - a - b, a + b - 1)]);
		}
	}
	n2 = 1ll * n * n2 % Mod;

	prep(n - 3, m);
	s[0] = f[0], w[0] = h[0];
	for(int i = 1; i <= m; i++) 
		s[i] = s[i - 1], upd(s[i], ((i % 2 == 1) ? h[i] : f[i])),
		w[i] = w[i - 1], upd(w[i], ((i % 2 == 0) ? h[i] : f[i]));
	for(int a = 0; a <= m; a++) {
		for(int b = 0; a + b <= qk && b <= m; b++) {
			int lc = a + 1, rc = min(m, qk - a - b);
			int lw, rw;
			if(lc > rc) continue;
			if(2 * (a + b) >= qk + 1) lw = max(0, qk - a - b - rc), rw = qk - a - b - lc;
			else lw = max(0, b - rc + a - 1), rw = b - lc + a - 1;

			if(rw < lw) continue;
			if((a + b + lc) % 2 == rw % 2) upd(n3, (s[rw] - ((lw == 0) ? 0 : s[lw - 1]) + Mod) % Mod);
			else upd(n3, (w[rw] - ((lw == 0) ? 0 : w[lw - 1]) + Mod) % Mod);
		}
	}
	n3 = 2ll * n * n3 % Mod;
	for(int a = 0; a <= m; a++) {
		for(int b = 0; b <= m; b++) {
			int w = min(qk - 2 * a - b, b - 1);
			if(w < 0) continue;
			if(b % 2 == 0) upd(n3, 1ll * n * f[w] % Mod);
			else upd(n3, 1ll * n * h[w] % Mod);
		}
	}
	
	sf[0] = 1, sh[0] = 0;
	for(int t = 1;  t <= qk; t++) {
		sf[t] = sf[t - 1], sh[t] = sh[t - 1];
		if(t % 2 == 0) upd(sf[t], C(t + n - 1, n - 1));
		else upd(sh[t], C(t + n - 1, n - 1));
	}

	for(int i = 0; i <= n; i++) {
		int t1 = ((i % 2 == 0) ? 1 : (Mod - 1));
		int t2 = C(n, i);

		int tr = 2 * (qk / 2) - (m + 1) * i;
		if(tr < 0) continue;
		int t3 = ((tr % 2 == 0) ? sf[tr] : sh[tr]);
		upd(n1, 1ll * t1 * t2 % Mod * t3 % Mod);
	}

	// cout << n1 << ' ' << n2 << ' ' << n3 << endl;
	upd(n1, Mod - n2), upd(n1, n3);
	cout << n1 << '\n';
}
总结:
  • 虽然说这样有点吃不到葡萄说葡萄酸之嫌,但是这题确实就是恶臭风格计数题开场对充要条件的转化,还有发掘最多只有两个位置不合法其实还是挺考验手法的。
  • 对于这道题的充要条件,会先从一个必要条件:如果 \(X_i + X_{i - 1} > S - X_i - X_{i - 1}\) 那么一定无解出发,再到这个条件就是充要条件。因为可以注意到这个问题的匹配方式很自由,所以不妨大胆直接从必要条件出发,很可能就是充分的。
  • 观察性质。

ARC158F

tag:考虑判定。

还差一步啊,还是差了一些!

面对这个问题分析充要条件,我们将给定的序列倒序那么相当于一个分别以 \(K_1, K_2, \dots, K_M\) 作为最高到最低关键字的稳定排序。很显然,重复出现的 \(K\) 只有第一个会起作用,只要知道这个去重后的结果,而之后出现的一定可以用组合数计算。

先考虑计算去重后的结果,这个时候则必须要涉及到有关 \(A,B\) 次序相关的手段。因为是稳定排序,所以可以唯一确定两个排列中数的对应关系。这里我自己思考的时候犯了一个错,我以为对于任意 \(i < j\) 都要确定先后关系才能唯一确定,实际上不是的,只要确定对于 \(i, (i+1)\) 的先后关系即可。可以把确定出来的关系看作拓扑序的约束,这样就很显然只要知道一条链的约束就可以唯一确定了。

具体而言,令 \(o_i\) 为在原序列中的次序。

  • \(o_i < o_{i+1}\)
    • \(S_1\) 中的数位为 \(B_i\)\(B_{i+1}\) 小的数位,\(S_2\) 中的为 \(B_i\)\(B_{i+1}\) 大的数位。
    • 要求 \(S_1\) 中最早出现的早于 \(S_2\) 中最早出现的,或者 \(S_2\) 中根本没有出现的数。
  • \(o_i > o_{i+1}\)
    • 类似的定义 \(S_1, S_2\),则要求 \(S_1\) 中最早出现的早于 \(S_2\) 中最早出现的,并且强制规定 \(S_1\) 中一定出现过至少一个。

产生了 \(O(n)\) 个约束。这样的计数看上去很难。但是注意到只有“强制规定 \(S_1\) 中至少出现过一个”和上面几个约束本质不同。直接状压对于 \(f[S]\)\(S\) 内满足其它几个约束的排列数。对着最后的 \(S\) 判断“至少在 \(S_1\) 中出现过一次”的约束即可。现在约束都形如 \((S_i, T_i)\)\(S_i\) 最早出现的一定在 \(T_i\) 之前。考虑 \(f[x]\) 转移到 \(f[S + \{x\}]\),如果存在一个 \(x\in T_j\)\(S_i \cap S = \empty\),那么不合法。\(S_i \cap S = \empty \iff S_i\subseteq (U - S)\),反过来我们选择判断此时是否存在 \(x\in T_i\)。这是一个高维前缀或的形式,可以预处理。而“强制规定 \(S_1\) 中至少出现过一个”的约束也可以用类似的高维前缀和解决。

现在问题变成了有 \(t\) 个不同的数,约束好了第一次出现的顺序,问构造成长度为 \(m\) 的序列方案数。设 \(g[i, j]\) 为填了前 \(i\) 个数目前出现了 \(j\) 个数,那么转移很显然是 \(g[i, j] = jg[i - 1, j] + g[i - 1, j - 1]\)。我们惊喜的发现这个东西的递推式和初值和第二类斯特林数一模一样!所以这个东西就是 \(S(m, t) = \sum\limits_{i = 0}^t \dfrac{(-1)^{t - i}i^m}{(t - i)!i!}\),就可以快速计算了。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5, K = 18;
const int Mod = 998244353;
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int qpow(int n, int m) {
	int res = 1;
	while(m) {
		if(m & 1) res = 1ll * res * n % Mod;
		n = 1ll * n * n % Mod;
		m >>= 1;
	}
	return res;
}
int n, m, qk;
struct node {
	int type, S1, S2;
} Q[N + 10];
int w[(1 << K) + 10], wc[K + 3][(1 << K) + 10], wf[(1 << K) + 10], tmp[(1 << K) + 10];

ll cp[N + 10], len = 0; vector <int> pos[N + 10];
ll a[N + 10], b[N + 10]; int o[N + 10];

void fwt(int *arr, int n) {
	for(int k = 1; k < (1 << n); k <<= 1) {
		for(int i = 0; i < (1 << n); i += (k << 1)) {
			for(int j = 0; j < k; j++) {
				ll rest = arr[i + j];
				arr[i + j] = rest % Mod;
				arr[i + j + k] = ((rest + arr[i + j + k]) % Mod + Mod) % Mod;
			}
		}
	}
}

int f[(1 << K) + 10];
int sum[K + 10], fac[K + 10], invf[K + 10];
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> qk;
	for(int i = 1; i <= n; i++) cin >> a[i], cp[i] = a[i];
	sort(cp + 1, cp + n + 1); len = unique(cp + 1, cp + n + 1) - cp - 1;
	for(int i = 1; i <= n; i++) {
		cin >> b[i];
		a[i] = lower_bound(cp + 1, cp + len + 1, a[i]) - cp;
		pos[a[i]].push_back(i);
	}
	for(int i = n; i >= 1; i--) {
		int d = lower_bound(cp + 1, cp + len + 1, b[i]) - cp;
		o[i] = pos[d].back();
		pos[d].pop_back();
	}

	int U = (1 << qk) - 1;
	for(int i = 1; i < n; i++) {
		int t1[20], t2[20], len1 = 0, len2 = 0;
		for(int j = 1; j <= qk; j++) t1[j] = t2[j] = 0;

		ll x; x = b[i]; while(x) t1[++len1] = x % 10, x /= 10;
		x = b[i + 1]; while(x) t2[++len2] = x % 10, x /= 10;

		if(o[i] < o[i + 1]) Q[i].type = 1;
		else Q[i].type = 2;
		int S1 = 0, S2 = 0;
		for(int j = 1; j <= qk; j++)
			if(t1[j] < t2[j]) S1 |= (1 << (j - 1));
			else if(t1[j] > t2[j]) S2 |= (1 << (j - 1));

		Q[i].S1 = S1, Q[i].S2 = S2;
		w[S1] |= S2;
		if(Q[i].type == 2) wf[S1]++;
	}

	for(int i = 0; i < (1 << qk); i++) {
		for(int j = 0; j < qk; j++)
			if((w[i] >> j) & 1) wc[j][i] = 1;
	}
	for(int j = 0; j < qk; j++) {
		for(int i = 0; i < (1 << qk); i++) tmp[i] = wc[j][i];
		fwt(tmp, qk);
		for(int i = 0; i < (1 << qk); i++) wc[j][i] = tmp[i];
	}
	fwt(wf, qk); reverse(wf, wf + (1 << qk));

	f[0] = 1;
	for(int S = 0; S < (1 << qk); S++) {
		for(int x = 0; x < qk; x++) {
			if(!((S >> x) & 1)) {
				if(!wc[x][U - S])
					upd(f[S + (1 << x)], f[S]);
			}
		}
	}

	fac[0] = 1; for(int i = 1; i <= qk; i++) fac[i] = 1ll * fac[i - 1] * i % Mod;
	invf[qk] = qpow(fac[qk], Mod - 2); for(int i = qk - 1; i >= 0; i--) invf[i] = 1ll * invf[i + 1] * (i + 1) % Mod;
	for(int t = 0; t <= qk; t++) {
		for(int i = 0; i <= t; i++) {
			int t1 = (((t - i) % 2 == 0) ? (1) : (Mod - 1));
			int t2 = qpow(i, m);
			int t3 = 1ll * invf[t - i] * invf[i] % Mod;
			upd(sum[t], 1ll * t1 * t2 % Mod * t3 % Mod);
		}
	}

	int ans = 0;
	for(int S = 0; S < (1 << qk); S++)
		if(!wf[S]) upd(ans, 1ll * f[S] * sum[__builtin_popcount(S)] % Mod);
	cout << ans << '\n';
}

ARC160C

tag:判定考虑构造。

这 tm 才 *1861?我请问了。

考虑判定毫无前途,考虑从小到大构造这个集合。设 \(f[i, j]\) 为目前合并出的最大值是 \((i+1)\)\(j\) 个的方案数,那么转移有 \(f[i, \lfloor\frac{c_{i} +j}{2}\rfloor] = \sum\limits_{k = j}^n f[i - 1, k]\)

这乍一看是 \(O(n^2)\) 的,我们精细分析下,令 \((i - 1)\) 时刻 \(j\) 值域为 \([V_1, V_2]\),那么 \(i\) 时刻 \(j\) 的值域就为 \([\frac{c_{i}}{2}, \frac{V_2 + c_{i}}{2}]\),初始值域为 \([0, 0]\)。这样一共会进行 \(O(n)\) 轮。第一轮值域 \([\frac{c_1}{2}, \frac{c_1}{2}]\),第二轮 \([\frac{c_2}{2}, \frac{c_1}{4} +\frac{c_2}{2}]\),以此类推,最后值域大小总和是 \(\sum\limits c_i(1+\frac{1}{2}+\frac{1}{4} + \dots)\),也就是 \(O(n)\) 级别的。

实现上第一维滚动掉,记录每一轮第二维值域,维护前缀和。

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 30, Mod = 998244353;
void upd(int &x, int y) {
	x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int a[N + 10], n, c[N + 10];
int f[N + 10], g[N + 10], h[N + 10];

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i], c[a[i]]++;

	f[0] = 1;
	int V1 = 0, V2 = 0;
	for(int i = 1; i <= N; i++) {
		if(!V1) h[0] = f[0]; else h[V1 - 1] = 0;
		for(int j = max(V1, 1); j <= V2; j++)
			h[j] = h[j - 1], upd(h[j], f[j]);
		for(int j = 0; j <= V2; j++)
			upd(g[(c[i] + j) / 2], (h[V2] - ((j <= V1) ? 0 : (h[j - 1])) + Mod) % Mod);
		V1 = c[i] / 2, V2 = (V2 + c[i]) / 2;
		for(int j = V1; j <= V2; j++) {
			f[j] = g[j];
			g[j] = 0;
		}
	}

	int sum = 0;
	for(int i = V1; i <= V2; i++) upd(sum, f[i]);
	cout << sum << '\n';
}

总结:

  • 实际上思路应该是从考虑判定变成考虑对合法序列的构造。构造的时候,因为合并是从小到大进位的,所以可以从小的位向大的位来考虑。
  • 这个值域分析???只能说写出来暴力 dp 后再修正吧,这也提醒我们,不要急于否定一个看上去很暴力的 dp,或者说一个 \(N = 2e5\) 但是二次的暴力!

ARC160D

tag:判定考虑构造。

看到这个 998244353 啪的一下我九点进来了很快啊然后我就不会了。

直接考虑判定?这也太难了,考虑构造。直接构造会产生重复,怎么办?首先倒转以下变成从全 \(0\) 序列加到 \(A\) 序列。钦定顺序,首先进行 2 操作,然后进行 1 操作。注意到如果同一个区间被加了超过 \(K\) 次不如用 \(1\) 操作,所以我们规定每个区间最多只会加 \(K\) 次。我们得到了这样一个构造策略,但是不知道一个构造策略和最终序列是否构成双射关系,证明证明:

  • 显然一个构造策略只会指向一个最终序列,我们证明最终序列到构造策略。
  • 对于 \(A_1\) 只会进行小于 \(K\) 次的二操作,也就是说 \(A_1\) 处进行的 \(2\) 操作应该唯一\(A_1 \bmod k\) 次。删除 \(A_1\),那么对于 \(A_2\) 也是同理,归纳法以此类推。

于是问题变成了:\(\sum\limits_{i = 1}^{2n - k + 1}x_i = \dfrac{M}{k}\),要求 \(\forall i \in [1, n - k + 1], b_i < k\)。这个经典的方程带上界,用容斥解,最终解数量为

\[\sum\limits_{t = 0}^{n -k+1}(-1)^t\binom{\frac{M}{k}+2n-(t+1)k}{2n - k}\binom{n - k + 1}{t} \]

总结:

  • 对于计数不能拘泥于“判定”,还可以考虑从构造出发,建立双射!
  • 建立双射可以从归纳法的角度考虑证明。
  • 这题还要很大胆的猜测。
  • 两道题都是不考虑判定,考虑构造?但是考虑构造也有不同的地方。
    • 这道题是先反方向将删除变成从 \(0\) 开始增加,然后直接表达出构造方案,证明是双射。
    • 上一道题则是按照顺序,划分出子问题,一步一步构造。
posted @ 2025-11-26 08:01  CatFromMars  阅读(2)  评论(0)    收藏  举报