专栏文章
20251118【NOIP】模拟 C. 修建道路
个人记录参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @min4mo3h
- 此快照首次捕获于
- 2025/12/01 20:29 3 个月前
- 此快照最后确认于
- 2025/12/01 20:29 3 个月前
20251118【NOIP】模拟 C. 修建道路
简化题意:
一棵树,任意加边,使新图恰有 个边双。对合法方案计数。
关键观察:
- 将所有割边断开,会形成恰好 个连通分量。
- 所有的割边都是原来的树边。
考虑结论 2 的证明:
- 考虑新加入的边,一定不会有新割边产生,因为两个端点一定在原树上是连通的。
所以可以转化,新问题为:
一棵树,选 个边作为割边,剩下的连通块加边使得连通块内不存在割边。对合法方案计数。
这个问题看起来还是很困难,因为我们无法对连通块内的合法方案数计数,所以考虑二项式反演。
二项式反演:
我有 个限制,为 。
设 为恰好违反 个限制的方案数, 为至少违反 个限制的方案数 。
有关系 ,即在 中选 个来满足。
回到原问题,我们使用二项式反演,将原问题转化为:
- 一棵树,选 个边作为割边,在剩下连通块里任意加边的方案数。
同时我们知道在一个大小为 的树内任意加边多的方案数是 ,含义就是总的边数是 ,减去原来的树上的边,这些边任意。
所以如果我们知道了那些边被割掉,形成了一些大小为 的连通块,答案就是
所以可以树形 dp。
设 表示现在在以 为根的子树内,子树大小为 ,钦定割掉 条边的方案数。
转移,考虑将 的儿子 转移到自己, 子树内和 子树内的状态都被确定了,所以只用考虑 到 这条边是否割:
这是把当前这条边割去了,此时以 为根的连通块的大小就确定了,所以就可以统计贡献。
此时是不割,继续向上递归。
这个复杂度是 的(子树大小固定,第二维不用枚举),所以要优化。
考虑多项式,首先设 ,转移改写形式为
但复杂度并没有变化,注意到这个复杂度瓶颈在于遍历数组中的每一个数。,所以使用另一个公式:拉格朗日插值
根据这个,我们知道了只用 个点值就可以表示出来原来的 次多项式,所以我们将每一次的 dp 变成点值,即一个 ,这样一次转移只用,枚举前两维,所以是 的,做 次是 的。
然后就是板子,使用 的高斯消元或 的拉插还原多项式,在套一个二项式反演的板子。
代码(赵队的):
CPP#include<bits/stdc++.h>
#define int long long
using namespace std;
int get() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)) { if(c == '-') f = -1; c = getchar(); }
while(isdigit(c)) { x = x * 10 + c - '0'; c = getchar(); }
return x * f;
}
const int N = 705, P = 1e9 + 7, sgn[] = { 1ll, P - 1 };
int n, fac[N], inv[N], ans[N];
struct Edge { int v, nxt; } edge[N << 1];
int head[N], tot;
int f[N][N], sze[N], a[N][N], g[N], tmp[N], x, C[N][N];
int qpow(int x, int y) {
int res = 1;
while(y) res = res * ((y & 1)? x : 1) % P, x = x * x % P, y >>= 1;
return res;
}
void add(int u, int v) { edge[++tot] = (Edge){ v, head[u] }, head[u] = tot; }
void inc(int &x, int y) { x = (x + y) % P; }
void dec(int &x, int y) { x = ((x - y) % P + P) % P; }
int S(int n) { return qpow(2, n * (n - 1) / 2 - (n - 1)); }
void init(int n) {
fac[0] = 1;
for(int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % P;
inv[n] = qpow(fac[n], P - 2);
for(int i = n; i >= 1; i--) inv[i - 1] = inv[i] * i % P;
for(int i = 1; i <= n; i++) g[i] = S(i);
for(int i = 0; i <= n; i++) {
C[i][0] = 1;
for(int j = 1; j <= i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
}
}
void dfs(int u, int lst) {
f[u][1] = 1, sze[u] = 1;
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].v;
if(v == lst) continue;
dfs(v, u);
for(int a = 1; a <= sze[u]; a++)
for(int b = 1; b <= sze[v]; b++) {
// 不连接
inc(tmp[a], f[u][a] * f[v][b] % P * g[b] % P * x);
// 连接
inc(tmp[a + b], f[u][a] * f[v][b]);
}
for(int i = 1; i <= sze[u] + sze[v]; i++) f[u][i] = tmp[i], tmp[i] = 0;
sze[u] += sze[v];
}
}
void Gauss_Jordan(int n) {
for(int i = 1; i <= n; i++) {
int mp = i;
for(int j = i + 1; j <= n; j++) if(a[j][i] > a[mp][i]) mp = j;
for(int j = 1; j <= n + 1; j++) swap(a[i][j], a[mp][j]);
int t = qpow(a[i][i], P - 2);
for(int j = 1; j <= n + 1; j++) a[i][j] = a[i][j] * t % P;
for(int j = 1; j <= n; j++) {
if(j == i || !a[j][i]) continue;
int t = a[j][i];
for(int k = 1; k <= n + 1; k++) dec(a[j][k], a[i][k] * t);
}
}
}
main() {
n = get();
for(int i = 1, u, v; i < n; i++)
u = get(), v = get(), add(u, v), add(v, u);
init(n);
for(int i = 1; i <= n + 1; i++) {
memset(f, 0, sizeof(f)), x = i;
dfs(1, 0);
for(int j = 1; j <= n; j++) inc(a[i][n + 2], f[1][j] * g[j] % P * x);
for(int j = 1; j <= n + 1; j++) a[i][j] = qpow(x, j - 1);
}
Gauss_Jordan(n + 1);
for(int i = 1; i <= n; i++) ans[i] = a[i + 1][n + 2];
for(int i = 1; i <= n; i++)
for(int j = i + 1; j <= n; j++) inc(ans[i], sgn[(j - i) & 1] * ans[j] % P * C[j - 1][i - 1]);
for(int i = 1; i <= n; i++) cout << ans[i] << " ";
return 0;
}
/*
3
1 2
1 3
*/
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...