数数专题
注:尚未完成,先发出来,然后去睡觉
数数,是一个从出生就开始学并一直学不会的东西
这里讲“数数”,是指:形如“计算满足xxx条件的方案数”的问题。
实现较复杂的问题会给代码。剩下的可以在 vjudge/原网站 上翻我的提交记录得到代码
注:我一般喜欢交vjudge,并且我的常用号是 LightningUZ_CF 而不是 LightningUZ
有一些数数,比如 GF 相关,会另外讲。
这里讲的主要是不用GF,用组合数,dp等技巧的数数题。
借助dp
动态规划是数数的好朋友,很多时候我们可以用动态规划解决数数问题。
有人称其为 “计数dp”。比如我们熟悉的背包,就是一种dp。
当然,和背包相关的dp,与数数的关系不大,不细展开。
来点例题:
CF814E
参考了LCA的做法,他的题解在 这里
LCA的题解太神仙了,导致我这个菜鸡几乎看一行式子睡一次觉,就为了想 “这里为啥-1?” 这种事情,睡了4、5觉才做出来
所以这里会仔细讲(
这题是比较纯正的dp,只用到了非常少量的组合数学内容,全他妈在讨论
我们发现这条件还挺多:最短路唯一,最短路递增,度数有限制,并且为 或 。
“最短路递增”,想象一下最后的最短路数组,相同的应该连续。那我们可以把连续的一段相同的划开,称为 “段”。
注意到,相邻两个段的最短路应该正好相差 。并且后面一段的每个点都会有 恰好一条 到上一段的边。
(如果没有,那最短路就不对;如果有两条以上,那最短路就不唯一)
段内部允许有边。
看到分段,很容易想到dp。虽然粗略一想似乎不太会,但我们不用急,慢慢来。
别的不管,先设 表示分到第 段的方案数,这个状态怎么也得有。
然后我们按套路,枚举上一段的点 ,然后 转移过来。 函数表示转移系数。
仔细一想,这个转移好像和段里面有多少个点有很大的关系。那我们得再记一维表示点数。
表示到第 段,最后一段分了 个点,方案数。
我们注意到这样还不太能做,因为 度点和 度点处理起来不太一样,所以这个 似乎不太能写出来。
那就记一下上一层有多少个 度和 度的点,记为 ,然后考虑它们在自己那一层(即,上一层)内部,以及和当前的这一层,如何匹配起来。
上一层的点数可以直接得知,就是 。这一层的点数需要再记一下。
再一想,上一层的每个点到上上层都恰好有一条边。那这么说, 度点其实只有 个插头,然后 度点有 个插头。所以转移函数的本质是要考虑这些 插头的匹配。
于是我们设这个转移的函数为, 表示:
- 这一层有 个点,上一层有 个点有 个插头, 个点有 个插头;
- 上一层的更前面已经满足了限制,并且上一层到上上层的边已经连好了;
- 我们要考虑,上一层内部的匹配,以及上一层与下一层的匹配;
的方案数。
容易写出转移: ( 很容易统计出来)
最后的答案就是 。
那我们把 搞出来就做完了。但是这个 看起来比较复杂,分情况讨论
以下称 “” 表示只有一个插头的点, 表示有 个插头的点
-
此时下一层没有点,那就只需要考虑层内的匹配
-
,
此时只有 点,那这样匹配一波,一定会形成若干个环。
我们枚举其中一个环来转移。
为了避免算重复,我们强制让枚举的那个环包含 点中标号最小的那个。
由于不能有重边和自环,环长至少为
得到转移:
其中 表示 项链数,也就是排一个环的方案数。
组合数里面的 是因为我们已经选好一个,接下来只要选 个
-
此时 点都有。我们考虑连一条边,这条边可能连接着:
,,
注意到我们不应该考虑 ,因为它要么会在 的时候被算到,要么会在考虑完 插头间的边之后,其中一个 变成
所以我们只需要考虑,一个 点和哪种点匹配了
为了避免重复,还是要强制选 里面编号最小的那个。
如果我们再选一个 ,那就有 种选法,并且两个插头都接起来,没了。这一部分是
如果我们选一个 ,那就有 种选法。而此时, 点插头没了,而 点的插头数从 个变成 个,也就便乘了 类点。于是 类点的数量并没有变化,只有 类点的数量少了一个。那这一部分是
综上,
-
-
此时我们需要考虑下一层的匹配
下一层的每个点都要找一个匹配。为了避免重复,我们考虑下一层里面标号最小的那个点和谁匹配。
它可能和 类点匹配,有 种选法,此时 类点少一个, 类不变。即
它可能和 类点匹配,有 种选法。此时有一个 类点变成 类点,所以是 少一个 多一个,即
综上,
注意边界
然后就可以先预处理出 ,然后对着式子搞 ,然后对着式子搞答案。
总的复杂度是 ,非常显然。
AGC24E
这题是一个“计树dp”,通常有一种套路: 表示 个点的树,...,的数量。
不过这题,要搞出这颗“树”要花一些功夫。
首先考虑一个问题,题目问的是本质不同的 序列组数量,而同一种序列组可能有不同的插入方案,如何去重?
我们发现,重复当且仅当我们插入的数有一堆连续的重复。比如aaabb要变成aaabbb。
此时我们强行插在最后一个位置,然后方案就是唯一的了。
由于我们要让字典序递增,再分析一下这种插入方式的性质,我们发现:每次插入进去的元素,要比“顶掉”的元素的字典序 严格 大。
为了避免边界情况,我们令序列的最后有一个 ,并且我们不能插在 后面。那插入在最后的情况相当于是顶掉了 ,没问题。
我们考虑每个数的 “流向”。即,我们画一根线。当它被插进来的时候,就新建一个线头。如果它没有被顶掉,那这根线竖直向下,否则就斜着向右下。
比如说,序列组:
0
1 0
1 1 0
1 1 1 0
1 1 4 1 0
1 1 4 5 1 0
1 1 4 5 1 4 0
它的 “流向” 示意图如下:
每个红色的圈圈表示一个线头的开始。用眼睛瞪这个图,结合脑子想,发现:
- 0一定是一条斜线向下
- 每一个 “线头” 一定比它上面的那个位置 严格 大(因为它要顶掉上面那个位置)
然后再一想,发现:我们可以把每个(0以外的)线头和它上面那个位置所在的线头连一条边。这样一定是颗树(因为“严格大于”这个东西不可能有环,并且显然连通)。我们考虑以 为根。
这颗树会有很多个叉,但是它的点权值在树上外向递增(即,父亲小于儿子)
考虑每个“线头”被插入进来的时间,我们发现这玩意很显然也递增。
那我们给树上的每个点两个权值,一个表示插入时间,一个表示填的数字。我们发现,如果知道了每个点的插入时间,也知道它的父亲,那我们其实就能唯一确定它去顶掉哪个点,再结合填的数字...我们就能唯一确定一个方案!(即,一个序列组)
于是变成了数树问题。
考虑到俩权值要递增,我们设 表示, 个点的树,根节点权值是 ,方案数。答案是 。
考虑根的所有子树,发现每个子树的根的权值都 ,并且一共有 个点。这是一个森林计数。
而我们发现“森林计数”和“树计数”可以互相转移!不妨换一下状态,设:
-
表示 个点的森林,每个树的根的权值都 ,方案数
-
表示 个点的树,根的权值是 ,方案数。
首先,。我们用后缀和优化掉这个东西。
然后 。
这个很明显,我们选出前面 个是森林,然后剩下的 个单独构成一颗树,就行了。
是因为我们要给它分配一个 “插入时间”。前面的 这样合并不会算重是因为,虽然它会算重树结构,但是此时的插入时间那一维不会重。
注意到这样的 是至少两棵树的。我们给它加上 ,就把独木成林的情况考虑到了。
然后就这样转移一波即可,答案是
AGC16F
好家伙,刚数完树,开始数DAG了
先拿SG函数做一波转化,再考虑反面,相当于要数 的方案数。
然后我们考虑按 的值把图分层。
考虑把 中 的那些点抽出来,发现:其余每个点到它们至少一条边,并且这些点内部不能有边。
抽出来之后,把它们删掉,发现剩下的点里面,整体 都 ,满足同样的性质,即,最优子结构。
于是考虑dp。设 表示 中最小的两个元素 值相同的情况, 表示全集,认为是 组成的集合。然后答案就是
注意到 不能把 分开,否则这个dp状态就不可用。然后枚举 的子集 ,记 表示 ,即 去掉 的那一部分。
那么 中每个点到 至少一条边 (和上题挺像,少了度数限制), 到 的边可以瞎连,不影响。
内部的边 全部不能连 ,而 内部的连边通过 得到。
那就是
设 表示 有多少出边到 。可得转移函数:
表示瞎连方案数,
表示连至少一条边的方案数,把 的转移变成 就行了,很明显 就可以去掉不连的方案。
这两个都可以每次预处理出 之后,扫一遍算出来。
于是总复杂度是 。
小结
我们可以先研究一波问题的性质,把问题转化成好做的形式,或者是提炼出我们真正需要知道的东西,再用dp做。
(接下来就开始飞了)
dp+复杂度平衡: loj547
首先考虑一个naive dp:
设 表示长度为 ,满足条件的01串个数。那么: 。
这玩意有两种搞法:
- 注意到 65537 是个 NTT 模数,对于 的数据(我实现的不太精细,如果精细实现可以做到 ),可以跑一个常系数齐次线性递推,
- 当 的时候,注意到 非常小。我们给这个递推赋予组合意义: 到 有 条重边, 到 有 条重边,求方案数。我们枚举跳了多少条 的边,计算出跳了多少条 的边,然后用组合数瞎jr算一下方案数加起来就行;复杂度 。
综合两种做法,就可以过了。
注意一个细节:做法1的递推里面,边界是:当 时,;。
而 是我们手动设置的,如果按做法2直接跑,得到的是 。容易发现,只有这一位不对。
如何把它修正回来?(修正主义)
设做法2跑一个 得到的方案是 ,也就是在DAG上从 跑到 的方案数。
一个很显然的想法是,我们直接跑这个计数,相当于是给 加了 之后做上面的递推。不合法的部分,就是这个 增加 之后的影响。考虑组合意义,这个影响就相当于在 DAG 上从 走到 的路径数。由于这个DAG的边只和点的相对位置有关,所以这个方案数就是 。
所以我们得到 。
注意到 ,所以这个式子也等于 。
不过我没想明白它的组合意义。评论区大佬也可以讲一下,如何形象一点的理解 这件事情。
dp+压缩自动机: CF506E
众所周知,一些dp可以用自动机的语言写出来,转化成自动机上的数路径问题
对于一个自动机,众所周知,我们可以压缩它
有以下几种策略:
- 转移边有很多重合部分,把它压起来,如SAM
- 有用的状态数很少,如PAM (本质不同回文串 O(n) 个)
- 根据问题特点,改写自动机,并按照上面两个方法压缩,如,本题
先写一个 naive-dp。设 , 表示串的总长,即 。
表示长度为 的回文串,使得 是它子序列的方案数。
每次考虑在串的两边加字符,并考虑它是 ,还是其它。然后讨论一波:
边界:
- 如果 ,
- 如果 ,
- 如果 ,
转移:
- 如果 ,
- 如果 ,
然后 就是答案了。
把状态 里面的那个 看成是在走步(每次走两步,分奇偶讨论下就行),并把 建点。
如果 ,我们称其为“结束点”,记作 类;如果 ,称其为“相同点”,记作 类;否则称为 “不同点”,记作 类。
我们把点排成方阵。 构成一个上三角, 都在左下角。
对角线上一定都是 。
一个 点,它可以向左下角连一条边;一个 点,它可以向正左/正右连一条边。
每个点上都有自环。根据dp方程, 点有 条自环, 点有 条自环, 点有 条自环。
我们从 开始走,走到 点结束,可以走边,可以走自环,走 步,问方案数。
暴力矩乘,,过不去。
我们考虑压缩这些点构成的自动机。注意到我们的 点只需要两条对角线,,,因为我们一定会走到这两条线之一就停止。
根据这个,我们走一条路下来, (长度) 肯定会从 变化到 。注意到,走一个 长度 ,走一个 长度 。设我们走了 个 , 个 ,那么
也就是说,确定了 就可以确定 。
然后我们注意到,一条路径的贡献,只和 有关,贡献是 ,和经过的顺序并没有关系。
值相同的路径可能有很多条,但是我们现在知道,本质不同只要O(m)条
设 表示经过了 个 的路径条数。然后我们的自动机就只需要关心 的数量了。
注意到我们怎么也得经过一个 ,那么 的值域是 , 的值域是
我们建出这样的一个自动机:点 表示经过 个 对应的状态, 同理。
为了让它俩能 “拼起来”,我们考虑把它俩对接,如下:
每个 的下面再挂一个 表示结束。
每个 点有 个自环, 点有 个自环, 点有 个自环。
这样,枚举 ,计算出 , 到 的路径就和原图上的 本质不同路径 一一对应了。
走一步相当于长度 。
如果 是偶数,把这个图的邻接矩阵搞出来求个 次幂,统计一下答案就行了。
如果 是奇数,稍微复杂些。
我们令最后一步的长度只 。那我们会走 步。
不合法,当且仅当从一个 的 类点转移到 。因为它要做最后一步,但它需要加两个相同字符,而我们强制让最后一步只加一个字符,就不能真正的匹配上。
我们把这种情况减掉即可。手动推一推,发现这种情况下有 。而且我们只能恰好走上 点,而不能在上面绕自环。
这就等价于我们走 步,走到一个 类点。
所以我们就把那个矩阵的 次幂算出来,枚举 ,计算得 ,求 到 的路径数量的和,就是不合法的方案数。
不要忘记乘个 。
容斥原理
每个的容斥背后,都有一个默默支持它的反演
常见的容斥,容斥系数是正负/正负+组合数的,本质是二项式反演
容斥系数带 的,本质是莫比乌斯反演,本质的本质是在质因数的集合上二项式反演, 是其容斥系数。
二项式反演,可以搞出来一堆集合的交/并,或者把“恰好”转换成“至少”
如,CTS2019 随机立方体
容斥也可以结合dp,通过dp记录容斥的贡献来做
如,CTS2019 氪金手游
由于这两题都属于概率期望,所以这里不讲(雾)
其实概率期望的本质也是数数
只不过我懒得再写一遍了qaq
去翻 “概率期望” 那篇罢
注,莫比乌斯容斥和这个东西的关系不大,所以这里略
naive题:51nod 1829
考虑直接瞎jr射,方案数是 ,但肯定会有不射满的 (戴黄看黄)
然后我们考虑容斥,容易想象到,我们应该这样做:
射在 个数内 - 射在 个数内 + 射在 个数内...
这样的容斥显然是对的。想象一下正负号,得到:答案为
可以用这个题来热热身。
如果您对容斥比较熟悉,不用动笔,用嘴巴就可以搞出这个题了。
反之,如果您用嘴巴切了这个题,说明您的容斥很强!
ZJOI2016 小星星
同样涉及到一个映射的问题,考虑和上个题类似的做法:我们先不保证射满,然后容斥。
先枚举一个集合 ,并限制我们必须射在这里面。然后做树形dp: 表示, 射到 , 子树射的方案数。
枚举 射到了 ,那在原图上 和 之间就要有边。如果有,就把这个方案加到 里面。
最后 的和就是 的答案,容斥一下加起来即可。
noiac2201 连续子序列: dp出容斥的贡献
(noi.ac的题有权限,这里给出题意)
称一个排列的子串为顺子,当且仅当:这个子串里两两之间差的绝对值为 (即,连续单增/单减)
求 个数的排列中,有多少种排列满足它所有顺子的长度都 。模 。
。
考虑反面,我们枚举它有多少个长度为 的顺子(枚举左端点),其余位置任意选,设这个方案为 。那么答案为
然后我们发现,两个顺子如果有交,那它们必须同时递增/递减,然后我们就可以把它合并成一个大块的顺子,称为一 “块”
对于一个块,它里面选多少个长度为 的顺子,我们其实并不知道。但是我们只关心它们的方案数乘上容斥系数的和(因为这玩意就是答案)
对于块之外的东西,每个位置都是独立的数,称为 “单点”
假设我们有 个块, 个单点,可以先安排出它们的相对顺序, 种方案,然后就可以唯一确定它们里面填什么数了。
对于一个块,它可以增或者减,那就还有 种分配方法。
所以它们仅填数字的方案(即,不考虑结构和容斥系数)就是 种。
设 表示前面 个数,选了 个块, 个单点,容斥贡献和。
如果第 个位置是单点,那么 贡献到
如果第 个位置属于一个块,那就枚举一个 表示上一块的结尾, 贡献过来。
发现这样并不对,因为我们不能直接用 贡献,因为 可能会有单点出来。
那我们再搞一个 ,表示强制令 为一个块的结尾,的方案数。
那
考虑 的转移:
- 如果当前选的顺子不和以前的重叠,贡献和是 。由于多选了一个,容斥贡献就乘一个 。
- 反之,就和以往的某个顺子重叠了。那以前的顺子,它的右端点要在 里面,并且选了这个顺子之后,容斥系数乘 ,而块数不变,因为重叠了
可得:。后面的 表示求和(这样写直观些)
然后我们可以用前缀和优化掉这个东西,复杂度 。
你开开心心的准备去写,一看数据五千。
然后你发现这个转移其实和 的关系比较大,注意到每次 这个东西只会变化一个 或者不变。
但我们也不能只记一个 ,因为 还要贡献一个
那我们为什么不把这个 的贡献也搞进来呢?那就只需要记 了。
我们记 , 同理。
我们把 的sigma里面的 用上面的转移化开,整理,把它们也搞成 的形式。得到:
为啥有一个 ?注意到那个 变成 的过程中, 这个东西要变化的。
然后注意到空间开不下,考虑滚动数组。
接下来是一个 典 中 典: 如何滚 ?
注意到 这一维不太好滚。
所以我们滚 。
这里 是完整的事件(
组合意义转化
很多东西具有一个组合意义
比如上面提到 的那个递推,它就可以看成DAG上的路径计数。
同时, 这个东西,也具有很好的意义: 个数,选 次,选一个数之后不删除,方案数就是 。
管道取珠
对于第 种序列, 相当于:取两次珠子,方案相同,且均为第 种序列的方案。
那么我们取两次珠子,方案相同,方案数就是
接下来就好做了,注意到数据范围非常小,设 表示:第一次取 的 个, 的 个第二次取 的 个, 的 个,相同,的方案数。
注意到 ,故省去 ,然后滚一下就行了。
AGC13E
和上一题类似。一段长度的平方,就相当于在这一段里面选两个位置的方案数。
而把这些平方乘起来,就相当于是在前 个里面选 个格子的方案数,根据乘法原理。
先不考虑障碍。设 表示在前 个位置里面分段之后选,最后一段里面选了 个,的方案数。
第一种是,在 这里划开一段,然后 这个位置把当前的 个格子全部选掉。这种方案是 ,前提是这一个位置不是障碍。
第二种是, 和 在一段,考虑前面选了几个,然后 这个位置上选剩下的(可能为 )。枚举一个 ,这样的方案数是 。
于是,
对于没被障碍隔开的一段,用矩阵优化转移。障碍特判一下就行。
复杂度 。
noiac 2448,2327
这两题的共同点是,需要求:长度为 的序列,每个数在 间。设 表示最长连续相同段,求 的分布(即,对于每个 ,求 的方案数)
注意到,恰好不太好做,但是“不超过”感觉挺能做。
那我们现在要求最长连续相同段不超过 的方案,设为 。
考虑 所在的那一段多长,有转移:。
注意一个细节:我们认为每一段都只能取 种,实际上第一段是可以取 种的。
最后乘一个 才是真正的答案。
前缀和优化,设 为 的前缀和。然后 。
移项,得
然后和上面的一个转化类似:把它看成DAG上的路径计数。
枚举跳了多少次 ,复杂度 。枚举 ,这样算,复杂度是调和的。
然后就得到了 。取差分得到 。然后发现 的差分就是 分布了。
复杂度 。
奇技淫巧
有这样一个式子,合法方案书=瞎选方案数*正确率。
假设我们能知道正确率,那问题就简单了。
noiac 2034
注意到 的方案可以和 拼起来的方案一一对应。
现在问题变成一个序列:知道它有 个 , 个 (和为 ,保证是正的),每个位置的前缀和都是正的,求方案数。设 为序列长度。
考虑一个序列的所有循环同构,我们发现,它一共有 个循环同构,而其中的恰好 个满足前缀和都是正的。
假设这个结论对,那就太好做了,答案是 。这就是 “瞎选方案数”乘以“正确率”。
证明如下
对于一个序列,我们把它写无限遍。
设 表示,前缀和最后一次为 的位置。由于序列和为正,每过一个周期和就会增,所以这个 的值是有限的。
对于 ,考虑 这一段区间。根据 的定义, 后面所有前缀和都 。
那这么说,这一段区间的前缀和都严格的大于 。 考虑区间内部的前缀和,设 ,则内部前缀和就是 的区间和,即 。我们知道 ,所以这个和大于 。
那我们把这一段区间取出来,它的每个位置前缀和都 ,是这个序列的一个循环同构。
所以我们有至少 个循环同构,使得它们任意位置前缀和
也容易发现,对于 ,它取出来的这段序列和 是本质相同的,所以不能算。
所以我们只有 恰好 个循环同构是满足条件的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】