0%

问题描述

给定一棵 $n$ 个结点的树,你从点 $x$ 出发,每次等概率随机选择一条与所在点相邻的边走过去。

有 $q$ 次询问,每次询问给定一个集合 $S$,求如果从 $x$ 出发一直随机游走,直到点集 $S$ 中所有点都至少经过一次的话,期望游走几步。

特别地,点 $x$(即起点)视为一开始就被经过了一次。

答案对 $998244353$ 取模。

输入

第一行三个正整数 $n,q,x$。

接下来 $n-1$ 行,每行两个正整数 $(u,v)$ 描述一条树边。

接下来 $q$ 行,每行第一个数 $k$ 表示集合大小,接下来 $k$ 个互不相同的数表示集合 $S$。

输出

输出 $q$ 行,每行一个非负整数表示答案。

思路

首先要知道 $min-max$ 容斥,然后根据 $min-max$ 容斥的常规套路就可以把原问题转换为求 $f(root)$,表示从 $root$ 开始到集合 $T$ 中元素的最小期望步数。

考虑求 $f(x)$。路径上概率期望的套路一般是根据期望的递推关系,对于每一个点列一个方程,最后解方程组得出期望。这题也可以按照这种方法,得出方程: 用 $O(n^3)$ 解这个方程组,然后套 $min-max$ 容斥的复杂度是 $O(n^32^n+n^2q)$,但这个复杂度是满足不了要求的。我们可以考虑优化如何解这个方程组。下面将式子进行化简: 那么就可以将上式写成:

考虑: 将 $\sum f[to]$ 代入原先式子,得到:

得到:

然后我们设属于集合 $T$ 的点 $A_x=0, B_x=0$,这样我们就可以从下往上递推出 $A_x, B_x$,然后因为 $root$ 没有父亲,所以我们要求的 $f[root]$ 就是 $B_{root}$。

