计数(2)

杂题

考虑到我真的太菜了,我无法对这些技巧进行一些有效的总结。很多时候我也不知道为什么我想不到,真的太糟糕了。所以我在这里先记录题目,之后再单开一篇来深入分析一下里面运用到了哪些司想

nfls18382 双子

给定 \(n\) 段颜色,每段颜色均相等,颜色为 \(1, 0\)\(-1\) 代表颜色既可以是 \(1\) 也可以是 \(0\),每段颜色中 \(a_i\) 个点,一共有 \(\sum a_i\) 个点。可以从前往后连边,如果 DAG 中有奇数条路径满足这些路径上 \(1, 0\) 交错出现,这个 DAG 合法。问合法 DAG 数量。

让我来反思一下我为什么场上没做出来,因为我当时看到这个连边就懵了,小脑开始萎缩,大脑停止思考,这一个图怎么计数。

众所周知,计数题我们总是要规定一个顺序,通常的做法是规定一个顺序,划分子问题或者变成增量。这里我们显然会采用增量的形式。你可能会说,变成增量难道就不是划分子问题了吗,其实也是的

于是我们考虑增量,增量下考虑如何快速判定一张图合法 DAG 数量。顺序是显然的,从 \(1\sim n\) 考虑加点,决策的实际意义是显然的,考虑 \(i\) 的连边。那么此时的状态其实也很明显,我们需要记录前缀中有多少 \(1, 0\) 结尾的路径数为奇数/偶数,然后可以设 \(f[i, j, k, l]\)\(j\) 为 0 奇,\(k\) 为 0 偶,\(l\) 为 1 奇,\(d = i - j - k - l\) 为 1 偶。这一切都是显然的,但是考场上我没有想到。我觉得这主要是我看到了这是一张图的计数就开始蒙圈的原因,没有深入思考。我总是觉得这是乱的,实际上这样一张 DAG 我们完全可以考虑增量的构造。对于任何一个计数结构也是这样,找到序,增量(划分子问题)。

转移的时候 syc 大蛇用的是同段之间枚举,但是我个人认为一个一个增量加入是最容易的。得到优化也会更自然,毕竟加入单点是一个相当容易的过程,可以搞得花活也更多。这也提醒我们增量可以考虑不同的增量形式。

然后我们优化,注意到我们转移的系数仅仅和 \([j>0], [k > 0]\) 有关,最后统计答案只和 \((j + l)\bmod 2\) 有关,然后就可以做到 \(O(n)\) 了。这很容易。然后我们发现虽然系数是 \(2^{i - [j> 0]}\),但是如果我们给它集体除以 \(2^{i - 1}\) 那么系数就会变成 \(2, 1\),这就是一个矩阵加速的形式了。然后就做完了。

我必须承认做不出来这题是我计数水平的严重欠缺,我必须承认这点,我必须承认。

#include <bits/stdc++.h>
#define ll long long
#define i128 __int128
#define il inline
using namespace std;
const int N = 1e5;
const int Mod = 1e9 + 7;
const int L = 32;
il int lowbit(int x) {
	return (x & (-x));
}
il int qpow(int n, i128 m) {
	int res = 1;
	while(m) {
		if(m & 1) res = 1ll * res * 1ll * n % Mod;
		n = 1ll * n * 1ll * n % Mod;
		m >>= 1;
	}
	return res;
}
il void upd(ll &x, ll y) {
	x = (((x + y) >= Mod) ? (x + y - Mod) : (x + y));
}
int n;
int c[N + 10];
ll a[N + 10];
struct mat {
	ll mat[8][8];
	void init() {
		memset(mat, 0, sizeof(mat));
		for(int i = 0; i < 8; i++)
			mat[i][i] = 1;
	}
	void reinit0() {
		memset(mat, 0, sizeof(mat)); 
		mat[5][0] = 2;
		mat[4][1] = 2;
		mat[2][2] = 1;
		mat[7][2] = 1;
		mat[3][3] = 1;
		mat[6][3] = 1;
		mat[5][4] = 2;
		mat[4][5] = 2;
		mat[6][6] = 1;
		mat[7][6] = 1;
		mat[7][7] = 1;
		mat[6][7] = 1;
	}
	void reinit1() {
		memset(mat, 0, sizeof(mat));
		mat[3][0] = 2;
		mat[2][1] = 2;
		mat[3][2] = 2;
		mat[2][3] = 2;
		mat[7][4] = 1;
		mat[4][4] = 1;
		mat[6][5] = 1;
		mat[5][5] = 1;
		mat[7][6] = 1;
		mat[6][6] = 1;
		mat[6][7] = 1;
		mat[7][7] = 1;
	}
};
struct vect {
	ll f[8];
	void init() {
		memset(f, 0, sizeof(f));
	}
};

