26.3.23 NOIP 题单

AT_arc101_c [ARC101E] Ribbons on Tree

https://www.luogu.com.cn/problem/AT_arc101_c

你直接设 \(f_{i, j}\) 表示考虑完了 \(i\) 子树,子树内有 \(j\) 个点匹配到了 \(i\) 子树外的方案数。

转移形如 \(\displaystyle f_{u, i+j-2k} \gets k! {i\choose j}{j \choose k} f'_{u, i} \cdot f_{v, j}\)。直接做是 \(O(n^3)\) 的,由于系数太复杂了所以几乎没有优化空间。

考虑容斥,枚举一个边集 \(S\),记 \(F(S)\) 表示强制不能覆盖到 \(S\) 中的边,剩下的随意的方案数。于是答案即为

\[\sum_{S \subseteq E}{(-1)}^{|S|} F(S) \]

在计算 \(F(S)\) 的时候,将 \(S\) 中的边全部断开,得到的每个联通块的匹配方案是独立的。

而对于一个大小为 \(m\)\(m\) 必须为偶数)的联通块,联通块内点两两匹配的方案数是 \((m-1) \times(m-3)\times \cdots \times 1\)

于是考虑 \(f_{i, j}\) 表示考虑到 \(i\) 的子树,\(i\) 所在联通块大小为 \(j\),不考虑 \(i\) 所在联通块的贡献和。在断开一条边的时候统计这个联通块的贡献即可。

转移时的决策是,要么继承子树中的联通块,要么断开这条边并使联通块数的奇偶性改变。

复杂度是树上背包的 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 5005, mod = 1e9 + 7;

int n, x, y, f[N][N], tot = 1, fac[N], ifac[N];
int h[N], h2[N], siz[N], ans, vr[N];
vector<int> g[N];

inline int C(int n, int m)
{
	return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}

inline int ksm(int x, int y)
{
	int res = 1;
	while(y)
	{
		if(y & 1) res = res * x % mod;
		y >>= 1, x = x * x % mod;
	}
	return res;
}

inline void cadd(int &x, int y)
{
	x = (x + y) % mod;
}

void dfs(int u, int fa)
{
	siz[u] = 1, f[u][1] = 1;
	for(auto v : g[u])
	{
		if(v == fa) continue;
		dfs(v, u);
	}
	for(auto v : g[u])
	{
		if(v == fa) continue;
		for(int i = 1; i <= siz[u] + siz[v]; i++) h[i] = 0;
		for(int i = 1; i <= siz[u]; i++)
		for(int j = 1; j <= siz[v]; j++)
		{
			if(!(j & 1)) cadd(h[i], -f[u][i] * f[v][j] % mod * vr[j] % mod);
			cadd(h[i + j], f[u][i] * f[v][j] % mod);
		}
		for(int i = 1; i <= siz[u] + siz[v]; i++) f[u][i] = h[i];
		siz[u] += siz[v];	
	}
	return;
}

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n;
	fac[0] = 1;
	for(int i = 1; i <= n; i++)
		fac[i] = fac[i - 1] * i % mod;
	ifac[n] = ksm(fac[n], mod - 2);
	for(int i = n; i >= 1; i--)
		ifac[i - 1] = ifac[i] * i % mod;
	vr[2] = 1;
	for(int i = 3; i <= n; i += 2)
		vr[i + 1] = vr[i - 1] * i % mod;
	for(int i = 1; i < n; i++)
	{
		cin >> x >> y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	dfs(1, 0);
	for(int i = 2; i <= n; i += 2)
		cadd(ans, f[1][i] * vr[i] % mod);
	cout << (ans + mod) % mod;
	return 0;
}

HDU4372 Count the Buildings

https://next.tboj.cn/p/8391

亦是洛谷 P4689 [FJOI2016] 建筑师。

考虑序列中的最大值,它左侧和右侧分别有 \((f-1)\)\((b-1)\) 个前后缀最大值。你可以在左侧和右侧划分出 \((f-1)\)\((b-1)\) 个组,然后用组内最大的元素来代表它(也就是无论如何都把最大的元素扔前面)。你还能发现这些组内元素的最大值是单调递增的。

所以设左侧有 \(x\) 个元素实际上求的就是\(x\) 个元素划分进 \((f-1)\) 个互不区分的非空轮换的方案数。右边同理。

于是答案就是

\[\sum_{m=0}^{n-1} {n-1 \choose m} {m \brack f-1} {n-m-1 \brack b-1} \]

但是这个式子太慢了。重新考虑组合意义。考虑最终一共划分出了 \((f+b-2)\) 个置换环,你会选择 \((f-1)\) 个放到左边,剩下的放到右边。于是答案也是

\[{n \brack f+b-2} {f+b-2 \choose f-1} \]

上下两个式子竟然是等价的!

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 2005, mod = 1e9 + 7;

