0%

72709102 AGC005

~K Perm Counting

如果一个排列 \(P\) 满足对于所有的i都有 \(|p_i-i|\ne k\),则称排列P为合法的,求合法排列数

考虑错排问题就是 \(k\ = 0\) 的特殊情况,这道题用同样做法来做,设 \(f_i\) 为恰好有 \(i\) 个满足 \(|p_i-i|\ne k\) 的方案数,\(g_i\) 为至少有 \(i\) 个满足 \(|p_i-i|\ne k\) 的方案数,则有

\[ g_i\ =\ \sum_{i\le j}f_j \] 根据广义容斥

\[ f_i\ =\ \sum_{i\le j}(-1)^{j-i}g_j \]

考虑如何求出 \(g_i\),我们利用下标和键值的关系不难看出这是一个二分图匹配问题,那么问题就转化为在一个二分图中选出 \(i\) 条匹配边的方案数,注意到每个点的度数不超过 \(2\),我们可以把这些链抽出来并在一起,不妨设 \(h_{i,j,k}\) 表示前 \(i\) 个点选出 \(j\) 条匹配边,第 \(i\) 个点与前一个点连边状态为 \(k\) 时的的方案数,转移很简单,不多赘述,最后答案即为 \(g_i\ =\ (h_{2n,i,0}+h_{2n,i,1})(n-i)!\)

关于解决二分图匹配问题很具有启发性,说不定下次会碰见一道题满足树的特殊性质的二分图匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
using namespace std;
inline int read(int f = 1, int x = 0, char ch = ' ')
{
while(!isdigit(ch = getchar())) if(ch == '-') f = -1;
while(isdigit(ch)) x = x*10+ch-'0', ch = getchar();
return f*x;
}
const int N = 4e3+5, P = 924844033;
int n, m;
int cut[N], h[N][N][2], g[N], f[N], fac[N];
int main()
{
n = read(), m = read(); fac[0] = 1;
for(int i = 1; i <= n; ++i) fac[i] = 1ll*fac[i-1]*i%P;
for(int i = 1, p = 1; i <= m; ++i)
{
cut[p] = 1; p += (n-i)/m+1;
cut[p] = 1; p += (n-i)/m+1;
}
for(int i = 0; i <= n<<1; ++i) h[i][0][0] = 1;
for(int i = 1; i <= n<<1; ++i)
for(int j = 1; j <= n; ++j)
{
h[i][j][0] = (h[i-1][j][0]+h[i-1][j][1])%P;
if(!cut[i]) h[i][j][1] = h[i-1][j-1][0];
}
for(int i = 0; i <= n; ++i) g[i] = 1ll*(h[n<<1][i][0]+h[n<<1][i][1])*fac[n-i]%P;
for(int i = 0, w = 1; i <= n; ++i, w = P-w) f[0] = (f[0]+1ll*w*g[i]%P)%P;
printf("%d\n", f[0]);
return 0;
}

Sugigma: The Showdown

给定两棵树和起点,两人分别在两棵树上交替移动,相遇后游戏结束,现在 \(A\) 想最大化游戏轮数,\(B\) 想最小化游戏轮数,求游戏轮数

很明显,最终如果游戏不能无限进行则 \(A\) 会最终不移动,那么问题关键就是求出 \(A\) 能移动到点集,我们发现有个很明显的充分条件是起点到这些点的经过点到 \(A\) 的距离小于 \(B\) 到这些点的距离,通过搜索判断可达性即可