mat operator * (const mat &A, const mat &B) {
	mat t;
	memset(t.mat, 0, sizeof(t.mat));
	for(int k = 0; k < 8; k++) {
		for(int i = 0; i < 8; i++) {
			for(int j = 0; j < 8; j++)
				upd(t.mat[i][j], 1ll * A.mat[i][k] * 1ll * B.mat[k][j] % Mod);
		}
	}
	return t;
}
vect operator * (const mat &A, const vect &F) {
	vect t;
	t.init();
	for(int i = 0; i < 8; i++) 
		for(int j = 0; j < 8; j++)
			upd(t.f[i], 1ll * A.mat[i][j] * 1ll * F.f[j] % Mod);
	return t;
} 


mat ca[2][L + 3];
il void matmul(int opt, ll c, vect &org) {
	while(c) {
		ll lt = lowbit(c);
		org = ca[opt][__lg(lt)] * org;
		c -= lt;
	}
}
int main() {
	 freopen("gemini.in", "r", stdin);
	 freopen("gemini.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	int sum = 0;
	for(int i = 1; i <= n; i++)
		cin >> c[i] >> a[i];

	ca[0][0].reinit0(), ca[1][0].reinit1();
	for(int i = 1; i <= L; i++)
		ca[0][i] = ca[0][i - 1] * ca[0][i - 1],
		ca[1][i] = ca[1][i - 1] * ca[1][i - 1];

	vect f;
	f.init();
	f.f[0] = 1;
	const int inv2 = qpow(2, Mod - 2);
	ll r = 0;
	for(int i = 0; i < n; i++) {
		i128 d = qpow(2, (i128) (r + r + a[i + 1] - 1) * (i128) a[i + 1] / (i128)2);

		if(c[i + 1] != -1) {
			vect tp = f;
			matmul(c[i + 1], a[i + 1], tp);
			d = 1ll * d * 1ll * qpow(inv2, a[i + 1]) % Mod;
			for(int j = 0; j < 8; j++)
				tp.f[j] = 1ll * d * 1ll * tp.f[j] % Mod;
			f = tp;
			r += a[i + 1];
		}
		else {
			vect tp0 = f, tp1 = f;
			matmul(0, a[i + 1], tp0);
			matmul(1, a[i + 1], tp1);
			d = 1ll * d * 1ll * qpow(inv2, a[i + 1]) % Mod;
			for(int j = 0; j < 8; j++)
				tp1.f[j] = 1ll * d * 1ll * (tp0.f[j] + tp1.f[j]) % Mod,
			f = tp1;
			r += a[i + 1];
		}
	}
	cout << (f.f[1] + f.f[3] + f.f[5] + f.f[7]) % Mod << endl;
}

60pts 的代码也贴上来 dp 的部分。

void explore(int o, int i, int j, int k, int l) {
	int d = i - j - k - l;
	if(!o) {
		if(col[i + 1] == 0) {
			upd(f[i + 1][1][k][l ^ 1], qpow(2, i - (k > 0)) * f[i][j][k][l] % Mod);
			if(k) upd(f[i + 1][j][k][l], qpow(2, i - (k > 0)) * f[i][j][k][l] % Mod);
		}
		else {
			upd(f[i + 1][j][1][l ^ 1], qpow(2, i - (j > 0)) * f[i][j][k][l] % Mod);
			if(j) upd(f[i + 1][j][k][l], qpow(2, i - (j > 0)) * f[i][j][k][l] % Mod);
		}
	}
	else {
		if(col[i + 1] == 0) {
			upd(g[i + 1][1][k][l ^ 1], qpow(2, i - (k > 0)) * g[i][j][k][l] % Mod);
			if(k) upd(g[i + 1][j][k][l], qpow(2, i - (k > 0)) * g[i][j][k][l] % Mod);
		}
		else {
			upd(g[i + 1][j][1][l ^ 1], qpow(2, i - (j > 0)) * g[i][j][k][l] % Mod);
			if(j) upd(g[i + 1][j][k][l], qpow(2, i - (j > 0)) * g[i][j][k][l] % Mod);
		}	
	}
}

增量考虑一个点方程就很容易,考虑一堆点就会很困难 QAQ

BZOJ 5473

有一个n个点,m个边的仙人掌。所谓仙人掌,就是任何一个点至多属于一个环。
每个边有1/2的概率被删掉。问期望剩下多少个边联通块。
所谓边联通块,就是问剩下的边,构成多少个联通块,单独一个点不算做联通块。
输出答案乘以2^m之后mod1000000007的结果。

无法场切,我承认这是我的问题。

我场上的想法是:我直接数数数,我直接狂暴数。什么?你说环!反手断环为链,我直接 dp,这 tm 怎么 dp,这也太难了,md,我不写了

这的确是我的问题。

首先思考一下单独一个点不算做联通块,这简单数一数就可以把这个问题扣掉。直接对于每个点删除所有相邻边数一下就行了,所以这个边联通块是在骗哥们。

然后我们考虑从简单情况入手,比如树,树可以从链和菊花入手,解决完树我们解决基环树,在解决仙人掌。毕竟这题直接 dp 太难了,我觉得这是相当好的思考过程。

树:删除 \(x\) 条边那么还剩下 \((x+1)\) 个联通块,于是有

\[S = \sum\limits_{i = 0}^{n - 1}{{n - 1}\choose x}(x+ 1)\\ = 2^{n - 1} + \sum\limits_{i = 0}^{n - 1}{{n - 1\choose x}}x \]

笑话来了,我不会化简这个二项式。

我拷打我自己,记不记得 \(\dfrac{k}{n}{n\choose k} = {{n - 1}\choose {k - 1}}\)?????你是真的弱智

\(S = 2^{n - 1} + (n-1)\sum\limits_{i = 0}^{n - 1}{{n -2}\choose i} = 2^{n - 1} + (n - 1)2^{n - 2}\)

考虑环带来的影响。我们本来是删掉一条边,联通块数 \(+1\)。环上只删除一条边是不会产生影响的,我们要把这部分“删除第一条边”所增加的联通块数量给减掉。但是你发现这是错误的,因为等到删除 \((n - 1)\) 条边之后联通块数量就不再增加了。

可以考虑增加边,先增加到生成树的情况。如果一条边不连那么 \(2^mn\) 种,每次连一条边都会使得一个联通块消失,从而变成一个联通块,这一部分是 \(-m2^{m - 1}\)。我们发现连接最后一条环边的时候不会产生联通块的消失,这一部分要加回来,所以对于每个环要加上 \(2^{m - c}\)。最后减去边联通块数量。


超牛做法:考虑加边的概率。删除树边那么 \(+1\) 联通块数量,有 \((n - 1)\) 跳边可以删每次删除的概率是 \(\dfrac{1}{2}\),期望数 \(1 + \dfrac{n - 1}{2}\)。考虑环的概率,删掉第一条边没事情发生,删掉别的可以。那么减去 \(1 - \dfrac{1}{2^{len}}\)。相当于减去完全没选这个环上的,全选了别的的情况。

似乎掌握的还不够啊。


nfls13099

给定 \(m\) 个大小为 \(n\) 的排列,对于排列 \(A\) 每次可以操作交换一个逆序对。对于每个排列计算 \(m\) 个排列中有多少 \(A\) 可达 \(B\)\(n\le 9, m \le 3e5\)

我去,\(n!\) 居然这么小,我去。资本你赢了。

诚然可以直接 \(O(n^2n!)\) 建图,问题转化为图上点可达性统计,经典超难 \(O(\dfrac{nm}{w})\),只能跑 \(n = 8\),资本你赢了。

不要什么都资本你赢了歪!

于是考虑如何较为好的完成这件事情。这题提供了思路不难想到可以转化为图上路径数量统计(或者说,考虑规范建图的流程,删除某些不要的边?)这的确是一个巧妙地转化。

我们规范一下 \(A\) 走到 \(B\),保证 \(A\) 只有一个操作序列能到 \(B\),这样就可以了。首先考虑让 \(A_1 = B_1\),我们会将 \(A\)\(B_1\) 的位置直接和 \(A_1\) 交换(如果是逆序对的话)。递归下去就变成一个子问题了。如此一来从 \(A\)\(B\) 有且仅有一种合法的操作。

你发现这是错误的,举个例子,这样无法从 \(3,4,2,1\) 走到 \(2,1,4,3\)。如果直接按照上面的方法那么就是 \(3,4,2,1\to 2,4,3,1\to2,1,3,4\),此时就 gg 了。但是 \(3,4,2,1\to 2,4,3,1\to2,4,1,3\to2,1,4,3\) 即可。为什么会产生这样的事情?因为直接交换会对逆序对数量产生很大影响。所以我们考虑一个一个交换,这样的效果出的效果是将比 \(x\) 大的前面一串数给循环往右位移了一位,最小化了逆序对的损失,这是好的。但又没有那么好,因为这样可能会错失一点逆序对。你发现上面就是一个例子。

这咋搞。我们希望我们的一次操作只会对当前这个逆序对产生影响,或者说对逆序对数量产生尽量小的影响。所以我们要交换它的前驱。

于是我们设 \(f[i,perm]\) 为在排列 \(perm\) 时已经确定了 \([1, i]\) 的内容的方案数。转移考虑 \((i + 1)\) 可以被哪些点替换,然后计算下一个状态刷表。即可。

我写了好久,因为如你所见我一开始想假了好多次。我生气了。

真的很巧妙,更多的思考明天总结。

CF1556F

给定一张完全无向图,对于每条边 \((i, j)\)\(p_w\) 的概率从 \(i\to j\),有 \((1 - p_w)\) 的概率从 \(j\to i\),求问缩点后入度为 0 的 scc 的期望大小和。\(n\le 14\)

首先这个“期望大小和”显然是一个很难做的东西。毕竟你很在状态里面跑 tarjan。其实原题面的描述更容易想到这个转化,那就是计算每个点“优秀”的概率和。所以你发现这个期望还是骗哥们的。

翻译过来我们要解决的问题就是对于这样的图 \(x\) 可以到达任意一个的概率。发现 \(f[i, S]\) 好难转移,可以设 \(f[i, S_1, S_2]\) 为目前已经对 \(S_1\) 点内构造了完全图,\(i\) 只能到达 \(S_2\) 内的点,这个状态数 \(O(n3^n)\),能够承受。然后考虑 \(S_1\) 内的所有点到 \(y\) 的连边情况,随便维护一下就做完了,读者自证不难 blablabla

不难个毛线歪!

\(S_2\) 扩展了 \(S_3(S_3\sub (S_1 - S_2))\),那么一定是 \(x\to y\to S_3\),同时 \((S_1 - S_2 - S_3)\) 内都 \(\to y\)。计算一下这两个概率乘一块。枚举两个子集,会爆炸。所以什么叫做完了歪


这个做法可以说是没有什么优化余地了。我觉得想到这个做法然后推倒,回到 \(f[i, S]\),代表 \(i\) 赢了 \(S\) 内所有人,这样一个状态,这是十分有魄力的。我应该就会在这一步被卡死。毕竟上面的做法完全能跑。不过毕竟 \(S_2\) 这个状态确实很多余。

我们逆流而上,回到过去,\(f[x, S]\) 为啥不容易转移。首先考虑 \(x\) 能直接到达的点,然后 \(x\) 无法到达的点肯定都要由 \(x\) 能直接到达的点转移而来。由于一个点既可以被直接到也可以被简洁到所以计数会产生重复。这是问题所在,于是不难想到解决方法:正难则反。一个点无法到那么就是既不能直接到也不能间接到,这部分贡献直接算即可。可以做到 \(O(3^nn)\)。不过我没这么写。


agc049_e

沃趣,这也太牛了。

先考虑最优化。四种操作一起维护太困难了,只考虑单点加减是简单的,只考虑区间加减则是经典老番中的结论:差分数组中正数的和。合起来可以 dp,\(f[i, j]\) 代表第 \(i\) 个数变成 \(j\) 的最小操作次数,那么有 \(f[i, j] = |a_i - j| + \min\limits\{f[i - 1, k] + m\max(j - k, 0)\}\)。这是一个 \(O(nV^2)\) 的 dp。这个最优化放在原题中都跑不过,计数肯定更困难,而且似乎也没什么优化空间。除了 slope tirck 还可以这样考虑:如果两个 \(B\) 的增减趋势差不多,那么它答案在方案上的选择应当也是差不多的。我们考虑定量的感受这股劲,画成一个直方图,每一层之间有很多一样的,不同层一共只有 \(O(n)\) 个。因为每次操作最多只能减小 \(1\),我们可以看作每一层每一层的操作是完全独立的,由此可以得到这些区间内的操作完全一致。于是我们可以对每一层的 \(01\) 序列进行如上的 dp,时间复杂度锐减到 \(O(n^2)\),大胜利了。

如果 \(j\) 取值 \(0/1\),为了方便我们化简上面的式子,\(f[i, 0] = a_i + \min\{f[i - 1, 0], f[i - 1, 1]\}, f[i, 1] = 1 - a_i + \min\{f[i - 1, 0] + m, f[i - 1, 1]\}\)

用 csy 大神教的方法,因为这中间完全可以压缩只剩下两个值 \(f[i, 0],f[i, 1]\),都设入状态进行 dp 就行了。这样复杂度 \(O(n^4c)\),不是很好。但是观察到 \(|f[i, 0] - f[i, 1]| = d \le m+1\),也就是说实际上有用的状态可以合并到 \(O(nm)\) 里面去。因为最后统计的时候我们只关心 \(\min\{f_{i, 0}, f_{i, 1}}\) 与方案数的乘积。具体怎么合并?\(g[i, k]\) 代表 \(f\) 转移到 \(i\)\(f[i, 0] - f[i, 1] = k\) 的方案数,\(h[i, k]\) 代表此时 \(f[i, 0]\) 的总和。讨论一下即可。时间复杂度 \(O(n^2cm)\)

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 50, M = 3500;
const ll Mod = 1e9 + 7;
ll f[N + 3][N * 2 + 10], g[N + 3][N * 2 + 10];
int n, c, m, val[N * M + 3], a[N + 3][M + 3], tot = 0;
void upd(ll &x, ll y) {
	x = (((x + y) >= Mod) ? (x + y - Mod) : (x + y));
}
int post[N + 10];
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> c;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= c; j++)
			cin >> a[i][j],
			val[++tot] = a[i][j];
	}
	sort(val + 1, val + tot + 1);
	int len = unique(val + 1, val + tot + 1) - val - 1;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= c; j++)
			a[i][j] = lower_bound(val + 1, val + len + 1, a[i][j]) - val;
		sort(a[i] + 1, a[i] + c + 1);
		post[i] = 0;
	}
	
	ll ans = 0;
	int rdl = m + 1;
	for(int t = 1; t <= len; t++) {
		ll w = val[t] - val[t - 1];
		// cout << t << ' ' << len << ' ' << (t <= len) << endl;
		for(int i = 0; i <= n; i++)
			for(int j = 0; j <= rdl + 5; j++)
				f[i][j] = g[i][j] = 0;
		f[0][0] = 1, g[0][0] = 0;
		for(int i = 1; i <= n; i++) {
			int x = 0, y = 0;
			while(post[i] <= c && a[i][post[i]] < t) post[i]++;
			post[i]--, x = post[i];
			y = c - x;
			if(i == 1) {
				upd(f[i][-m - 1 + rdl], x);
				upd(g[i][-m - 1 + rdl], 0);
				upd(f[i][-m + 1 + rdl], y);
				upd(g[i][-m + 1 + rdl], y);
				continue;
			}
			for(int d = 0; d <= rdl + 5; d++) {
				if((d - rdl) >= 0) {
					upd(f[i][2 * 0 - 1 + rdl], x * f[i - 1][d] % Mod);
					upd(g[i][2 * 0 - 1 + rdl], x * (g[i - 1][d] - f[i - 1][d] * (d - rdl + Mod) % Mod + Mod) % Mod);
					upd(f[i][2 * 1 - 1 + rdl], y * f[i - 1][d] % Mod);
					upd(g[i][2 * 1 - 1 + rdl], y * (g[i - 1][d] - f[i - 1][d] * (d - rdl + Mod - 1) % Mod + Mod) % Mod);
				}
				else if((d - rdl) < 0 && (d - rdl) >= - m) {
					upd(f[i][2 * 0 - 1 + d], x * f[i - 1][d] % Mod);
					upd(g[i][2 * 0 - 1 + d], x * g[i - 1][d] % Mod);
					upd(f[i][2 * 1 - 1 + d], y * f[i - 1][d] % Mod);
					upd(g[i][2 * 1 - 1 + d], y * (g[i - 1][d] + f[i - 1][d]) % Mod);
				}
				else {
					upd(f[i][2 * 0 - 1 - m + rdl], x * f[i - 1][d] % Mod);
					upd(g[i][2 * 0 - 1 - m + rdl], x * g[i - 1][d] % Mod);
					upd(f[i][2 * 1 - 1 - m + rdl], y * f[i - 1][d] % Mod);
					upd(g[i][2 * 1 - 1 - m + rdl], y * (g[i - 1][d] + f[i - 1][d]) % Mod);
				}
			}
		}
		for(int d = 0; d <= rdl + 5; d++) {
			if((d - rdl) >= 0) upd(ans, w * (g[n][d] - (d - rdl) * f[n][d] + Mod) % Mod);
			else upd(ans, w * g[n][d] % Mod);
		}
	}
	cout << ans << endl;
}
posted @ 2025-07-09 23:35  CatFromMars  阅读(24)  评论(1)    收藏  举报