先预处理出所有集合 $T$ 的 $f[root]$,然后对于每一个询问,枚举子集,用 $min-max$ 容斥得出答案。这样总复杂度就是 $O(n*2^n+qn^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
81
82
83
84
85
86
87
88
89
90
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#include <vector>
#include <map>
#include <set>
#define MAXN
#define p 998244353
#define INF 0x3f3f3f3f
#define rint register int
#define LL long long
#define LD long double
using namespace std;

int n, q, x, cnt, head[20], deg[20], a[20], b[20], f[1<<19], siz[1<<19];

struct Edge {int next, to;} edge[40];

void addedge(int from, int to)
{
edge[++cnt].next=head[from];
edge[cnt].to=to;
deg[to]++;
head[from]=cnt;
}

int ksm(int x, int y)
{
int sum=1;
while(y)
{
if(y&1) sum=(1LL*x*sum)%p;
x=(1LL*x*x)%p; y>>=1;
}
return sum;
}

void dp(int sta, int x, int fa)
{

int suma=0, sumb=0;
if((sta>>(x-1))&1) {a[x]=b[x]=0; return;}
for(rint i=head[x]; i; i=edge[i].next)
{
int to=edge[i].to;
if(to==fa) continue;
dp(sta, to, x);
suma=(suma+a[to])%p;
sumb=(sumb+b[to])%p;
}
int temp=ksm((deg[x]+p-suma)%p, p-2);
a[x]=1LL*temp;
b[x]=1LL*(sumb+deg[x])%p*temp%p;
}

int main()
{
scanf("%d%d%d", &n, &q, &x);
for(rint i=1, x, y; i<n; ++i)
{
scanf("%d%d", &x, &y);
addedge(x, y);
addedge(y, x);
}
for(rint sta=0; sta<(1<<n); ++sta)
{
dp(sta, x, 0);
f[sta]=b[x];
siz[sta]=siz[sta>>1]+(sta&1);
}
while(q--)
{
int sta=0, ans=0, k;
scanf("%d", &k);
for(rint i=1, x; i<=k; ++i)
{
scanf("%d", &x);
sta|=(1<<(x-1));
}
for(rint i=sta; i; i=(i-1)&sta)
{
if(siz[i]&1) ans=(ans+f[i])%p;
else ans=(ans+p-f[i])%p;
}
printf("%d\n", ans);
}
return 0;
}

反演的原理

反演一般是解决这样一个问题:有两个数列 $\lbrace f_n\rbrace,\lbrace g_n\rbrace$,满足关系: 题目要你求 $f_n$,但你却知道数列 $\lbrace g_n\rbrace$ 怎么求。这时就可以利用反演,用 $g_0, g_1, \cdots, g_n$ 求出 $f_n$,即:

我们就考虑,如果 $\lbrace f_n\rbrace,\lbrace g_n\rbrace$ 能够反演,系数 $a,b$ 应该满足什么关系。

我们假设它们能够反演,将上面两个式子合并:

如果能够反演,那无论 $\lbrace g_n\rbrace$ 为多少,上式都应成立,因此反演的充要条件就是:

满足这个条件的式子有很多,而下面会提到其中的一部分。

普通容斥(子集反演)

形式一: 形式二:

1.BZOJ4455 小星星

代码链接

2.CF451 Devu and Flowers

代码链接

二项式反演

1.BZOJ3622 已经没有什么好害怕的了

代码链接

2.BZOJ1879 Bill的挑战

代码链接

斯特林反演

莫比乌斯反演

形式一:

形式二:

1.BZOJ2301 Problem b

代码链接

2.BZOJ2820 YY的GCD

代码链接

最值反演

两种普通形式:

期望意义下:

1.LOJ2542 随机游走

代码链接

12 月 29 日 AtCoder Grand Contest 030 比赛链接:https://atcoder.jp/contests/agc030

C - Coloring Torus

思路: 如果当 $k$ 为 $4$ 的倍数时,有一种简单粗暴的方法就是奇数列填 $1 \sim k/2$ 偶数列填 $k/2+1 \sim k$,这显然是满足要求的,但因为这种方法每个数它左右两边都一样,可扩展性不强。我们可以在这个的基础上稍加变化,就是把第 $i$ 列向后移 $i$ 个,形式化的说就是:

对于奇数行 $col_{i,j} = (i+j)\mod k/2$ 对于偶数行 $col_{i,j} = (i+j)\mod k/2 +k/2$

这样构造出来的矩阵就有很好性质,对于每个数不光它上下两个数是它的 $+1$ 和 $-1$,而且它左右两个数是它的 $k/2+1$ 和 $k/2-1$。因此很自然的可以想到如果 $k$ 不是 $4$ 的倍数时,设 $n$ 为 $\lceil \frac{k}{4} \rceil * 4$,然后将多出来的 $n-k$ 个数都减去 $n/2$。因为上面提到的性质,减去 $n/2$ 后可以看成将它上下两个数与左右两个数交换,这样依旧可以保证满足题目要求

代码链接

D - Inversion Sum

思路: 先把计数转化为求期望,我们设 $f_{i,j}$ 为 $A_i>A_j$ 的概率。可以发现每一次操作会改变 $O(n)$ 个 $f_{i,j}$ 的值,因此可以直接在对应的值上面修改即可。

代码链接

E - Less than 3

思路: 如果在 $01$ 之间画上蓝线,在 $10$ 之间画上蓝线,可以发现我们的每一次操作就是将一根线向左或向右移。且红线和蓝线之间的距离不超过 $3$,红线蓝线交替出现,串的左右两段有无数条线。而我们要做的就是把第一个串的线与第二个串对应起来,因此就枚举第二个串的第一根线对应于第一个串的那一根线,然后这种情况的答案就是确定的且可以 $O(n)$ 计算出来。

代码链接

F - Permutation and Minimum

思路: 有一个脑洞很大的 $DP$,从大到小考虑每一个数,设 $f_{i,j,k}$ 为考虑数 $i$ 时,有 $j$ 没有配对的确定的数,有 $k$ 个没有配对的 $-1$ 且。接下来考虑转移:

如果数 $i$ 确定且和它一对的另一个数也确定,那么它不会对答案产生影响,有 $f_{i,j,k}=f_{i+1,j,k}$ 如果数 $i$ 确定且和它一对的另一个数是 $-1$,那么它可以不配对,有 $f_{i,j,k}=f_{i+1,j-1,k}$;这个它可以和一个 $-1$ 配对,有 $f_{i+1,j,k+1}$ 如果数 $i$ 不确定,那么它可以不配对,有 $f_{i,j,k}=f_{i+1,j,k+1}$;它也可以和一个 $-1$ 配对,有 $f_{i,j,k}=f_{i+1,j,k+1}$;它还可以和一个确定的数配对,有 $f_{i,j,k}=(j+1)*f_{i+1,j+1,k}$

我们想这个 DP 的系数是怎么来的,如果两个 $-1$ 配对我们可以先把所有的 $-1$ 看成没有区别的,系数就为 $1$,最后乘上这样配对数的阶乘;如果一个 $-1$ 和一个确定的数配对,那么它可以有 $j+1$ 种选择,系数就是 $j+1$。

代码链接

结束位置集与状态

结束位置集

对于 $S$ 的一个子串 $t$,我们将 $S$ 中 $t$ 的所有结束位置叫做一个结束位置集,记为 $endpos(t)$。

状态:

我们将 $ S$ 的子串按照 $endpos$ 几何被分为若干个等价类,每一个等价类就对应于后缀自动机的一个状态。

性质:

我们设 $len(t_1)<=len(t_2)$,那么 $t_1$ 是 $t_2$ 的后缀当且仅当 $endpos(t_2) \subseteq endpos(t_1)$,$t_1$ 不是 $t_2$ 的后缀当且仅当 $endpos(t_1)\bigcap endpos(t_2)=\emptyset$。

一个状态所对应的所有子串,其中短的子串一定是长的子串的一个后缀,且子串的长度是连续的。也就是说,一个状态对应的所有子串,都是其中最长子串的一系列连续后缀。

后缀链接

定义:

我们考虑 $S$ 的一个前缀 $S[1,2,…,i]$,它会有 $i$ 个连续的后缀,这组连续的后缀可能会在中间若干个地方「断掉」并分为若干段,其中每一段的连续后缀都对应一个状态。我们可以用一个叫后缀链接的东西把这若干段链接起来。

后缀链接不属于DFA(有限状态自动机)的一部分,由于它奇妙的性质和强大的功能,构造 SAM 时会需要后缀链接。

性质:

所有后缀链接构成一棵根节点为 $t_0$(初始状态)的树。后缀链接构成的树本质上是 $endpos$ 集合构成的一棵树,树的边表示 $endpos$ 集合的包含关系。

构造算法

我们先用一个结构体来储存一个状态内的信息。其中 $len$ 该为对应子串的最长长度,$fa$ 为该状态的后缀链接,$next$ 为该状态的转移。

1
struct State {int len, fa, next[26]...;} st[MAXN*2];

假设我们构造好了 $S[1,2,..,i]$ 的SAM,并向其中添加字符 $s[i+1]$,那么我们就要对 $S[1,2,..,i]$ 所有后缀所对应的状态增加相应的转移,并且可以发现所有这些状态都被一条后缀链接构成的路径连接到初始状态上。我们只需要新建一个状态 $cur$,并对这条路径上的状态增加相应的转移就好。这个转移一共有三种情况:

1
int cur=++tot; st[cur].len=st[last].len+1;

先考虑最简单的情况,就是路径上的所有状态都没有 $s[i+1]$ 的转移。在这种情况下只要简单地把路径上的状态增加一个 $s[i+1]$ 的转移并指向状态 $cur$,再将 $cur$ 的后缀链接连向初始状态。

1
2
for(; p && !st[p].next[c]; p=st[p].fa) st[p].next[c]=cur;
if(!p) st[cur].fa=1;

再考虑第二种情况,路径上有一个状态 $p$ 有 $s[i+1]$ 的转移,设这个转移指向状态 $q$,且 $q$ 的 $len$ 等于 $p$ 的 $len+1$,也就是说 $q$ 对应的最长子串是 $p$ 对应的最长子串加 $s[i+1]$。这样我们只要把 $p$ 之前的状态增加一个 $s[i+1]$ 的转移并指向状态 $cur$,再将 $cur$ 的后缀链接连向 $q$。

1
2
int q=st[p].next[c];
if(st[p].len+1==st[q].len) st[cur].fa=q;

最后考虑最复杂的一种情况,路径上有一个状态 $p$ 有 $s[i+1]$ 的转移,设这个转移指向状态 $q$,但 $q$ 的 $len$ 不等于 $p$ 的 $len+1$,也就是说 $q$ 对应的最长子串不是 $p$ 对应的最长子串加 $s[i+1]$。这里我们要将状态 $q$ 分裂开,新建一份 $q$ 的拷贝 $t$,令 $t$ 状态对应的最长子串为 $p$ 对应的最长子串加 $s[i+1]$ 且令 $p$ 之后的状态的 $s[i+1]$ 转移指向的状态由 $q$ 变为 $t$,最后,将状态 $q$ 和 $cur$ 的后缀链接指向 $t$。

1
2
3
4
5
6
int t=++tot;
st[t].len=st[p].len+1;
memcpy(st[t].next, st[q].next, sizeof(st[q].next));
st[t].fa=st[q].fa;
for(; p && st[p].next[c]==q; p=st[p].fa) st[p].next[c]=t;
st[q].fa=st[cur].fa=t;

最终代码

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
int len, tot=1, last=1;
char s[MAXN];

struct State {int len, fa, next[26];} st[MAXN*2];

void insert(int c)
{
int cur=++tot, p=last;
st[cur].len=st[last].len+1;
for(; p && !st[p].next[c]; p=st[p].fa) st[p].next[c]=cur;
if(!p) st[cur].fa=1;
else
{
int q=st[p].next[c];
if(st[p].len+1==st[q].len) st[cur].fa=q;
else
{
int t=++tot;
st[t].len=st[p].len+1;
memcpy(st[t].next, st[q].next, sizeof(st[q].next));
st[t].fa=st[q].fa;
for(; p && st[p].next[c]==q; p=st[p].fa) st[p].next[c]=t;
st[q].fa=st[cur].fa=t;
}
}
last=cur;
}

int main()
{
scanf("%s", s+1);
len=strlen(s+1);
for(rint i=1; i<=len; ++i) insert(s[i]-'a');
/*
进行操作
*/
return 0;
}

例题

1.hihocoder1445 重复旋律5

代码链接

2.hihocoder1457 重复旋律7

代码链接

3.BZOJ3998 弦论

代码链接

4.hihocoder1465 重复旋律8

代码链接

5.BZOJ4566 找相同字符

代码链接

6.hihocoder1449 重复旋律6

代码链接

7.BZOJ2882 工艺

代码链接

8.hihocoder1449 重复旋律9

代码链接

离散傅里叶变换

定义:

是将 $\omega_n^1,\omega_n^2,…,\omega_n^n$ 带入多项式所得到的一种特殊的多项式点值表示

性质:

考虑将 $A(x)$ 的每一项按下标的奇偶分成两个多项式之和:

$A(x) = (a_0 + a_2x^2 + … + a_{n - 2}x^{n - 2}) + (a_1x + a_3x^3 + … + a_{n-1}x^{n-1})$

如果设:

$A_1(x) = a_0 + a_2x + … + a_{n - 2}x^{\frac{n}{2} - 1}$

$A_2(x) = a_1 + a_3x + … + a_{n - 1}x^{\frac{n}{2} - 1}$

那么:

$A(x) = A_1(x^2) + xA_2(x^2)$

于是我们把 $x = \omega_n^k$ 带入$A(x)$:

当 $k\subseteq \lbrace 0,1,2,… ,\frac{n}{2}-1\rbrace $ 时:

$A(\omega_{n}^{k})=A_1(\omega_{\frac{n}{2}}^{k}) + \omega_n^kA_2(\omega_{\frac{n}{2}}^{k})$

$A(\omega_{n}^{k+\frac{n}{2}})=A_1(\omega_{\frac{n}{2}}^{k}) - \omega_n^kA_2(\omega_{\frac{n}{2}}^{k})$

这样如果我们知道了 $A_1(x),A_2(x)$ 的离散傅里叶变换表示就可以 $O(n)$ 地求出 $A(x)$ 的离散傅里叶变换。

快速傅里叶变换

根据上面所提的离散傅里叶变换的性质,我们就可以递归地在 $O(nlogn)$的时间内将多项式的系数表示转换为点值表示。下面就介绍一下优化和实现上的细节:

蝴蝶操作:

有一个很神奇的性质:将一个多项式按下标的奇偶分为两部分,并将奇数部分放在偶数部分之后,然后不断递归下去。那么多项式下标为 $i$ 的数在这个操作之后的下标就变成了「将 $i$ 二进制翻转所得的数」。

1
for(rint i=0; i<lim; ++i) pos[i]=(pos[i>>1]>>1)|((i&1)<<(l-1));

根据这个性质,我们可以先预处理出多项式中每个数操作之后的位置,并将它放到这个位置。然后类似于递归时回溯的操作,不断向上还原求出点值表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(rint i=0; i<lim; ++i)
if(i<pos[i]) swap(a[i], a[pos[i]]);
for(rint i=1; i<lim; i<<=1)
{
Complex del(cos(PI/i), sin(PI/i));
for(rint j=0; j<lim; j+=(i<<1))
{
Complex t(1, 0);
for(rint k=0; k<i; ++k, t=t*del)
{
Complex x=a[j+k], y=t*a[j+k+i];
a[j+k]=x+y; a[j+k+i]=x-y;
}
}
}

傅里叶逆变换

按照上述方法我们在 $O(nlogn)$ 内将多项式的系数表示转换成了点值表示,然后我们在 $O(n)$ 的时间内将两个多项式的点值表示相乘,得到了乘积的点值表示。利用傅里叶逆变换就可以同样在 $O(nlogn)$ 内将点值表示转换成系数表示。

离散傅里叶变换的另一个性质:把多项式 $A(x)$ 的离散傅里叶变换作为另一个多项式 $B(x)$ 的系数,并将 $\omega_{n}^{0}, \omega_{n}^{-1}, \omega_{n}^{-2}, …, \omega_{n}^{-(n - 1)}$ 代入多项式 $B(x)$ ,得到的每个数除 $n$,这样的 $n$ 个数就恰好是多项式 $A(x)$ 的系数。

根据上面的性质,我们在求离散傅里叶变换的代码上稍作修改,就能在 $O(nlogn)$ 内将点值表示转换成系数表示。

最终代码

下面是用 FFT 优化的高精度乘法代码(BZOJ2179

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
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#include <vector>
#include <map>
#include <set>
#define MAXN 200005
#define INF 0x3f3f3f3f
#define rint register int
#define LL long long
#define LD long double
using namespace std;

const double PI=acos(-1);

int n, m, len, pos[MAXN], ans[MAXN];
char sa[MAXN], sb[MAXN];

struct CP
{
double x, y;
CP(double a=0, double b=0)
{
x=a; y=b;
}
friend CP operator * (CP a, CP b)
{
return CP(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
friend CP operator + (CP a, CP b)
{
return CP(a.x+b.x, a.y+b.y);
}
friend CP operator - (CP a, CP b)
{
return CP(a.x-b.x, a.y-b.y);
}
} a[MAXN], b[MAXN];

void fft(CP a[], int op)
{
for(rint i=0; i<n; ++i)
if(i<pos[i]) swap(a[i], a[pos[i]]);
for(rint i=1; i<n; i<<=1)
{
CP omg(cos(PI/i), op*sin(PI/i));
for(rint j=0; j<n; j+=(i<<1))
{
CP t(1, 0);
for(rint k=0; k<i; ++k, t=t*omg)
{
CP x=a[j+k], y=t*a[j+k+i];
a[j+k]=x+y; a[j+k+i]=x-y;
}
}
}
}

int main()
{
scanf("%d", &len);
scanf("%s%s", sa, sb);
for(rint i=0; i<len; ++i)
{
a[i].x=sa[len-1-i]-'0';
b[i].x=sb[len-1-i]-'0';
}
for(n=1; n<(len*2); ) n<<=1, m++;
for(rint i=0; i<=n; ++i) pos[i]=(pos[i>>1]>>1)|((i&1)<<(m-1));
fft(a, 1);
fft(b, 1);
for(rint i=0; i<n; ++i) a[i]=a[i]*b[i];
fft(a, -1);
for(rint i=0; i<n; ++i)
{
ans[i]+=(int)(a[i].x/n+0.5);
ans[i+1]+=ans[i]/10;
ans[i]%=10;
}
len=len*2-1;
while(!ans[len] && len>=1) len--;
for(rint i=len; i>=0; i--) printf("%d", ans[i]);
return 0;
}

例题

1.BZOJ3771 Triple 2.BZOJ4259 残缺的字符串 3.AGC005F Many Easy Problems