如果游戏可以无限进行,则 \(A\) 一定可以达到一条边 \((u,\ v)\) 满足在 \(B\)\(d_{u,v}>2\),达到这条边后左右跳即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <algorithm>
#include <cctype>
#include <cstring>
#include <cstdio>
using namespace std;
inline int read(int f = 1, int x = 0, char ch = ' ')
{
while(!isdigit(ch = getchar())) if(ch == '-') f = -1;
while(isdigit(ch)) x = x*10+ch-'0', ch = getchar();
return f*x;
}
const int N = 2e5+5;
struct Edge
{
int next, to;
Edge(int next = 0, int to = 0):next(next), to(to) {};
}edge[N<<2];
int ha[N], hb[N], tot;
void _add(int *h, int x, int y) { edge[++tot] = Edge(h[x], y); h[x] = tot; }
void add(int *h, int x, int y) { _add(h, x, y); _add(h, y, x); }
int fa[N], son[N], size[N], top[N], d[N];
int ans, cir;
void dfs(int x)
{
size[x] = 1;
for(int i = hb[x]; i; i = edge[i].next)
{
int y = edge[i].to; if(y == fa[x]) continue;
d[y] = d[x]+1, fa[y] = x, dfs(y),
size[x] += size[y], son[x] = size[son[x]]>size[y]?son[x]:y;
}
}
int lca(int x, int y)
{
while(top[x] != top[y])
{
if(d[top[x]] < d[top[y]]) swap(x, y);
x = fa[top[x]];
}
return d[x]<d[y]?x:y;
}
int dis(int x, int y) { return d[x]+d[y]-2*d[lca(x, y)]; }
void dfs(int x, int topf)
{
top[x] = topf; if(son[x]) dfs(son[x], topf);
for(int i = hb[x]; i; i = edge[i].next)
{
int y = edge[i].to;
if(y != fa[x]&&y != son[x]) dfs(y, y);
}
}
void escape(int x, int fa, int dep)
{
ans = max(ans, d[x]<<1);
for(int i = ha[x]; i; i = edge[i].next)
{
int y = edge[i].to;
if(dis(x, y) > 2) cir = 1;
if(y == fa||dep+1 >= d[y]) continue;
escape(y, x, dep+1);
}
}
int n, a, b;
int main()
{
n = read(), a = read(), b = read();
for(int i = 1; i < n; ++i)
{
int x = read(), y = read();
add(ha, x, y);
}
for(int i = 1; i < n; ++i)
{
int x = read(), y = read();
add(hb, x, y);
}
dfs(b); dfs(b, b); escape(a, 0, 0);
printf("%d\n", cir?-1:ans);
return 0;
}

Many Easy Problems

给定一棵无根树,定义 \(f(i)\),对于所有大小为 \(i\) 的点集,求出能够包含它的最小连通块大小之和。对于 \(i=1 \to n\) 的所有 \(i\),求出 \(f(i)\)

想这道题的过程收获挺多的,首先这道题问的点集,按照套路就是思考每个点对于 \(f(i)\) 的贡献,但没想出来,所以看到联通块,又想到 \(dfs\) 序与联通块的联系,得到一个的做法,简单来说就是枚举每个点对,考虑点对在 \(dfs\) 序的子序列的位置用组合数算下贡献就好了,这个思路想到 \(O(n^3)\) 就想不下去了

再考虑每个点的贡献,其实很好算,设 \(s_i\) 为定根后节点 \(i\) 的子树大小,考虑节点 \(i\) 会被多少个点集算到,得到

\[ f(k)\ =\ \sum_{i=1}^n\binom{n}{k}-\binom{n-s_i}{k}-\sum_{j\in son_i}\binom{s_j}{k} \]

化简得到

\[ f(k)\ =\ n\binom{n}{k}-\sum_{i=2}^n\binom{n-s_i}{k}+\binom{s_i}{k} \]

主要问题是求出后面的式子,设它的生成函数为 \(h\) 则有

\[ h\ =\ \sum_{k}\sum_{i=2}^n[\binom{n-s_i}{k}+\binom{s_i}{k}]x^k \]

这时候我就被降智了,考虑交换求和序,有

\[ \begin{aligned} h\ &=\ \sum_{i=2}^n\sum_{k}\binom{n-s_i}{k}x^k+\binom{s_i}{k}x^k\\\\ &=\ \sum_{i=2}^n(1+x)^{s_i}+(1+x)^{n-s_i} \end{aligned} \]

开一个桶 \(c\) 记录一下相同的指数,则

\[ h\ =\ \sum_{i=0}^nc_i(1+x)^i \]

这不就是一个裸的多项式复合函数吗,结果一看多项式复合函数的复杂度,溜了,溜了

接着我发现这就是多项式的平移吗,那就求值完平移再插回去就好了呀,学了半天求值和插值,一发交上去只拿了暴力分

稍微优化一下,改用原根求值,最后直接 \(INTT\) 即可,还是只拿了暴力分

翻了具体数学提高知识水平,发现下降幂可做,但求下降幂又要多点求值自闭

最后重新看这个式子,好像就是一个裸的卷积

\[ \begin{aligned} h\ &=\ \sum_{k}\sum_{i=0}^nc_i\binom{i}{k}x^k\\\\ &=\ \sum_{k}\frac{1}{k!}\sum_{i=0}^n(c_i\times i!)(\frac{1}{(i-k)!})x^k \end{aligned} \]

后面稍微转置一下就是卷积了

同时,我们惊喜地发现多项式线性变换同样是一个简单的卷积