int stl[N][N], C[N][N], b, f, n;

void solve()
{
	cin >> n >> f >> b;
	cout << stl[n - 1][f + b - 2] * C[f + b - 2][f - 1] % mod << '\n';
	return;
}

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	for(int i = 0; i <= 2000; i++)
	{
		stl[i][0] = (i == 0);
		C[i][0] = 1;
		for(int j = 1; j <= i; j++)
		{
			stl[i][j] = (stl[i - 1][j - 1] + (i - 1) * stl[i - 1][j] % mod) % mod;
			C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
		}
	}
	int t; cin >> t;
	while(t--) solve();
	return 0;
}

TopCoder-13444 CountTables

你发现行和列之间的限制相关性太强了。于是想到先保证任意两行不相同,然后对列做。

\(f_i\) 表示 \(n\)\(i\) 列的矩阵,满足行、列之间两两互不相同的答案。转移的时候考虑整体减去不合法,有

\[f_i = {(c^i)}^{\underline n} - \sum_{j = 1}^{i-1} {i \brace j} f_j \]

其中 \({(c^i)}^{\underline n}\) 是保证任意两行不同的方案数,\(\displaystyle \sum_{j = 1}^{i-1} {i \brace j} f_j\) 是枚举有 \(j\) 种两两不同的列。

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 4005, mod = 1e9 + 7;

int n, m, c, ans, S[N][N], f[N];

inline void cadd(int &x, int y)
{
	x = (x + y) % mod;
}

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> c;
	for(int i = 0; i <= 4000; i++)
	{
		S[i][0] = (i == 0);
		for(int j = 1; j <= i; j++)
			S[i][j] = (S[i - 1][j - 1] + j * S[i - 1][j] % mod) % mod;
	}
	if(n > m) swap(n, m);
	for(int i = 1, tp = c; i <= m; i++, tp = tp * c % mod)
	{
		int rg = 1;
		for(int j = 0; j <= n - 1; j++)
			rg = rg * (tp - j) % mod;
		f[i] = rg;
		for(int j = 1; j < i; j++)
			cadd(f[i], -S[i][j] * f[j] % mod);
	}
	cout << (f[m] + mod) % mod;
	return 0;
}

AT_arc062_d [ARC062F] AtCoDeerくんとグラフ色塗り

https://www.luogu.com.cn/problem/AT_arc062_d

前置知识:群论、Polya 定理,可以看 这篇博客

首先求出图的所有点双分量。不难发现一条边无论如何也不可能移动到其他点双分量,所以每个点双分量内的情况是独立的。

我们将所有点双分量分为以下几类:

  • 对于只有两个点的点双分量,它只有一条边,这条边无法参与循环,所以贡献是 \(k\)
  • 对于只有一个环的点双分量(也就是点数与边数相同),它的染色方案数就是【模板】Pólya 定理中的染色方案数。
  • 对于有至少两个环的点双分量,手玩一下发现每两条边都可以随意交换。设这个点双中有 \(m'\) 条边,方案数是插板法的 \({m' + k - 1 \choose k - 1}\)

所有贡献全乘起来就行了。

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int mod = 1e9 + 7, B = sqrt(1e9) + 5, N = 205;

int n, ans, m, k, dfn[N], low[N], idx, C[N][N], x, y;
vector<int> primes, g[N];
bool notpr[B];

inline int ksm(int x, int y)
{
	int res = 1;
	while(y)
	{
		if(y & 1) res = res * x % mod;
		y >>= 1, x = x * x % mod;
	}
	return res;
}

inline void cadd(int &x, int y)
{
	x = (x + y) % mod;
}

inline int getphi(int x)
{
	int res = x;
	for(auto p : primes)
	{
		if(p > x) break;
		if(x % p) continue;
		while(x % p == 0) x /= p;
		res = res * (p - 1) % mod * ksm(p, mod - 2) % mod;
	}
	if(x > 1) res = res * (x - 1) % mod * ksm(x, mod - 2) % mod;
	return res;
}

int calc(int n)
{
	int ans = 0;
	for(int i = 1; i <= sqrt(n); i++)
	{
		if(n % i) continue;
		cadd(ans, ksm(k, i) * getphi(n / i) % mod);
		if(i == n / i) continue;
		cadd(ans, ksm(k, n / i) * getphi(i) % mod);
	}
	ans = ans * ksm(n, mod - 2) % mod;
	return ans;
}

stack<int> stk;

