OI经验

· · 个人记录

3 月 8 日讲义(计数)

Section 1

处理 k 次方和的一类问题(一般来说 k\le 100,甚至 k\in \{2,3,4\})。

假如说有个计数问题,令 S 为所有合法方案构成的集合。每个方案 x\in S 有一个价值 val(x),我想计算

\sum_{x\in S} val(x)^k.

标准手段

例 1 OSU!

你要随机生成一个长度为 n 的 01 序列,第 i 个位置以 pi 的概率为 1,以 1-p_i 的概率为 0. 你最后得到的序列的价值定义为所以 "1的连续段" 的长度的立方和。

(比如说 0 0 1 1 1 0 1 0 1 1 0 0 的价值 3^3+1^3+2^3)。

求你最后生成的序列的价值的期望。

解法

X_i 表示考虑01序列的前 i 个元素,后缀的 1 连续段的长度。

如果说 i+1 是 0:产生 X_i^3 的贡献。否则的话它转移到 X_{i+1}E[(X_{i+1})^3] = p_{i+1} E[(X_i+1)^3].

例 2

(原创?)给出 n 个整数 a_1,\dots, a_n 排成 1 列。我们在每相邻的两个数之间随机插入 AND / OR / XOR,得到一个式子,(从左到右)计算这个式子可以得到一个值。问最终得到的值的平方的期望。

解法

所有运算都是按位运算。

把一个数按照二进制写开 v = v_{31}\cdot2^{31} + v_{30} \cdot 2^{30} + ... + v_1 \cdot 2 + v_0。它的平方 v^2 = (... + ... + ...)^2,我把平方拆开,就可以把计算归约到计算”任意两个位的乘积期望“。