常数巨大的 \(AC\) 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
using namespace std;
typedef long long int64;
inline int read(int f = 1, int x = 0, char ch = ' ')
{
while(!isdigit(ch = getchar())) if(ch == '-') f = -1;
while(isdigit(ch)) x = x*10+ch-'0', ch = getchar();
return f*x;
}
const int N = 2e5+5, P = 924844033;
struct Edge
{
int next, to;
Edge(int next = 0, int to = 0):next(next), to(to) {};
}edge[N<<1];
int tot, head[N];
void _add(int x, int y) { edge[++tot] = Edge(head[x], y); head[x] = tot; }
void add(int x, int y) { _add(x, y), _add(y, x); }
int n, s[N];
void dfs(int x, int fa)
{
s[x] = 1;
for(int i = head[x]; i; i = edge[i].next)
{
int y = edge[i].to; if(y == fa) continue;
dfs(y, x); s[x] += s[y];
}
}
int64 qpow(int64 a, int b)
{
int64 ret = 1;
for( ; b; b >>= 1, a = a*a%P) if(b&1) ret = a*ret%P;
return ret;
}
int64 fac[N], inv[N<<2], ifac[N];
int64 C(int n, int m) { return n < m?0:fac[n]*ifac[m]%P*ifac[n-m]%P; }
struct Poly
{
vector<int> _;
inline int& operator [] (int i) { return _[i]; }
inline int ti() { return _.size()-1; }
inline void set(int ti) { _.resize(ti+1); }
inline void rev() { reverse(_.begin(), _.end()); }
};
int lim;
int64 w[2][N<<2];
void prepare(int ti)
{
for(lim = 1; lim <= ti; lim <<= 1);
w[0][0] = w[1][0] = w[0][lim] = w[1][lim] = 1;
int64 g = qpow(5, (P-1)/lim);
for(int i = 1; i < lim; ++i) w[1][lim-i] = w[0][i] = w[0][i-1]*g%P;
}
void NTT(Poly &A, int t)
{
if(!t) A.set(lim-1);
for(int i = 0, j = 0; i < lim; ++i)
{
if(i > j) A[i] ^= A[j] ^= A[i] ^= A[j];
for(int k = lim>>1; (j ^= k) < k; k >>= 1);
}
for(int mid = 1; mid < lim; mid <<= 1)
for(int len = mid<<1, j = 0; j < lim; j += len)
for(int k = 0, p = 0, q = lim/len; k < mid; ++k, p += q)
{
int64 x = A[j+k], y = w[t][p]*A[j+k+mid]%P;
A[j+k] = x+y<P?x+y:x+y-P;
A[j+k+mid] = x-y<0?x-y+P:x-y;
}
if(!t) return; int64 v = inv[lim];
for(int i = 0; i < lim; ++i) A[i] = A[i]*v%P;
}
Poly operator * (Poly A, Poly B)
{
int n = A.ti(), m = B.ti();
if(n <= 128||m <= 128)
{
Poly C; C.set(n+m);
int *c = &C[0], *a = &A[0], *b = &B[0];
for(int i = 0; i <= n; ++i)
{
int *f = c+i; int64 x = a[i];
for(int j = 0; j <= m; ++j) f[j] = (f[j]+x*b[j]%P)%P;
}
return C;
}
prepare(n+m), NTT(A, 0), NTT(B, 0);
for(int i = 0; i < lim; ++i) A[i] = 1ll*A[i]*B[i]%P;
NTT(A, 1); A.set(n+m);
return A;
}
Poly F, G;
int px[N<<1], py[N<<1];
int main()
{
for(int i = 1; i <= N<<2; i <<= 1) inv[i] = qpow(i, P-2);
n = read(), fac[0] = fac[1] = ifac[0] = ifac[1] = 1;
for(int i = 1; i < n; ++i)
{
int x = read(), y = read();
add(x, y);
}
dfs(1, 0); F.set(n), G.set(n);
for(int i = 2; i <= n; ++i)
{
inv[i] = (P-P/i)*inv[P%i]%P,
fac[i] = i*fac[i-1]%P,
ifac[i] = inv[i]*ifac[i-1]%P;
++F[s[i]], ++F[n-s[i]];
}
for(int i = 0; i <= n; ++i) F[i] = F[i]*fac[i]%P, G[i] = ifac[i]%P;
F.rev(), F = F*G, F.set(n), F.rev();
for(int i = 1; i <= n; ++i) printf("%lld\n", (n*C(n, i)%P-F[i]*ifac[i]%P+P)%P);
return 0;
}