void dfs(int u, int fa)
{
	dfn[u] = low[u] = ++idx;
	stk.push(u);
	if(g[u].empty()) return;
	for(auto v : g[u])
	{
		if(!dfn[v])
		{
			dfs(v, u);
			low[u] = min(low[u], low[v]);
			if(low[v] >= dfn[u])
			{
				int siz = 0, h = -1;
				set<int> st;
				while(h != v && !stk.empty())
				{
					h = stk.top(); stk.pop();
					st.insert(h);
					siz++;
				}			
				siz++;
				st.insert(u);
				int cnt = 0;
				for(auto p : st)
				for(auto q : g[p])
					if(p < q && st.count(q)) cnt++;
				if(siz == 2) ans = ans * k % mod;
				else if(cnt == siz) ans = ans * calc(siz) % mod;
				else ans = ans * C[cnt + k - 1][k - 1] % mod;
			}
		}
		else if(v != fa) low[u] = min(low[u], dfn[v]);
	}
	return;
}

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	for(int i = 0; i <= 200; i++)
	for(int j = 0; j <= i; j++)
	{
		if(j == 0) C[i][j] = 1;
		else C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
	}
	for(int i = 2; i <= B - 4; i++)
	{
		if(!notpr[i]) primes.push_back(i);
		for(int j = 0; j < primes.size() && i * primes[j] <= B - 4; j++)
		{
			notpr[i * primes[j]] = 1;
			if(i % primes[j] == 0) break;
		}
	}
	cin >> n >> m >> k; ans = 1;
	for(int i = 1; i <= m; i++)
	{
		cin >> x >> y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) dfs(i, 0);
	cout << ans;
	return 0;
}

P4128 [SHOI2006] 有色图

https://www.luogu.com.cn/problem/P4128

这个题 一模一样,只是其中的两种颜色变成了 \(m\) 种。

P4916 [MtOI2018] 魔力环

https://www.luogu.com.cn/problem/P4916

一个长度为 \(n\) 的环,选择恰好 \(m\) 个位置染色,要求不存在连续的 \(k\) 个位置被染色的本质不同方案数。我们认为两种方案是本质不同的,当且仅当它们不循环同构。

依然考虑 Burnside 引理,对每个置换计算不动点数量。假设这个环被转了 \(i\) 次,那显然有 \(d = \gcd(i, n)\) 个等价类,这些等价类中的点的颜色都必须相同。

image

现在其实是,一个长度为 \(n\) 的环上,\(d\) 种颜色交替出现,要给每种颜色的点决定一个状态(染/不染),使所有这种颜色的点状态相同,要求总共出现 \(m\) 个被染色的点,且不存在任何连续 \(k\) 个位置都被染色。

由于不同颜色的点在环上交替出现,以上问题其实等价于:一个长度为 \(d\) 的小环,要选择 \(m\cdot (d/n)\) 个点染色,且不出现环上连续 \(k\) 个位置都被染色的不同方案数。注意这里小环中的点代表的是颜色,而颜色是有标号的,所以方案不同当且仅当每个颜色对应的状态都不同,即使循环同构我们也不认为是相同的。

记上述问题的答案为 \(f(d)\),则显然

\[f(d) = [n | md] g(d, m \cdot (d/n)) \]

其中 \(g(n, m)\) 是在大小为 \(n\) 的环上选 \(m\) 个点染色,要求不出现 \(k\) 个连续位置被染色的方案数。和上面类似,\(g(n, m)\) 的计数过程也是有标号的。

考虑把 \(m\) 个黑球插进 \((n-m)\) 个白球的环上的 \((n-m)\) 个空隙中,相当于 \(m\) 个球要放进 \((n-m)\) 的箱子(箱子可以为空)的方案数。

\(h(n, m)\) 表示 \(m\) 个没有区别的球放进 \(n\) 个有区别的箱子,要求每个箱子不超过 \(k\) 个球的方案数,则

\[g(n, m) = \frac{n \cdot h(n-m, m)}{n - m} \]

有一个 \(\frac{n}{n-m}\) 的系数。这是因为,我们的计数是有标号的,我们依次钦定环上的 \(n\) 个点中每个点是白色,这样一来,一种给每两个白点之间塞黑点的方案就对应着,从它开始给后面的点标号的方案数。这样会得到 \(n\cdot h(n-m, m)\) 种方案。

例如,在计算 \(n=5, m=2, k = 2\)\(g(n, m)\) 的时候:

image

(图中的点表示白点,边上的数表示放在两个白点之间的黑点)

上图是我们钦定 \(1\) 是白色的方案,共 \(6\) 种方案。同理我们还会钦定 \(2, 3, 4, 5\) 是白色,得到共 \(30\) 种方案。

但是这些方案会有重复统计,比如下面的三个方案被统计了三次。

image

容易发现每个方案被统计的次数就是 \((n-m)\),所以要除掉。

现在只需要考虑 \(h(n, m)\) 如何计算了。不难想到容斥,钦定一些箱子放了超过 \(k\) 个球,其他的任意,我们给这些箱子直接塞进 \((k+1)\) 个球,然后就又是一个没有限制的问题了。于是