当我们只关注某两个位的时候,可以做DP:$f[k][0/1][0/1]$ 表示处理了前 k 个数,我关注的两个位分别是 blabla 的概率。 复杂度:$32^2 \cdot n$。 #### 管道取珠 跳过。 #### 例 给定一个 $n$ 个点的无权树。给定 $m\le 100$,求 $\sum_{i,j} dis(i,j)^m$。$n\le 10^5$。 ##### 解法 DP:$g[x][k]$:表示以 $x$ 为根的子树里所有点到 $x$ 的距离的 $k$ 次方和。 $O(m^2 n)$。 两种点对: * 祖先 - 后代 * 其他情况。 优化:$f[x][k] = \sum_{y:x子树内} dis(y,x)^{k\downarrow}$. $$ n^{k\downarrow} = n(n-1)(n-2)\dots (n-k+1). $$ 组合解释:从 $y$ 到 $x$ 这一条路中有 $dis(y,x)$ 这么多条边,我在其中(有顺序的)选 $k$ 条不同的边的方案数。 转移:$\sum_{d_2} (d_2+1)^{k\downarrow} = \big((d_2-k+1)+(k)\big)(d_2)\dots (d_2-k+2) = f(y,k) + f(y,k-1)\cdot k$. 计算答案? * 想法一:在每个点转化回通常幂的和。(转化会花费 $m^2$ 的时间,$O(m^2 n)$) * 想法二:斯特林数。 * $$ d^m = \sum_{i=0}^m S_2(m,i) \cdot d^{i\downarrow} \\ d^m = \sum_{i=0}^m S_2(m,i) {d\choose i} \cdot i! $$ * $S_2(m,i)$ 表示把 $m$ 个元素分成 $i$ 个非空集合的方案数。 * 组合理解:$d^m$ 是把 $m$ 个元素放进 $d$ 个框里的方案数。枚举 $i$ 个框非空。先把 $m$ 个元素分成 $i$ 组,再从 $d$ 个框里选出 $i$ 个来放。 * (一种理解:考虑 $m=2$ 的情形:$d^2$ 可以表示一个有 $d$ 个同学的班级,要选出一个班长和一个学习委员的方案数。如果班长和学委不是同一个人:$d^{2\downarrow}$;如果是同一个人的话:$d^{1\downarrow}$。$m=3$ 的情形,$d^3$ 可以表示要选一个班长、学委和体委的方案数。我可以做分类讨论:班长学委体委 互不相同 / 班长学委是同一个人 / 学委体委同一个人 / 班长体委是同一个人 / 三个都是同一个人)对于一般的 $m$,我不想做分类讨论:用斯特林数就给出了一个通用的公式。 回到原题,我们可以得到:$\sum_{i,j} dis(i,j)^m = \sum_{k=0}^m S_2(m,k) \cdot \sum_{i,j}dis(i,j)^{k\downarrow}$. * 换根DP:在 $O(mn)$ 的时间里,算出以每个点 $x$ 为根的时候,$f(x, i) = \sum_{y}dis(x,y)^{i\downarrow}$。 #### 习题 > 给定 $n, m$,求所有 $n$ 个点的带标号无向图的连通块个数的 $m$ 次方和。$n\le 50000, m\le 20$,结果对 $998244353$ 取模。 ### Section 2 杂题 #### CoinChange: > 你有面值为 $10^k, k\in \{0,1,2,..., \infty\}$ 以及 $5\cdot 10^k, k\in \{0,1,2,\dots, \infty\}$ 的钞票。每种钞票你都有无限张。 > > 问有多少种方案能凑出 $M$ 元。$M\le 10^{18}$。 ##### 解法 价值为 $v$ 的物品对应的生成函数 $\frac{1}{1-x^v}=1+x^v+x^{2v}+\dots $. 令 $S = \{10^k, 5\cdot 10^k\}$,那么我想计算: $$ [x^M]\left( \prod_{v\in S} \frac{1}{1-x^v}\right) $$ 我们把面值从小到大排序,假如说 $1=v_1 < v_2 < v_3 < \dots < v_T \le M$。 我写一个naiveDP:$f(i, w)$ 表示只使用前 $i$ 个面值,凑出 $w$ 元的方案数。 首先: * $f(1, w) \equiv 1$。 * 当我对 $v_1$ 做完决策之后,我需要保证 $M\bmod v_2 = w \bmod v_2$. * 如果我想凑出 23 元,这个余数 ”3“ 我只能用 1 元来凑。 * 换句话说,我只关心 $w \bmod v_2 = M\bmod v_2$ 的那些 $w$ 的 DP 值。 * 令 $r = M\bmod v_2$,我们定义一个变换:$g(1, w) = f(1, w \cdot v_2 + r)$ 。 * 假如说 $f(1,w)$ 是一个关于 $w$ 的多项式 $P(w)$,那么 $g(1,w) = P(wv_2 + r)$ 也是一个关于 $w$ 的多项式。 * 之后的运算里,我们就用 $g(1, w)$ 去做 DP,我们可以把 $v_2,\dots, v_T, (M-r)$ 都除以 $v_2$。 * $f(1, x) => f(2, x + t\cdot v_2)

回顾:

如果有 K 个不同的面值的话,时间复杂度是 O(K^3) 或者 O(K^2)

基于斯特林数的容斥

异或图

给定 m 张无向图。每个图的大小都是 n=10m\le 64

定义两张图 G_1,G_2 的异或为 G_1\oplus G_2,一条边在 G_1\oplus G_2 出现,当且仅当它在G_1, G_2 两个图里的恰好一张出现。

问多少个不同的子集,使得子集的异或图联通。

解法

容斥:首先不考虑任何限制,有 2^m 张图。

对于不连通的图,我们枚举它的一个划分 [n] = S\sqcup T,我们要求 S、T 之间没有边,计算出这种情形下的子集个数(高斯消元)(每条跨越 S、T 的边给出一个线性限制,联立一个方程组。计算这个方程组的秩为 r,那么方案数为 2^{m-r})。从 2^m 里把它们减掉。

这样的问题在于可能会多减:如果一个图里实际上有三个连通块,那么在上述过程里会被减掉 3 次。我们把它补回来:我们枚举划分 [n] = S\sqcup T\sqcup R,要求 S,T,R 两两之间没有边,计算出方案数 w,我们在最终答案里加上 2w

