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\) 中的边,剩下的随意的方案数。于是答案即为
在计算 \(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
亦是洛谷 P4689 [FJOI2016] 建筑师。
考虑序列中的最大值,它左侧和右侧分别有 \((f-1)\) 和 \((b-1)\) 个前后缀最大值。你可以在左侧和右侧划分出 \((f-1)\) 和 \((b-1)\) 个组,然后用组内最大的元素来代表它(也就是无论如何都把最大的元素扔前面)。你还能发现这些组内元素的最大值是单调递增的。
所以设左侧有 \(x\) 个元素实际上求的就是把 \(x\) 个元素划分进 \((f-1)\) 个互不区分的非空轮换的方案数。右边同理。
于是答案就是
但是这个式子太慢了。重新考虑组合意义。考虑最终一共划分出了 \((f+b-2)\) 个置换环,你会选择 \((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\) 列的矩阵,满足行、列之间两两互不相同的答案。转移的时候考虑整体减去不合法,有
其中 \({(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)\) 个等价类,这些等价类中的点的颜色都必须相同。

现在其实是,一个长度为 \(n\) 的环上,\(d\) 种颜色交替出现,要给每种颜色的点决定一个状态(染/不染),使所有这种颜色的点状态相同,要求总共出现 \(m\) 个被染色的点,且不存在任何连续 \(k\) 个位置都被染色。
由于不同颜色的点在环上交替出现,以上问题其实等价于:一个长度为 \(d\) 的小环,要选择 \(m\cdot (d/n)\) 个点染色,且不出现环上连续 \(k\) 个位置都被染色的不同方案数。注意这里小环中的点代表的是颜色,而颜色是有标号的,所以方案不同当且仅当每个颜色对应的状态都不同,即使循环同构我们也不认为是相同的。
记上述问题的答案为 \(f(d)\),则显然
其中 \(g(n, m)\) 是在大小为 \(n\) 的环上选 \(m\) 个点染色,要求不出现 \(k\) 个连续位置被染色的方案数。和上面类似,\(g(n, m)\) 的计数过程也是有标号的。
考虑把 \(m\) 个黑球插进 \((n-m)\) 个白球的环上的 \((n-m)\) 个空隙中,相当于 \(m\) 个球要放进 \((n-m)\) 的箱子(箱子可以为空)的方案数。
记 \(h(n, m)\) 表示 \(m\) 个没有区别的球放进 \(n\) 个有区别的箱子,要求每个箱子不超过 \(k\) 个球的方案数,则
有一个 \(\frac{n}{n-m}\) 的系数。这是因为,我们的计数是有标号的,我们依次钦定环上的 \(n\) 个点中每个点是白色,这样一来,一种给每两个白点之间塞黑点的方案就对应着,从它开始给后面的点标号的方案数。这样会得到 \(n\cdot h(n-m, m)\) 种方案。
例如,在计算 \(n=5, m=2, k = 2\) 时 \(g(n, m)\) 的时候:

(图中的点表示白点,边上的数表示放在两个白点之间的黑点)
上图是我们钦定 \(1\) 是白色的方案,共 \(6\) 种方案。同理我们还会钦定 \(2, 3, 4, 5\) 是白色,得到共 \(30\) 种方案。
但是这些方案会有重复统计,比如下面的三个方案被统计了三次。

容易发现每个方案被统计的次数就是 \((n-m)\),所以要除掉。
现在只需要考虑 \(h(n, m)\) 如何计算了。不难想到容斥,钦定一些箱子放了超过 \(k\) 个球,其他的任意,我们给这些箱子直接塞进 \((k+1)\) 个球,然后就又是一个没有限制的问题了。于是
现在你就可以算答案了。根据 Burnside 引理,答案是
注意有一个 \(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 周末晚会
类似上一题的分析,只是没有强制选 \(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;
}

浙公网安备 33010602011771号