\[h(n, m) = \sum_{i = 0}^{\min\left(\frac{m}{k+1}, n\right)} (-1)^i {n \choose i} {m-i(k+1) + n - 1 \choose n - 1} \]

现在你就可以算答案了。根据 Burnside 引理,答案是

\[\frac{1}{n}\sum_{i = 1}^n f(\gcd(i,n)) \]

注意有一个 \(n= m\) 的 corner,答案是 \([m \le k]\),需要判一下。

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int mod = 998244353, N = 2e5 + 5;

int n, m, k, ans, fac[N], ifac[N];

inline int C(int n, int m)
{
	return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}

inline int ksm(int x, int y)
{
	int res = 1;
	while(y)
	{
		if(y & 1) res = res * x % mod;
		y >>= 1, x = x * x % mod;
	}
	return res;
}

inline void cadd(int &x, int y)
{
	x = (x + y) % mod;
}

int h(int n, int m)
{
	int res = 0;
	for(int i = 0; i <= min(m / (k + 1), n); i++)
		cadd(res, C(n, i) * C(m - i * (k + 1) + n - 1, n - 1) % mod * ((i & 1) ? -1ll : 1ll));
	return res;
}

int g(int n, int m)
{
	return n * ksm(n - m, mod - 2) % mod * h(n - m, m) % mod;
}

inline int f(int c)
{
	if(m * c % n) return 0;
	return g(c, m * c / n);
}

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> k; fac[0] = 1;
	if(n == m) {cout << (m <= k); return 0;}
	for(int i = 1; i <= N - 5; i++)
		fac[i] = fac[i - 1] * i % mod;
	ifac[N - 5] = ksm(fac[N - 5], mod - 2);
	for(int i = N - 5; i >= 1; i--)
		ifac[i - 1] = ifac[i] * i % mod;
	for(int i = 1; i <= n; i++)
		cadd(ans, f(__gcd(i, n)));
	cout << (ksm(n, mod - 2) * ans % mod + mod) % mod;
	return 0;
}

这样直接做,复杂度可能是单老哥?实际上好像比单老哥快很多,甚至可能就是 \(O(n)\) 级别的。

不太清楚,但甚至还可以用莫反继续优化,优化之后连枚举 \(i\) 都不需要了,瓶颈完全变成了预处理阶乘和逆元。

不过这些都是锦上添花,和这题的核心思路无关了。

BZOJ1547 周末晚会

https://next.tboj.cn/p/8397

类似上一题的分析,只是没有强制选 \(m\) 个的限制了。

\(g(n)\) 的定义可以改为:一个长度为 \(d\) 的小环,要选择 \(m < d\) 个点染色,且不出现环上连续 \(k\) 个位置都被染色的不同方案数。然后还是随便做。

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int mod = 1e8 + 7, N = 2e5 + 5;

int n, m, k, ans, fac[N], ifac[N], inv[N], fs[2005][2005];

inline int C(int n, int m)
{
	return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}

inline int ksm(int x, int y)
{
	int res = 1;
	while(y)
	{
		if(y & 1) res = res * x % mod;
		y >>= 1, x = x * x % mod;
	}
	return res;
}

inline void cadd(int &x, int y)
{
	x = (x + y) % mod;
}

int h(int n, int m)
{
	int res = 0;
	for(int i = 0; i <= min(m / (k + 1), n); i++)
		cadd(res, C(n, i) * C(m - i * (k + 1) + n - 1, n - 1) % mod * ((i & 1) ? -1ll : 1ll));
	return res;
}

int g(int n, int m)
{
	return n * inv[n - m] % mod * h(n - m, m) % mod;
}

inline int f(int c, int m)
{
	int res = 0;
	if(m < c) cadd(res, g(c, m));
	return res;
}

int calc(int n)
{
	
	int res = 0;
	for(int i = 1; i <= n; i++)
	{
		int d = __gcd(i, n);
		for(int m = 0; m < d; m++)
			cadd(res, f(d, m));
	}
	return res;
}

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	fac[0] = 1;
	for(int i = 1; i <= N - 5; i++)
		fac[i] = fac[i - 1] * i % mod;
	ifac[N - 5] = ksm(fac[N - 5], mod - 2);
	for(int i = N - 5; i >= 1; i--)
		ifac[i - 1] = ifac[i] * i % mod;
	inv[0] = 1;
	for(int i = 1; i <= N - 5; i++)
		inv[i] = ifac[i] * fac[i - 1] % mod;
	int t; cin >> t;
	while(t--)
	{
		cin >> n >> k; ans = calc(n) * ksm(n, mod - 2) % mod;
		if(n <= k) cadd(ans, 1);
		cout << (ans % mod + mod) % mod << '\n';
	}
	return 0;
}
posted @ 2026-03-26 09:21  心灵震荡  阅读(25)  评论(0)    收藏  举报