上述的问题在于可能会多加:如果一个图有 4 个连通块,它目前的贡献是:1 - 7 + 6\cdot 2 = +6. 我们把它再减掉:枚举 [n] = S\sqcup T\sqcup R\sqcup Q,要求两两没有边。计算出方案数 w,在答案里减掉 6w

一般地,我们枚举把 [n] 个点划分成 k 个非空连通块,要求连通块之间没有边,计算出方案数 w,在最终答案里加上 (k-1)!\cdot (-1)^{k-1}

正确性:假如说一个图实际有 c 个连通块,它在这个计算过程中的总贡献是:

\sum_{k=1}^c S_2(c, k) \cdot (-1)^{k-1} \cdot (k-1)! = [c = 1].

复杂度:O(bell(n) \cdot n^2\cdot m),其中 bell(n) 表示把 n 个元素划分成若干个集合的方案数(它相当于一行第二类斯特林数的和)。

n 个点 m 条边的带标号简单无向连通图个数

##### 解法 标准做法:DP:$f(n, m)$ 表示 n 个点 m 条边的无向连通图个数。 转移:用所有的图,减掉不合法的。对于不合法的图,枚举 1 号点所在的连通块的情形。 $$ f(n, m) = {{n\choose 2} \choose m} - \sum_{i=1}^{n-1} \sum_{j=0}^m {n-1\choose i-1}\cdot f(i,j) \cdot {{n-i\choose 2}\choose m-j}. $$ 复杂度:$O(n^6)$:状态 $O(n^3)$,转移 $O(n^3)$。 用斯特林数容斥,希望设计一个状态数 $O(n^3)$,转移 $O(n)$ 的算法。 ##### 容斥 Naive 想法:用所有的图,减去至少有两个连通块的。 $$ \binom{n\choose 2}{m} - \sum_{i=1}^{n-1} {n-1\choose i-1} {{i\choose 2}+{n-i\choose 2}\choose m} $$ 如果一个图里实际有 3 个连通块,它会在方案里计 -2 倍的贡献。我们加一个修正项: $$ 2\cdot \sum_{i_1+i_2+i_3 = n} (把n个点划分成i_1+i_2+i_3的方案) \cdot {{i_1\choose 2}+{i_2\choose 2}+{i_3\choose 2}\choose m} $$ 一般地,如果一个图里实际有 $k$ 个连通块,它会在之前的计算里计 $(k-1)!\cdot (-1)^{k}$ 的贡献,加一个修正项: $$ (k-1)!\cdot (-1)^{k-1} \sum_{i_1+i_2+\dots+i_k = n} (划分的方案) \cdot {{i_1\choose 2}+...+{i_k\choose 2}\choose m}. $$ 我们现在考虑用一个DP来实现这个计算: * 一个比较naive 的想法是,令 $g(k, n, t)$ 表示 "把 $n$ 个点划分成 $k$ 块,划分结束之后有 $m$ 个可以连边的点对" 的方案数。 * 转移:$g(k, n, m) = \sum_{i=1}^n {n-1\choose i-1} \cdot g(k-1, n-i, m - {i\choose 2})$. * 答案:$ANS = \sum_{k=1}^n (k-1)!\cdot (-1)^{k-1} \cdot \sum_t g(k, n, t)\cdot {t\choose m}$。 * 复杂度:$g$ 的状态数 $O(n^4)$,转移 $O(n)$,答案统计 $O(n^3)$,总复杂度 $O(n^5)$。 **优化**. 我们希望优化掉 $g(k, n, m)$ 里面的 $k$ 这一维。考虑答案是怎么计算出来的: $$ ANS = \sum_{t} {t\choose m} \cdot \sum_{k=1}^{n} (k-1)! (-1)^{k-1} g(k,n,t). $$ 考虑一个简单一点的问题: $$ ANS = \sum_{t} {t\choose m} \cdot \sum_{k=1}^{n} g(k,n,t). $$ * 在这个例子里,可以直接优化掉 $k$ 这一维。令 $g(n,t)$ 表示 n 个点划分成若干块,划分完之后有 t 个可以连边的点对。 考虑一个稍微难一点的问题: $$ ANS = \sum_{t} {t\choose m} \cdot \sum_{k=1}^{n} (-1)^{k-1} g(k,n,t). $$ * 令 $g(n,t)$ 表示 n 个点划分成若干块,划分完之后有 t 个可以连边的点对,根据块数的奇偶性产生 +1 / -1 的贡献。 * 每次加入一个新的块会翻转奇偶性,翻转符号就可以了。$g(n, m) = \mathrm{1}[m=={n\choose 2}] -\sum_{i=1}^n {n-1\choose i-1} \cdot g( n-i, m - {i\choose 2})$. 考虑一个更难一点的问题: $$ ANS = \sum_{t} {t\choose m} \cdot \sum_{k=1}^{n} k!\cdot (-1)^{k-1} g(k,n,t). $$ * 令 $g(n,t)$ 表示 n 个点划分成**有顺序的**若干块,划分完之后有 t 个可以连边的点对,根据块数的奇偶性产生 +1 / -1 的贡献。 * 修改一下 DP,不需要钦定一号点:$$g(n, m) = \mathrm{1}[m=={n\choose 2}] -\sum_{i=1}^n {n\choose i} \cdot g( n-i, m - {i\choose 2})$$. 考虑我们实际要算的值: $$ ANS = \sum_{t} {t\choose m} \cdot \sum_{k=1}^{n} (k-1)!\cdot (-1)^{k-1} g(k,n,t). $$ * 为了体现 $(k-1)!$,我们只让最后一次枚举是无序的。 * 令 $g(n,t)$ 表示 n 个点划分成**有顺序的**若干块,划分完之后有 t 个可以连边的点对,根据块数的奇偶性产生 +1 / -1 的贡献。 * $g(n, m) = \mathrm{1}[m=={n\choose 2}] -\sum_{i=1}^n {n\choose i} \cdot g( n-i, m - {i\choose 2})$. * 我们定义 $h(n, t)$ 表示一个辅助数组。 * $h(n, m) = \mathrm{1}[m == {n\choose 2}] - \sum_{i=1}^{n} {n-1\choose i-1} \cdot g(n-i, m - {i\choose 2})$. 那么: $$ ANS = \sum_{t} h(n, t)\cdot {t\choose m}. $$ 最后的复杂度就是 $O(n^4)$。 ##### 应用 > 给定一个 $n$ 个点的图,初始为空。每次随机一个pair $(u, v) \in {[n]\choose 2}$,在 u 和 v 之间连一条边。重复这个过程直到图连通。 > > 问期望多少次之后这个过程停止。 $n \le 100$。 ##### 解法 1 假设在图连通之后,这个随机过程**并不会**停下来。 当图不连通的时候,我们执行一步过程需要花费 1 元钱;否则的话免费。我们把这个过程执行无穷多步,问我们期望的花费是多少。 考虑这个随机过程已经运行了一段时间。假如说现在图里有 $t$ 条本质不同的边。那么此时图连通的概率为 $f(n, t) / {{n\choose 2}\choose t}$。 如果图不连通的话,我们需要花钱来执行这个随机过程。每次随机,有 $\frac{t}{{n\choose 2}}$ 的概率随到一条已经有的边。期望下我需要花费 $\frac{{n\choose 2}}{{n\choose 2} - t}$ 元钱来连出下一条本质不同的边。所以期望下我在这个过程中的花费是 $\left( 1 - \frac{f(n, t)}{{{n\choose 2}\choose t}}\right) \cdot \frac{n\choose 2}{{n\choose 2} - t}$。 对 $t$ 求和得到: $$ ANS = 1 + \sum_{t=1}^{{n\choose 2}} \left( 1 - \frac{f(n, t)}{{{n\choose 2}\choose t}}\right) \cdot \frac{n\choose 2}{{n\choose 2} - t}. $$ ##### 解法 2 $$ ANS = E[步数] = \sum_{i=0}^{\infty} \Pr[图在 i 步之后没有连通] $$ 考虑计算 $\Pr[图在 i 步之后没有连通]$:容斥。 枚举把 $n$ 个点划分成两份,要求不能存在跨越的边:$\sum_{i} {n-1\choose i-1}\left(\frac{{i\choose 2}+{n-i\choose 2}}{n\choose 2}\right)^i$. 这么算不对:如果有一次随机过程把图分成了 3 块,那这个过程在上面的式子里产生了 3 份贡献。枚举 3 划分,把这些贡献减掉。 。。。 ### 比赛经验 赛前 赛时 考前半小时 写文操快读等必要东西,可以用压缩软件提前看题目名,节省时间 考试进行时 时间规划很重要。建立自己的反馈机制。(例:儒略日) 注意时效。恰烂分也香——暴力的重要性(快速拿分,提示正解)。 一些 历年省一线的例子。 合理舍弃程序的正确性:乱搞。( NOIP T3 ) 赢在考试——通过签到题(开场读题好习惯,0.5h读题绝对不亏)。 赛后 : 这还卷?! ### 学习经验 算法 流派招式 没有很难的算法。现在 CCF 已经有了考纲。但我觉得早在这之前,任何一个善于总结的人都能自己得出其中的结论:FFT 和生成函数是不考的;LCT 是不考的;计算几何几乎是不考的......。有趣的是,OI 模拟赛中经常出现这些知识点。我不喜欢这一现象,一方面它不能起到“模拟”的作用(准确反映出选手在正式比赛当中的水平),另一方面它会给选手的训练带来一些错误的引导。 算法是基础但不能过于强调。 身怀屠龙之术,不识龙为何物,直沦为暴力老哥。 (历年正解)一些常用算法的普遍出现 历年的DP。 思维 识破 基础的算法有其固有的套路,非常重要。很多基础算法的固有套路,在比赛里多次出现过。第一次遇到时不容易自己想到,看了题解往往让人直呼巧妙。当你在平时遇到一道题,是基础算法在某些条件下的巧妙使用,请一定要多加关注。例如:很多二维 DP 可以把第二维搬到线段树上;需要考虑序列上所有区间,可以分治,每次只解决跨过中点的区间;多次矩阵快速幂,可以通过预处理矩阵的 2k 次幂,转化成向量乘法......。 分析问题性质的能力很重要。很多题看起来很难,其实是通过巧妙的设计,隐藏了关键的性质。有时候选手一读完题会被看似复杂的问题震住,然后就停止思考了。此时特别需要选手静下心来,在纸上写写画画,认真分析题目性质,将复杂的、实际的问题,转化成简单的、本质的模型。有时候完成这些分析,解题就成功了一大半。 建立自己的知识框架(网络流的流传,NOIT1的树剖)。 出题人的角度。 做题提取本质。(讲相互联系的题) 最重要的能力(贯穿始终,算法代码积累一定程度都会) (讲一点思维题)移球游戏,贪吃蛇 代码 忍杀 强大的代码能力和良好的代码习惯。(例5 冬令营血亏80pt) 首先特殊的语法,空间MLE,RE的敏感性,memset是否导致错误的时间复杂度,交互题,乃至传统题避免与std重名(nxt)。 不要过于依赖调试,甚至是输出调试,对自己代码持保留态度。 如何提升:虽然有题解,但还是建议对这自己代码硬调,代码能力就是在每次犯错、反馈的机制下练出来的,比赛爆炸不如平时头铁。 ```cpp #include<bits/stdc++.h> using namespace std; inline int read(){ int x=0,f=1; char s; while((s=getchar())<'0'||s>'9') if(s=='-') f=-1; while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^'0'),s=getchar(); return x*f; } const int N=3e5+5; int T,n,m,q,rt,ans; int gn(int i,int j){ if(i<1||i>n||j<1||j>m) return 0; return (i-1)*m+j; } int head[N],tot; struct node{ int to,nxt,w; }e[N<<2]; int dx[5]={0,0,0,1,-1}; int dy[5]={0,1,-1,0,0}; inline void add(int u,int v,int w){ if(!v||!u) return; e[++tot].to=v,e[tot].nxt=head[u],head[u]=tot,e[tot].w=w; e[++tot].to=u,e[tot].nxt=head[v],head[v]=tot,e[tot].w=w; } struct po{ int col,lv; }p[N]; bool ok[N],fl[N],vis[N]; int que[N],top; inline bool check(int u,int v){ if(!u||!v) return 0; if(p[u].col!=p[v].col&&p[u].lv>=p[v].lv) return 1; return 0; } void dfs(int u,int fap){ if(vis[u]) return; vis[u]=1; if(!fl[u]) ++ans,fl[u]=1,que[++top]=u; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==0) continue; if(e[i].w!=3||v==fap) continue; if(ok[v]){ if(check(rt,v)) if(!fl[v])++ans,fl[v]=1,que[++top]=v; }else if(!vis[v])dfs(v,u); } } void dfs2(int u,int x,int y,int fap,int ty){ if(vis[u]) return; vis[u]=1; if(!fl[u]) ++ans,fl[u]=1,que[++top]=u; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fap||e[i].w!=2) continue; int tmp=gn(x+dx[ty],y+dy[ty]); if(tmp!=v) continue; if(ok[v]){ if(check(rt,v)) if(!fl[v]) ++ans,fl[v]=1,que[++top]=v; }else dfs2(v,x+dx[ty],y+dy[ty],u,ty); } } char s[N<<1]; int main(){ // freopen("chess.in","r",stdin); // freopen("chess.out","w",stdout); T=read(); while(T--){ memset(head,0,sizeof(head)),tot=0; memset(ok,0,sizeof(ok)); memset(fl,0,sizeof(fl)); top=0,rt=0,ans=0; n=read(),m=read(),q=read(); for(int i=1,ty;i<=n;++i){ scanf("%s",s+1); for(int j=1;j<m;++j){ ty=s[j]-'0'; add(gn(i,j),gn(i,j+1),ty); } } for(int i=1,ty;i<n;++i){ scanf("%s",s+1); for(int j=1;j<=m;++j){ ty=s[j]-'0'; add(gn(i,j),gn(i+1,j),ty); } } for(int i=1,col,lv,x,y;i<=q;++i){ ans=0; for(int j=1;j<=top;++j) fl[que[j]]=0; top=0; col=read(),lv=read(),x=read(),y=read(); p[gn(x,y)].col=col,p[gn(x,y)].lv=lv; ok[rt]=1; rt=gn(x,y); //处理直通道路 for(int j=head[rt];j;j=e[j].nxt){ int v=e[j].to; if(e[j].w==2){ if(ok[v]){//有就不能走了 if(check(rt,v)) ++ans,fl[v]=1,que[++top]=v; } else for(int k=1;k<=4;++k){ int tmp=gn(x+dx[k],y+dy[k]); if(tmp==v){ memset(vis,0,sizeof(vis)); vis[rt]=1; dfs2(v,x+dx[k],y+dy[k],rt,k); break; } } } } //处理其他道路 for(int j=head[rt];j;j=e[j].nxt){ int v=e[j].to; if(e[j].w==0||e[j].w==2) continue; if(e[j].w==1){ if(!ok[v]) { if(!fl[v]) ++ans,fl[v]=1,que[++top]=v;} else if(check(rt,v)) if(!fl[v]) ++ans,fl[v]=1,que[++top]=v; }else{ if(ok[v]) { if(check(rt,v)&&!fl[v]) ++ans,fl[v]=1,que[++top]=v;} else { memset(vis,0,sizeof(vis)); vis[rt]=1; dfs(v,rt); } //只有互通道路才需要dfs } } cout<<ans<<'\n'; } } return 0; } ```