计数类 dp 做题记录(长期更新)
前言
因为本人太弱,急需锻炼思维,固从现在起开始着手写计数题,并写下题解分析思路的欠缺。另外本文将长时间更新,所以我准备把它置顶,尽量日更!
upd on 24.11.6
现版本改成长期更新。
P3643 [APIO2016] 划艇 2024.8.28
简要题意
现在有 \(n\) 个区间,每个区间范围为 \([l_i,r_i]\)。现在有 \(n\) 个元素需要赋值,每个元素的值要么为零,要么在给定的区间内。对于一个值非零的元素 \(a_i\),需要满足它的数值严格大于所有标号比它小的元素,即 \(a_i\ge\max_{1\le j<i}\{a_j\}\)。求方案数。
数据范围:\(n\le500,1\le l_i\le r_i\le10^9\)。
题解
首先去想题目性质,然后很高兴地发现根本没有什么性质。然后先考虑朴素 dp,我们令 \(f_{i,j}\) 表示第 \(i\) 个元素值为 \(j\) 的方案数,最后答案为 \(\sum_{i=1}^{i\le n}\sum_{j}f_{i,j}\)。
然后考虑转移,其实转移也很暴力我就直接放式子了:
为方便转移,初始 \(f_{0,1}=1\)。
然后不用多说这个肯定爆了。第二维值域是 \(10^9\) 所以能够想到将区间离散化,然后第二维改成区间。这样转移就需要小小的改变一下,因为涉及到了区间选若干点,所以需要加一个系数。那么系数我们怎么求呢?
假设当前区间长度为 \(len\),元素为第 \(i\) 个。枚举到前 \(j\) 个元素时,现在有 \(i-j\) 个元素将会放在当前区间,我们就把问题抽象成有 \(1\) 到 \(len\) 一共 \(len\) 个数,我们需要从中选出最多 \(m\) 个,选择的方案数就是转化时乘上的系数。考虑对于一次选择,我可以选或不选,若我选就会从中选取一个数就正常做;但如果我不选呢?就把它看成我选了零。于是我们就可以往序列中加入 \(m\) 个零,现在有一共 \(len+m\) 个数,我要从中选出 \(m\) 个,方案为 \({len+m}\choose m\)。然而因为 dp 状态钦定第 \(i\) 个数必选,所以我们实际往序列中加入的零的个数应该会比上述操作少一个(因为保证至少有一个数也就是 \(i\) 不为零)。于是最后的 dp 转移就变成了下面的:
但是现在总复杂度还是 \(O(n^4)\) 的,还需要一个小优化,然后考虑哪些状态可以一起考虑。我们可以发现,对于当前的状态 \(f_{i,j}\),我只要满足一个状态 \(f_{k,l}\) 的第二维小于 \(j\) 也就是 \(l<j\),就可以将所有的 \(f_{k,l},l<j\) 累加然后整体乘上一个组合数,所以记一个 \(g_{i,j}=\sum_{k=1}^{j-1}f_{i,k}\),然后就得到了最后的转移式:
时间复杂度 \(O(n^3)\) 不做过多解释。
代码
点击查看代码
int n, l[N], r[N], z[N << 1], tot;
ll f[N], c[N], inv[N], ans;
ll add(ll x, ll y){
x += y; return x >= p ? x - p : x;
}
signed main(){
// fileio(fil);
n = rd();
for(int i = 1; i <= n; ++i){
z[i - 1 << 1 | 1] = l[i] = rd(), z[i << 1] = r[i] = rd() + 1;
}
sort(z + 1, z + 1 + (n << 1));
tot = unique(z + 1, z + 1 + (n << 1)) - z - 1;
for(int i = 1; i <= n; ++i){
l[i] = lower_bound(z + 1, z + 1 + tot, l[i]) - z;
r[i] = lower_bound(z + 1, z + 1 + tot, r[i]) - z;
}
inv[1] = f[0] = c[0] = 1;
for(int i = 2; i <= n; ++i)inv[i] = 1ll * (p - p / i) * inv[p % i] % p;
for(int i = 1; i < tot; ++i){
int len = z[i + 1] - z[i];
for(int j = 1; j <= n; ++j)c[j] = c[j - 1] * (len + j - 1) % p * inv[j] % p;
for(int j = n; j; --j)if(l[j] <= i and i + 1 <= r[j]){
ll s = 0; int cnt = 1;
for(int k = j - 1; ~ k; --k){
s = add(s, c[cnt] * f[k] % p);
cnt += l[k] <= i and i + 1 <= r[k];
}
f[j] = add(f[j], s);
}
}
for(int i = 1; i <= n; ++i)ans += f[i];
printf("%lld", ans % p);
return 0;
}
小结
其实做完这道题时感觉完全不够紫题,但是在看题解之前怎么都切不了。其实暴力 dp 我肯定会,离散化我想到了,后面的组合数也很基础,最后的前缀和相对于其他优化也简单得多。但是,为什么我就是做不出来呢?因为我不熟悉知识间的组合与衔接,不肯从暴力入手,老是想怎么直接出正解,而真正的正解需要前面大量的铺垫。它或许是 OIer 做题时的妙手偶得,但更是大量的经验与积累!
而对于我来说,我拿到一道题应该去做什么?我首先要去分析题目的性质,然后根据性质看看能不能得出进一步结论。有了以上的东西,我就可以去根据已有的东西思考如何得出答案,这一期间可以先将时间复杂度暂放。最后再来慢慢优化求解的过程,方法。还有不要忘了验证正确性!
AGC30 - D - Inversion Sum 2024.8.29
简要题意
有一个长度为 \(n\) 的序列,现在有 \(q\) 次操作,每次操作都可以交换两个数的位置,对于每次操作可以选择执行或不执行。对于所有的情况求出一共的逆序对数量。
题解
首先要知道一个技巧:在多种情况下计数可以转换成求概率再乘上情况数。
然后就可以将题意转换成求 \(a_i>a_j,i<j\) 的概率和,于是就可以设计一个 dp \(f_{i,j}\) 表示 \(a_i>a_j,i<j\) 的概率。当一次操作交换 \(x,y\) 时,对于所有的 \(f_{x,i},f_{y,i},f_{i,x},f_{i,y}\) 都会改变。就拿 \(f_{x,i}\) 举例,一次操作后它有 \(\frac{1}{2}\) 的概率继承之前的状态,还有 \(\frac{1}{2}\) 的概率变成 \(f_{y,i}\) 的状态,所以转移就是两个状态相加除以二。对于每次操作都需要改变 \(O(n)\) 的状态,时间复杂度 \(O(nq)\)。
代码
点击查看代码
int n, q, a[N];
ll f[N][N];
ll qmi(ll x, int y){
ll res = 1;
for(; y; y >>= 1, x = x * x % p)if(y & 1)res = res * x % p;
return res;
}
const ll i2 = p + 1 >> 1;
ll add(ll x, ll y){
x += y; return x >= p ? x - p : x;
}
signed main(){
// fileio(fil);
n = rd(), q = rd();
for(int i = 1; i <= n; ++i)a[i] = rd();
for(int i = 1; i <= n; ++i)for(int j = 1; j <= n; ++j)f[i][j] = a[i] > a[j];
for(int i = 1; i <= q; ++i){
int x = rd(), y = rd();
for(int j = 1; j <= n; ++j)if(j ^ x and j ^ y)f[x][j] = f[y][j] = add(f[x][j], f[y][j]) * i2 % p, f[j][x] = f[j][y] = add(f[j][x], f[j][y]) * i2 % p;
f[x][y] = f[y][x] = add(f[x][y], f[y][x]) * i2 % p;
}
ll s = qmi(2, q), res = 0;
for(int i = 1; i < n; ++i)for(int j = i + 1; j <= n; ++j)res = add(res, f[i][j]);
printf("%lld", res * s % p);
return 0;
}
draw 2024.8.30
简要题意
有一个 \(4\times N\) 的木板需要粉刷,第 \(i\) 行 \(j\) 列的颜色记为 \(A(i,j)\)。 有 256 种颜色,记为 \(0\dots255\),为了使得粉刷比较好看,粉刷需要满足如下。
要求:
- \(A(x,y)>=A(x,y-1)\);
- 有一些指定的 \((x1,y1)\) 和 \((x2,y2)\) 满足 \(A(x1,y1)=A(x2,y2)\);
请问有多少种满足要求的粉刷方式?
数据范围
\(1\le n\le15,0\le M\le100\)
题解
先考虑题目性质:
- 根据要求的第一条,我们可以知道对于一个合法的木板,每一行没有影响,而且每一行的数从小到大单调不减。
- 题目中的数据范围超级小。
首先如果你单纯想用三维甚至二维 dp 就解决问题可能比较麻烦,既然数据范围很小,我们可以考虑高维 dp 的做法。其实我们可以很自然的想到一个爆炸的 dp,我们考虑 \(f_{i1, c1, i2, c2, i3, c3, i4, c4}\) 表示每一行填到某一位以及当前位置的颜色它的方案数。然后可以把这个东西抽象成有四个完全背包,这个状态相当于把四个背包放在一起考虑。对于每个背包,有无数个标号为 \(0\dots255\) 的物品,代价为 1,价值为方案数。然后类似背包直接转移。
然后 dp 爆炸了。考虑优化。其实你会发现上面的 dp 我们没有任何的限制,导致它很混乱,会有很多的状态,导致我们复杂度爆炸。所以我们需要合并一些能够合并的状态。对于上面的状态我们能够确定一点就是每一行的位置是一定要单独维护的,这一点毋庸置疑。但是每一个位置的颜色可以改变,如果没有限制直接记录下来会产生很多状态,所以我们可以每次把颜色统一起来。现在我们换一种 dp 状态,考虑 \(f_{col,i1,i2,i3,i4}\) 表示当前要填的颜色(数)是 \(col\),我们每一行填到某个位置时的方案数,然后转移就和上面的一样,可以类比一下。
至于要求某的点颜色相等,就是在转移的时候判断一下当前的状态是否合法,也就是对于对应行的填的位置必须都在有限制的位置的同一侧,这个可以预处理一下。最后时间复杂度就是 \(O(256n^4)\)。
代码
点击查看代码
const int N = 20, p = 1e5, M = 105;
int n, m, o[N][N][N][N], xx[M], yy[M], _x[M], _y[M], ii[5];
int f[N][N][N][N];
signed main(){
freopen("draw.in", "r", stdin);
freopen("draw.out", "w", stdout);
n = rd(), m = rd();
for(int i = 1; i <= m; ++i)xx[i] = rd(), yy[i] = rd(), _x[i] = rd(), _y[i] = rd();
for(int i = 1; i <= m; ++i)for(ii[1] = 0; ii[1] <= n; ++ii[1])for(ii[2] = 0; ii[2] <= n; ++ii[2])
for(ii[3] = 0; ii[3] <= n; ++ii[3])for(ii[4] = 0; ii[4] <= n; ++ii[4])o[ii[1]][ii[2]][ii[3]][ii[4]] |= ii[xx[i]] >= yy[i] ^ ii[_x[i]] >= _y[i];
f[0][0][0][0] = 1;
for(int c = 0; c < 256; ++c){
for(int i = 1; i < 5; ++i)for(ii[1] = 0; ii[1] <= n; ++ii[1])for(ii[2] = 0; ii[2] <= n; ++ii[2])
for(ii[3] = 0; ii[3] <= n; ++ii[3])for(ii[4] = 0; ii[4] <= n; ++ii[4]){
int nkp = f[ii[1]][ii[2]][ii[3]][ii[4]];
if(++ii[i] <= n)(f[ii[1]][ii[2]][ii[3]][ii[4]] += nkp) %= p;
--ii[i];
}
for(ii[1] = 0; ii[1] <= n; ++ii[1])for(ii[2] = 0; ii[2] <= n; ++ii[2])for(ii[3] = 0; ii[3] <= n; ++ii[3])
for(ii[4] = 0; ii[4] <= n; ++ii[4])if(o[ii[1]][ii[2]][ii[3]][ii[4]])f[ii[1]][ii[2]][ii[3]][ii[4]] = 0;
}
printf("%05d", f[n][n][n][n]);
return 0;
}
小结&反思
我在考试的时候被硬控了很久,然后就是感觉思维没打开,不敢去想最开始的八维 dp,只限制于二维到三维,结果转移一直写不出来,但好像 max 有一种神秘做法,好像跟我的想法差不多,思路几乎一模一样,我只差最后一个地方的转移没有想清楚,但是考试时我没有笃定我的信念想下去,非常可惜!然后最近 hfu 也跟我聊过,他也提到了我的这个弱点,我的确应当及时反思,但是越在关键时候越要相信自己,我就没有这种冲劲,太过拘泥。我以后要注意不要给自己一些紧迫感、压抑感,要学会放松、学会顺着自己的想法,不要太在意他们的指点!
Road of the King 2024.9.6
简要题意
有一个 \(n\) 个点的图,目前一条边都没有。
有一个人在 \(1\) 号点要进行 \(m\) 次移动,终点不必是 \(1\) 号点,假设第 \(i\) 次从 \(u\) 移动到 \(v\),那么在 \(u\) 与 \(v\) 之间连一条有向边。
问有多少种序列能满足:最终 \(n\) 个点组成的图是一个强连通图。答案对 \(10^9+7\) 取模。
数据范围
\(1\le n,m\le 300\)
题解
对于这种连通图计数类问题,有一个常见的套路,就是你去考虑 \(1\) 号点的连通情况。就比如这一道题我们需要考虑现在一共走到过哪一些点,以及一号点所在的强连通分量大小。根据这个思路可以很容易的写出状态 \(f_{i,j,k}\) 表示走了 \(i\) 步,一共走到过 \(j\) 个点,其中一号点所在的强连通分量大小为 \(k\)。
我们可以发现一个性质,就是如果现在去走一个在一号点所在的强连通分量中的点,那么目前所有点都会变成一个强连通分量(显然)。所以状态的转移也就差不多出来了。
但是如果正常转移你会发现很难写,对于一个状态 \(f_{i,j,k}\) 有非常多的转移方法,但是从 \(f_{i,j,k}\) 转移到其他地方就要简单很多,外面可以分三种情况讨论:
- 下一步走之前没走过的点:\(f_{i+1,j+1,k}=f_{i,j,k}\times(n-j)\);
- 下一步走之前走过但是不在一号点所在强连通分量中的点:\(f_{i+1,j,k}=f_{i,j,k}\times(j-k)\);
- 下一步走一号点所在强连通分量中的点:\(f_{i+1,j,j}=f_{i,j,k}\times k\)。
最后答案为 \(f_{m,n,n}\),此题得解!
代码
点击查看代码
int n, m;
ll f[N][N][N];
int add(ll x, int y){
return x - p + y >= 0 ? x - p + y : x + y;
}
signed main(){
// fileio(fil);
n = rd(), m = rd();
f[0][1][1] = 1;
for(int i = 0; i < m; ++i)for(int j = 1; j <= n; ++j)for(int k = 1; k <= j; ++k){
f[i + 1][j + 1][k] = add(f[i + 1][j + 1][k], f[i][j][k] * (n - j) % p);
f[i + 1][j][k] = add(f[i + 1][j][k], f[i][j][k] * (j - k) % p);
f[i + 1][j][j] = add(f[i + 1][j][j], f[i][j][k] * k % p);
}
cout << f[m][n][n];
return 0;
}
P9823 [ICPC2020 Shanghai R] The Journey of Geor Autumn 2024.11.20
简要题意
给定 \(1\le k\le n\),我们规定满足下面性质的排列称为“好排列”:
\(\forall k<i\le n,a_i>\min_{i-k\le j\le i-1}\{a_j\}\)
求好排列的个数对 \(998244353\) 取模。
数据范围:\(1\le n\le 10^7\)。
题解
看到这题首先有一个一眼的 \(O(n^2)\) 暴力,我们设 \(f_{i,j}\) 表示填到第 \(i\) 个位置,已经填的最大的数为 \(j\) 的方案数。然后考虑这一位怎么填。如果不填 \(j\),就是 \(f_{i,j}\leftarrow f_{i-1,j}\times(j-i+1)\);如果填 \(j\),就是 \(f_{i,j}=\sum_{l=j-k}f_{i-1,l}\)。然后考虑第二个转移是一段区间,可以记录一个前缀和优化成 \(O(n^2)\)。
但是这个题数据范围卡的很死,显然是让你寻找线性做法。然后你会发现其实不管你怎么变化,只要第一维记录下标它就一定还需要记录别的东西。
引用大佬 \(\text{max0810}\) 的话来说就是:
枚举下标没前途
所以果断转换第一维的状态,像这种计数题中还可以记录填的数是什么,因为题目中限制条件是大于某个数,所以我们维护前缀最大值。我们可以理解成把 \(1\) 到 \(i\) 之间的数撒在这个排列中合法的方案数。这样就可以做了!
我们设 \(f_i\) 表示填了前 \(i\) 个数的方案数。考虑假设我们已经填了前 \(x\) 个数,下一个填什么?显然可以是 \(x+1\) 到 \(x+k\)。假设我们在里面选择了一个数 \(y\),那么对于 \(x+1\) 到 \(y-1\) 这些数的填法就没有了要求,对于这些数填入剩下位置的方案数就是 \(A_{n-x-1}^{y-x-1}\)。
然后我们的转移也就呼之欲出了:
这样虽然状态是 \(O(n)\) 的,但是转移还是带了一个 \(k\)。于是考虑换一种写法,我们从前面的转移到后面,于是有:
然后把后面的系数拆开:
于是我们就记录 \(f_j\times(n-j-1)!\) 的前缀和即可。
代码
点击查看代码
const int N = 1e7 + 5, p = 998244353;
int n, k;
ll f[N], g[N], fac[N], inv[N];
ll qmi(ll x, int y){
ll res = 1;
for(; y; y >>= 1, x = x * x % p)if(y & 1)res = res * x % p;
return res;
}
int main(){
freopen("b.in", "r", stdin);
freopen("b.out", "w", stdout);
n = rd(), k = rd(); fac[0] = inv[0] = 1;
for(int i = 1; i <= n; ++i)fac[i] = fac[i - 1] * i % p;
inv[n] = qmi(fac[n], p - 2);
for(int i = n - 1; i; --i)inv[i] = inv[i + 1] * (i + 1) % p;
if(k == 1)return puts("1") & 0;
for(int i = 1; i <= k; ++i)f[i] = fac[n - 1] * inv[n - i] % p;
for(int i = 1; i <= n; ++i){
if(i <= k)f[i] = (f[i] + g[i - 1] * inv[n - i]) % p;
else f[i] = (f[i] + (g[i - 1] + p - g[i - k - 1]) * inv[n - i]) % p;
g[i] = (g[i - 1] + f[i] * fac[n - i - 1]) % p;
}
wt(f[n]);
return 0;
}
[ARC178D]Delete Range Mex 2024.11.20
简要题意
对于一个排列 \(A\),每次可以选择一个区间 \([l,r]\) 删去当前排列中的 \(\operatorname{mex}(A_l,A_{l+1}\dots A_r)\) 这个数,可以操作任意次。现在给定一个序列 \(B\),求长度为 \(n\) 的排列 \(A\) 能够通过若干次操作变成 \(B\) 的 \(A\) 的数量对 \(998244353\) 取模。
数据范围:\(1\le m\le n\le500,0\le B_i<n\)。
题解
对于这道题目我们先去寻找它有什么性质。考虑我们如何删掉一个数 \(x\)?我们需要选择一段区间,满足 \(x\) 不在里面但是小于 \(x\) 的数全在。那么这有什么特殊的呢?我们这样考虑。
加入我们现在要把 \(0\) 到 \(n-1\) 从小到大插入序列,如果我们后面要删除 \(x\),我们在插入 \(x\) 时就需要把它放在当前序列的最左边或者最右边某个位置(如下图),也就是说当前 \(x\) 可以插入的位置范围是 \([1,lpos)\cup(rpos,n]\)。

回到此题。现在给出了最后剩下的 \(m\) 个数,而我们需要把删去的数填入这 \(m+1\) 个空中。于是就有 \(f_{l,r,x}\) 表示插入到了 \(x\),且区间 \([l,r]\) 内不能再填其他数的方案数。
枚举 \(x\),对于 \(x\),若 \(x\) 没有被删除,说明它的位置已经确定,我们可以先将其记录下来。转移时就只能转移到旁边的区间:\(f_{\min\{pos_x,l\},\max\{pos_x+1,r\},x}\leftarrow f_{l,r,x-1}\)。因为 \(pos_x\) 不能动。
若 \(x\) 要被删掉,就大概枚举一下 \(x\) 的位置,是一个标准的区间 \(DP\)。当计算到区间 \([l,r]\) 时,\(f_{l,r,x}\) 的转移需要从子区间来,转移式子就是:
解释一下转移。对于一个区间 \([l,r]\) 不能填数,说明最后填 \(x\) 时肯定填在了一个端点。如果填在 \(r\),子区间就只能在 \([l,pos]\) 中选择,如果填在 \(l\) 同理。然后可以前缀和优化一下,最后时间复杂度 \(O(nm^2)\)。
代码
点击查看代码
const int N = 505, p = 998244353;
int n, m, a[N], pos[N], f[N][N][N];
int add(int x, int y){return x - p + y >= 0 ? x - p + y : x + y;}
int main(){
freopen("c.in", "r", stdin);
freopen("c.out", "w", stdout);
n = rd(), m = rd();
for(int i = 1; i <= m; ++i)a[i] = rd(), pos[a[i]] = i;
if(pos[0])f[pos[0]][pos[0] + 1][0] = 1;
else for(int i = 1; i <= m + 1; ++i)f[i][i][0] = 1;
for(int x = 1; x < n; ++x)if(pos[x])
for(int i = 1; i <= m + 1; ++i)for(int j = i; j <= m + 1; ++j)
f[min(pos[x], i)][max(pos[x] + 1, j)][x] = add(f[min(pos[x], i)][max(pos[x] + 1, j)][x], f[i][j][x - 1]);
else{
for(int i = 1, tmp = 0; i <= m + 1; ++i, tmp = 0)for(int j = i; j <= m + 1; ++j)
tmp = add(tmp, f[i][j][x - 1]), f[i][j][x] = add(f[i][j][x], tmp);
for(int j = 1, tmp = 0; j <= m + 1; ++j, tmp = 0)for(int i = j; i; --i)
tmp = add(tmp, f[i][j][x - 1]), f[i][j][x] = add(f[i][j][x], tmp);
}
wt(f[1][m + 1][n - 1]);
return 0;
}
ARC132E 2024.12.12
简要题意
有 \(n\) 个方块,每个方块有一个初始状态可能为左右或者空。每次操作随机选择一个空进行操作。每次操作可以向左或者向右走一直到下一个空或者走出边界,走到的每个格子会变成左或者右,这取决于移动方向。
求无法操作时方格为左的期望数。
数据范围:\(n\le 10^5\)。
题解
首先看到这个操作我们可以把每一段方格看成一个整体,以空为分界线,然后最自然的思路就是对于每一个整体考虑对答案的贡献。
然后大概说一下我的想法。就是对于第 \(i\) 个连续段考虑它被更新过和没被更新过两种状态下的贡献。
- 对于没被更新的情况,需要满足对于它左边的空都向左走,右边的空都向右走,贡献就是原始区间中左方格的个数。
- 对于被更新的情况,如果最后被左边向右的覆盖则无贡献,否则就贡献区间长度。
然后经过一番推导你会发现,第二种情况求解的复杂度似乎是 \(O(n)\) 的,然后就炸了。所以我们需要优化一下思路。
我们可以从最终状态入手。
考虑最终的方格状态一定是左边一段左方格,右边一段右方格,然后中间的一段是原始状态。原因显然。
所以我们只用讨论最后哪一段没有被覆盖过,直接枚举是 \(O(n)\) 的。然后对于每一段,它左边的格子都有贡献,它本身的贡献是它原始贡献,它的右边没有贡献。于是我们只用算出它的系数就能求解问题了。
于是我们可以设计一个状态 \(f_n\) 表示让 \(n\) 个连续段都向左且不影响右边放个的状态的概率,考虑从 \(f_{n-1}\) 转移。因为只有最后一个空选择向右走才会不合法,于是就把第 \(n-1\) 的选择方法解放了,所以转移就是 \(f_n=f_{n-1}\times(1-\frac{1}{2n-2})\)。
然后对于枚举段的右边和左边其实是等价的,所以这段的答案就是 \(f_{left}\times f_{right}\times(\operatorname{cnt}_{left}+\operatorname{original}_i)\)。
其中 \(\operatorname{cnt}\) 是前缀个数,\(\operatorname{original}\) 是当前段原始左方格数。
代码
点击查看代码
signed main(){
for(int i = 1; i <= n; ++i)
if(a[i] == '<')++cnt;
else if(a[i] == '.')s[++tot] = i, c[tot] = cnt;
c[++tot] = cnt; f[1] = 1;
for(int i = 2; i <= tot; ++i)f[i] = 1ll * f[i - 1] * (p + 1 - qmi(2 * i - 2, p - 2)) % p;
for(int i = 1; i <= tot; ++i)ans = (0ll + ans + 1ll * f[i] * f[tot - i + 1] % p * (c[i] - c[i - 1] + s[i - 1])) % p;
}
ARC101E 2024.12.25
简要题意
有一棵树,树上有偶数个节点。你需要给这些点两两配对,一组已经配对的点会将两点之间的树边进行一次覆盖。一组合法方案需要满足树上所有边都被覆盖至少一次,求合法方案数。
数据范围:\(n\le5000\)。
思路
首先我们去观察题目性质,发现没有什么特殊的地方。我最开始只想到一个非常暴力的 \(dp\),设 \(f_{u,i}\) 表示以 \(u\) 为根的子树内有 \(i\) 个点已经匹配好的方案数。但是当我去考虑转移时,我发现他有很多种情况:
- \(u\) 一个儿子的子树内互相匹配,但是需要有一个点与外面的点匹配(不然这个子树与 \(u\) 之间的边就无法被覆盖);
- 一个子树内的点向 \(u\) 的其他子树匹配;
- 一个子树的点向 \(u\) 子树以外的点匹配。
或许还有一些没有罗列出来,但反正就是不可做。于是我们正难则反,考虑先求出不合法的情况,然后容斥做。
题解
如何求不合法的情况呢?我们可以通过钦定一些边不覆盖来容斥。比如当我计算到以 \(u\) 为根的子树时,我就去枚举 \(u\) 所在的连通块的大小,对于一个 \(u\) 的儿子 \(v\),分讨一下连通块是否包括 \(v\)。
具体的,我们设 \(f_{u,i}\) 表示以 \(u\) 为根的子树,\(u\) 所在连通块大小为 \(i\) 的方案数。对于 \(v\) 在连通块的时候,有转移:
若 \(v\) 与 \(u\) 之间的边不覆盖,则有:
你乍一看这不就是树上背包吗?时间复杂度 \(O(n^2)\),可以通过此题!
现在我们已经基本找出状态转移的方程,但现在我们还需要思考一个问题:
一个点数为 \(k\) 的连通块,将里面的点不重不漏两两匹配的方案数
首先对于 \(k\) 为奇数的时候是无贡献的;所以只用考虑 \(k\) 为偶数的情况。考虑递推求解答案,设 \(s_k\) 表示点数为 \(k\) 的贡献。对于一个点,我有 \(k - 1\) 种选择方案,而剩下的 \(k-2\) 个点的方案是 \(s_{k-2}\),固可得递推式:\(s_{k}=(k-1)\times s_{k-2}\)。
但考虑到我们只是没有考虑这些方案中会有的不合法情况,所以需要稍微容斥一下,在转移的时候还需要给一个 \((-1)^k\)。
然后看到之前的 \(dp\),我们发现对于第一种情况合并两个连通块似乎不好计算方案,于是我们改写状态,设 \(f_{u,i}\) 表示以 \(u\) 为根的子树,\(u\) 所在连通块大小为 \(i\) 时不考虑 \(u\) 所在连通块中匹配情况的方案数,这样在合并两个连通块时我们就直接把系数乘上就行,所以最后第一种情况的转移式为:
最后答案就是 \(\sum\limits_{i}f_{1, i}\times s_i\)。
代码
点击查看代码
void dfs(int u, int fa){
sz[u] = f[u][1] = 1;
for(int i = hd[u]; i; i = e[i].nxt){
int v = e[i].to; if(v == fa)continue;
dfs(v, u); copy(f[u], f[u] + 1 + sz[u], g);
fill(f[u], f[u] + 1 + sz[u], 0);
for(int j = 1; j <= sz[u]; ++j)for(int k = 1; k <= sz[v]; ++k)
f[u][j] = del(f[u][j], mul(mul(f[v][k], s[k]), g[j])),
f[u][j + k] = add(f[u][j + k], mul(g[j], f[v][k]));
sz[u] += sz[v];
}
}
P3343地震后的幻想乡 2025.2.12
简要题意
给定一张 \(n\) 个点 \(m\) 条边的图,边的边权是 \([0, 1]\) 之间均匀分布的随机实数,且相互独立。求最小生成树的最大边权的期望值。
思路
首先有一个比较神秘的跟概率有关的东西,虽然题面中已经给出提示,但这里还是进行简单说明:
引理:将长度为 \(n\) 的区间随机划成 \(m\) 段,每段长度期望是 \(n\over m\)。
笔者询问 deepseek,deepseek 给出了三种证明方法,在此仅给出笔者知道的一种。
我们设第 \(i\) 段长度为 \(L_i\),就有:\(n=\sum\limits_{i=1}^mL_i\)。
根据期望的线性性和对称性我们可以得到:\(n=E(\sum\limits_{i=1}^mL_i)=\sum\limits_{i=1}^mE(L_i)\) 和 \(L_1=L_2=\dots=L_m\)。
所以 \(E(L_i)={n\over m}\)。
所以第 \(k\) 大的边贡献就是 \(k\over m+1\)。我们首先能够有一个 naive 的想法,枚举每个边的大小关系,然后暴力跑 kruskal。这个想法可以启发我们去思考如何统计所有第 \(k\) 大的边的贡献。
首先看每种情况做贡献的概率,确定了选择哪些边要选和哪个边做贡献后就好做了,有 \(P={(k-1)!(m-k)!\over m!}\)。因为期望等于总方案数除以概率,所以现在我们只需要去找方案数,这个就 dp 去求。
因为恰好选第 \(k\) 大的边使图连通不好描述,所以容斥,变成选第 \(k\) 大的边前图不连通的方案数减去选第 \(k\) 大的边后图不连通的方案数。于是就有一个 dp,设 \(f_{S,i}\) 表示 \(S\) 构成的点集中选 \(i\) 条边图不连通的方案,现在考虑枚举子集进行转移。转移就从若干已经连通的子图中选出剩下的一些边,但是不能使整个图连通,所以从严格意义上说是枚举真子集。所以引入 \(g_{S,i}\) 表示 \(S\) 构成的点集中选 \(i\) 条边图连通的方案,就有下面两个转移式:
其中 \(d_S\) 为 \(S\) 的导出子图。
最后的答案就是:
化简得:
代码
点击查看代码
signed main(){
n = rd(), m = rd();
for(int i = 1, u, v; i <= m; ++i)u = rd(), v = rd(), ++mp[(1 << u - 1) | (1 << v - 1)];
for(int S = 1; S < (1 << n); ++S)for(int T = S; T; T = T - 1 & S)d[S] += mp[T];
c[0][0] = c[1][0] = 1;
for(int i = 1; i <= m; c[++i][0] = 1)for(int j = 1; j <= i; ++j)c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
for(int S = 1; S < (1 << n); ++S)for(int i = 0; i <= d[S]; ++i){
for(int T = S & S - 1; T; T = T - 1 & S)if(T & (S & - S))
for(int j = 0; j <= min(i, d[T]); ++j)f[S][i] += g[T][j] * c[d[S ^ T]][i - j];
g[S][i] = 1.0 * c[d[S]][i] - f[S][i];
}
for(int i = 0; i <= m; ++i)ans += f[(1 << n) - 1][i] / c[m][i];
ans /= 1.0 + m;
printf("%.6f", ans);
return 0;
}
CF1874F 2025.3.24
简要题意
给一个序列 \(M,m_i\in[1,n]\),统计满足 \(\forall i\le j\le m_i,p[i,j]\neq[i,j]\) 的排列 \(p\) 数量。
数据范围:\(1\le n\le200\)。
题解
首先我们会感觉到直接统计这种东西很难做,所以会想到容斥转化题意,于是变成求 \(i\le j\le m_i,p[i,j]=[i,j]\) 的数量。为了方便下面直接把容斥系数丢进 dp 中。我们设 \(f_{l,r}\) 表示 \([l,r]\) 中填完整个区间满足条件的序列数量,枚举钦定的序列长度,然后就有:
其中 \(g_{l,r,x}\) 表示 \([l,r]\) 中还有 \(x\) 个位置未考虑、只统计钦定序列的方案数。因为钦定了 \(r-l+1-x\) 个位置,剩下的地方就没有限制,所以直接乘上阶乘,然后转移要注意一下容斥系数。现在考虑 \(g\) 如何转移。
对于 \(g_{l,r,x}\) 我们依次考虑每一位填的数。如果我们不准备钦定第 \(i\) 位,那么可以直接从 \(g_{l,i-1,x-1}\) 转移。如果钦定了第 \(i\) 位就从 \(g_{l,i-1,x}\) 转移。然后考虑合并 \(g\) 的信息。对于两个钦定的序列 \([l1,r1]\) 和 \([l2,r2]\) 如果有 \(l1\le l2\le r1\le r2\),那么这两个序列等价于 \([l1,l2)\)、\([l2,r1]\) 和 \((r1,r2]\)。容易发现:如果拆开这些区间,容斥系数会取反,但是最后结果不会变,因为两个 \([l2,r1]\) 系数相反抵消了。也就是说这些钦定的序列之间的所有关系都可看做是不交或包含的关系,这些关系使区间构成树形结构。
这启发我们可以类似树形 dp 地合并,只不过呈现出来的是区间 dp 的形式。为方便转移,我们将 \([l,r]\) 拆成 \([l,pos]\) 和 \((pos,r]\),并且把未考虑的元素统一丢到一边的区间转移即可不重不漏。就有:
时间复杂度 \(O(n^4)\)。
代码
点击查看代码
signed main(){
n = rd(); fac[0] = 1; for(int i = 1; i <= n; ++i)rp[i] = rd(), fac[i] = Mul(fac[i - 1], i), g[i][i - 1][0] = 1;
for(int l = n; l; --l)for(int r = l; r <= n; ++r){
for(int x = 1; x <= r - l + 1; ++x)g[l][r][x] = g[l][r - 1][x - 1];
for(int x = 0; x < r - l + 1; ++x)for(int pos = l + max(0, x - 1); pos < r; ++pos)g[l][r][x] = Add(g[l][r][x], Mul(g[l][pos][x], f[pos + 1][r]));
if(r <= rp[l])for(int x = 0; x <= r - l + 1; ++x)f[l][r] = Sub(f[l][r], Mul(fac[x], g[l][r][x]));
g[l][r][0] = Add(g[l][r][0], f[l][r]);
}
int ans = 0;
for(int i = 0; i <= n; ++i)ans = Add(ans, Mul(fac[i], g[1][n][i]));
cout << ans;
return 0;
}
「FAOI-R5」波特检测 2025.4.8
简要题意
有一个序列 \(A,A_i\in[0,2^k)\),其中 \(A_i\) 在值域内随机取值。你需要给出一个 01 序列 \(H\),满足不存在 \(i\neq j,A_i\oplus A_{i-1}=A_j\oplus A_{j-1},H_{i}=H_{i-1}=H_j=H_{j-1}=0\),求任意给出一个 \(H\) 合法的概率。
题解
首先可以发现如果对序列做一次差分没有任何影响,于是将条件改成 \(A_i=A_j\),所以如果 \(H\) 中有连续的 0 就需要满足 \(A\) 不同,否则就随便填,我们可以设 \(f_i\) 表示序列中有 i 个连续两个 0 的方案数,于是答案就为:
考虑 dp 求 \(f_i\)。设 \(g_{i,j,0/1}\) 表示填了前 \(i\) 个位置,有 \(j\) 个连续的 0 且第 \(i\) 个位置填 0/1 的方案数。简单 dp 有:
但是这样是 \(O(n^2)\) 的,然后这里有一个 trick:看到数对相乘就往向量和矩阵上面想。于是考虑生成函数,设 \(G_{i,0/1}=\sum\limits_jg_{i,j,0/1}x^j\),然后把转移写成矩阵形式:
这个时候直接 NTT 套矩阵快速幂是 \(O(n\log^2n)\),显然爆炸,于是考虑优化。因为是生成函数,所以我们不关心 \(x\) 的取值。若我们通过 \(O(n)\) 次矩阵快速幂求出若干点值然后再 \(O(n\log n)\) 快速插值就可以保证复杂度。\(x\) 的选取和 NTT 原理相同,我们直接取单位根即可。
代码
点击查看代码
signed main(){
n = rd(), k = rd();
lim_init(n << 1); init(lim); rev_init();
rt = qmi(3, (p - 1) / lim);
for(int i = 0, cur = 1; i < lim; ++i, cur = Mul(cur, rt)){
mat A(2, 2), B(2, 1); A[0][0] = cur, A[0][1] = A[1][0] = A[1][1] = 1;
B[0][0] = B[1][0] = 1;
A = qmi(A, n - 1) * B; f[i] = Add(A[0][0], A[1][0]);
}
// cout << lim << endl;
// for(int i = 0; i < lim; ++i)printf("F[%d] = %d\n", i, f[i]);
NTT(f, 0); for(int i = 0; i < lim; ++i)f[i] = Mul(f[i], inum[lim]);
// cout << lim << endl;
// for(int i = 0; i < lim; ++i)printf("F[%d] = %d\n", i, f[i]);
for(int i = 0, s = 1, cur = qmi(2, k); i < n; ++i, s = Mul(s, cur--))
ans = Add(ans, Mul(f[i], Mul(s, qmi(qmi(2, k), n - i))));
cout << Mul(ans, Inv(qmi(qmi(2, k), n)));
return 0;
}
Piling Papers G 2025.5.7
简要题意
给定长度为 \(n(1≤n≤300)\) 的整数序列 \(a(1≤a_i≤9)\),和整数区间 \([A,B](1≤A≤B≤10 ^{18} )\),有 \(q\) 次询问,每次询问给出 \(l,r\)。每次询问开始,你有一个空串 \(x\),你可以正序对 \(a_{l∼r}\) 进行操作,操作有三种:\(x→\overline{x+a_i}\),\(x→\overline{a_i+x}\),或者什么也不做,求 \(x\) 的取值在 \([A,B]\) 的不同的操作方案数,对 \(10^9+7\) 取模。
题解
首先可以转化成两个 \(\le x\) 的限制相减。下文 \(m\) 表示限制的位数。
看到数据范围可以想到先将每个 \((l,r)\) 的答案做出来。发现不好向左边拓展,于是对于一个固定的 \(l\),我们去求所有的 \(r\)。现在你能想到一个最简单的 dp,设 \(f_{l,r}\) 表示区间 \([l,r]\) 的答案,但是显然这没法转移。所以我们考虑增加限制。
这里有个“妙手”,就是我们直接记录当前的一段和限制的一段之间的关系。具体的,就是多加入一点状态,设 \(f_{l,r,i,j,0/1/2}\) 表示区间 \([l,r]\) 得到的长度为 \(j-i+1\) 数和限制中第 \(i\) 位到第 \(j\) 位的关系是小于/等于/大于的方案数。此时状态的复杂度是 \(O(m^2n^2)\),转移是 \(O(1)\) 的,可以接受。
转移时就每次向右拓展一个位置,枚举一段限制,然后分类讨论填数后区间与限制的关系。比如我要把拓展的数填在后面,我就去看填数前区间和限制的关系。如果之前区间不等于限制那么填数后关系不会改变,因为比较数的大小是从高到低比较;但是如果关系是相等,我们就需要看填的这一位了。如果是填在前面也同理。
为了方便大家理解,这里写一下在填在后面的转移式:
其中 \(t\) 的值由 \(a_{r+1}\) 和限制的第 \(j+1\) 位决定;填在前面同理。
最后答案是:
因为如果位数没有限制多肯定可行。
代码
点击查看代码
inline int cmp(int x, int y){return x == y ? 2 : x > y;}
void init(ll x){
m = 0; while(x)b[++m] = x % 10, x /= 10;
reverse(b + 1, b + 1 + m);
}
void sol(ll x, bool o){
init(x);
for(int l = 1; l <= n; ++l){
for(int i = 0; i < 20; ++i)for(int j = 0; j < 20; ++j)f[i][j][0] = f[i][j][1] = f[i][j][2] = 0;
for(int r = l; r <= n; ++r){
for(int i = 1; i <= m; ++i)for(int j = m; j > i; --j){
if(a[r] == b[i])for(int k = 0; k < 3; ++k)upd(f[i][j][k], f[i + 1][j][k]);
else for(int k = 0; k < 3; ++k)upd(f[i][j][a[r] > b[i]], f[i + 1][j][k]);
upd(f[i][j][0], f[i][j - 1][0]); upd(f[i][j][1], f[i][j - 1][1]);
upd(f[i][j][cmp(a[r], b[j])], f[i][j - 1][2]);
}
for(int i = 1; i <= m; ++i)upd(f[i][i][cmp(a[r], b[i])], 2);
for(int i = 1; i <= m; ++i){
upd(ans[l][r][o], f[i][m][0]);
upd(ans[l][r][o], f[i][m][2]);
if(i > 1)upd(ans[l][r][o], f[i][m][1]);
}
}
}
}
喜爱之钥 2025.6.26
简要题意
有 \(n\) 个人,\(L\) 把锁和 \(L+K\) 把钥匙,其中有 \(K\) 把是假的。现在这些人轮流且按照最优策略去尝试配对锁和钥匙,求每个人成功解锁的期望次数分别是多少。
题解
注意到每个人均按照最优决策,所以在决策时我们需要去选择成功概率最大的。假设现在有 \(x\) 个锁,\(x+y\) 个钥匙。对于第一个人成功的概率为 \({x\over x(x+y)}={1\over x+y}\)。我们称第一个人选择的锁和钥匙为一号锁、钥匙,现在去考虑第二个人以及后面人的情况:
- 选择一号锁和非一号钥匙。因为我们排除了一号钥匙所以还剩下 \(x+y-1\) 个钥匙,故成功的概率为 \(1\over x+y-1\)。
- 选择非一号锁和一号钥匙。成功的概率也为 \(1\over x+y-1\)。接下来考虑证明。
设 \(A,B,C\) 分别为第一个人拿到的钥匙为真的,第一个人失败,第二个人成功,下面有:
\[\begin{aligned} P(A|B)&={P(B|A)P(A)\over P(B)}\\&={{x-1\over x}\cdot{x\over x+y}\over{x+y-1\over x+y}}={x-1\over x+y-1}\\ P(C|B)&=P(A|B)P(C|AB)+P(\overline A|B)P(C|\overline AB)\\&={x-1\over x+y-1}\cdot{1\over x-1}+0={1\over x+y-1} \end{aligned} \]证毕。
- 选择非一号锁和非一号钥匙。成功的概率为 \((1-{1\over x+y-1}){1\over x+y-1}={x+y-2\over (x+y-1)^2}<{1\over x+y-1}\),所以不进行此种决策。
所以第二个人会选择与第一个人相同的锁或钥匙,并且选择锁或者钥匙的概率相同,所以第二个人会有 \({x-1\over 2x+y-2}\) 的概率选择锁,有 \(x+y-1\over 2x+y-2\) 的概率选择钥匙。并且如果第二个人选了锁,接下来的人就会一直选相同的锁直到锁开;否则就是一直选相同的钥匙直到发现其是假钥匙或者开了一把锁。因为这样他们的成功率比没有任何信息去瞎蒙的情况成功概率更高。并且我们可以计算第二个人成功的概率:\((1-{1\over x+y})\cdot{1\over x+y-1}={1\over x+y}\),和第一个人相同,以此类推可得到后面的成功概率同样为 \({1\over x+y}\)。
也就是说除了第一个人外,我们对于相同的 \(x,y\) 都进行本质相同的操作(选相同的锁/钥匙),并且成功概率相同。这个问题我们尝试 dp 解决。设 \(f_{i,j,c}\) 表示现有 \(i\) 个锁 \(i+j\) 个钥匙,轮到了第 \(c\) 个人期望成功次数。
首先肯定从 \(x=1,y=0\) 的情况开始倒推(废话?)。我们进行一点小分讨进行转移:
-
选择的是能够配对的锁或钥匙,有 \(f_{i,j,c}\leftarrow f_{i-1,j,c-l}\)。首先注意 \(c-l\) 是在模 \(n\) 意义下的,因为这 \(n\) 个人是轮着来的。然后我们要将锁和钥匙稍微分开处理,首先是因为选择相同锁或钥匙的概率不同,其次就是注意枚举的 \(l\) 的上界也不同。具体的,对于锁来说,因为最多就 \(i\) 把所以上界为 \(i\);对于钥匙上界就可以到 \(i+j\)。转移式就是:\(f_{i,j,c} = {\left((i-1)\sum f_{i-1,j,c-l}\right)+\left((i+j-1)\sum f_{i-1,j,c-l}\right)\over (i+j)(2i+j-2) }\)。
-
注意如果只选钥匙的时候我们还可以有 \(f_{i,j,c}\leftarrow f_{i,j-1,c-i}\),因为我们如果要发现这个钥匙是假的需要让 \(n\) 个人轮 \(i\) 次,每次选相同的这个假钥匙,所以有 \(f_{i,j,c}={j\over i+j}\cdot{i-1\over 2i+j-2}\cdot f_{i,j-1,c-i}\)。因为每次要选锁,所以还要乘选择锁的系数,然后那个 \({j\over i+j}\) 是选到假钥匙的概率。
-
最后还要考虑的是这一次本身成功的贡献。考虑是固定一个相同的锁还是钥匙,直接拿成功的概率乘上每次选择的概率即可。
直接做时间复杂度 \(O(n^2LK)\),记得对于第一种情况进行前缀和优化,最后时间复杂度 \(O(nLK)\)。
代码
点击查看代码
const int p = 1e9 + 7, len = 1e5;
int n, l, k;
int f[5005][55][105], s[5005][55][105];
inline int Add(int x, int y){return (x += y) >= p ? x - p : x;}
inline int Sub(int x, int y){return x < y ? x - y + p : x - y;}
inline int Mul(int x, int y){return 1ll * x * y % p;}
inline int Inv(int x, int y = p - 2){int s = 1; for(; y; y >>= 1, x = Mul(x, x))if(y & 1)s = Mul(s, x); return s;}
inline int calc(int i, int j, int k){return Add(Mul(k / n, s[i][j][n]), s[i][j][k % n]);}
inline int F(int i, int j, int a, int b){return Sub(calc(i, j, b + len * n), calc(i, j, a - 1 + len * n));}
signed main(){
for(int T = rd(); T--; ){
n = rd(), l = rd(), k = rd();
for(int i = 1; i <= l; ++i)for(int j = 0; j <= k; ++j){
int t1 = Mul(i + j - 1, Inv(2 * i + j - 2)), t2 = Sub(1, t1), q = Inv(i + j);
t1 = Mul(t1, q); t2 = Mul(t2, q);
for(int c = 1; c <= n; ++c){
int tmp = Add(Mul(t1, F(i - 1, j, c - i - j, c - 1)), Mul(t2, F(i - 1, j, c - i, c - 1)));
if(j)tmp = Add(tmp, Mul(Mul(j, t2), f[i][j - 1][(c - i + i * n - 1) % n + 1]));
f[i][j][c] = Add(tmp, Add(Mul(t1, (i + j - c + n) / n), Mul(t2, (i - c + n) / n)));
s[i][j][c] = Add(s[i][j][c - 1], f[i][j][c]);
}
}
for(int i = 1; i <= n; ++i)printf("%d%c", f[l][k][i], " \n"[i == n]);
}
return 0;
}
I'm Here 2025.6.26
简要题意
一棵树,每个点有编号,你每天可以做出一些选择,若选择一个点 \(u\),则不能选择 \(u\) 子树内的点,并且第 \(i\) 次选择的点编号需要小于等于 \(n-i+1\),最后必须选 1 号点。求长度为 \(i,i\in[1,n]\) 的合法选择序列个数。
题解
小清新困难题。首先这玩意就是内向树拓扑序上抓一些点,但是要多一个限制,就是第 \(i\) 个位置需要小于等于 \(n-i+1\)。首先我们假设没有这个多的限制其实就是在树上跑一遍背包即可,做法显然。现在考虑限制二,这里有个妙手,就是我们考虑后缀最大值的限制一定是比限制二严格的,所以我们在 dp 中加一维去维护后缀最大,于是就有了 dp。设 \(f_{i,j,k}\) 表示从大到小考虑到了 \(i\),填了 \(j\) 个数,且最后这个数填在序列的位置 \(k\) 上。注意第三维的限制是强制填在一个地方,这样这个已有的序列中间是会有很多空位置的,这个用来给后面更小的数腾位置用。然后转移是简单的:
- \(i\) 没被选,直接继承 \(f_{i+1}\) 的东西即可。
- \(i\) 被选为后缀最大值,我们去枚举最后填在了哪里,注意枚举的范围是 \((k,n-i+1]\),于是有 \(f_{i,j,l}\leftarrow f_{i+1,j-1,k}\)。
- \(i\) 没有被选为后缀最大值,我们就需要考虑以 \(i\) 为根的子树怎么填。我们预处理出树上背包 \(g_{u,i}\) 表示在以 \(u\) 为根的子树中填 \(i\) 个的合法方案数。于是就可以转移了:\(f_{i,j+x,k}\leftarrow f_{i+sz_i,j,k}\times g_{i,x}\times{k-j\choose x}\)。
最后注意一下边界即可,复时间杂度 \(O(n^4)\)。
代码
点击查看代码
int n, hd[N], cnt;
struct edge{int nxt, to;}e[N << 1];
inline void add(int u, int v){e[++cnt] = {hd[u], v}; hd[u] = cnt;}
int f[N][N][N], g[N][N], fac[N], ifac[N], sz[N];
int h[N][N], q[N];
inline int C(int n, int m){return Mul(Mul(fac[n], ifac[m]), ifac[n - m]);}
void dfs(int u){
sz[u] = 0; h[u][0] = 1;
for(int z = hd[u], v; z; z = e[z].nxt){
dfs(v = e[z].to); for(int i = 0; i <= sz[u] + sz[v]; ++i)q[i] = h[u][i], h[u][i] = 0;
for(int i = 0; i <= sz[u]; ++i)for(int j = 0; j <= sz[v]; ++j)upd(h[u][i + j], Mul(q[i], Mul(h[v][j], C(i + j, i))));
sz[u] += sz[v];
}
++sz[u]; for(int i = sz[u]; i; --i)upd(h[u][i], g[u][i] = h[u][i - 1]);
}
const string FileName = "";
signed main(){
// fileio(FileName);
fac[0] = ifac[0] = 1; for(int i = 1; i < N; ++i)fac[i] = Mul(fac[i - 1], i);
ifac[N - 1] = Inv(fac[N - 1]); for(int i = N - 2; i; --i)ifac[i] = Mul(ifac[i + 1], i + 1);
for(int T = 1/*rd()*/; T--; cnt = 0){
n = rd(); for(int i = 1, u, v; i < n; ++i)u = rd(), v = rd(), add(min(u, v), max(u, v));
dfs(1); f[n + 1][0][0] = 1;
for(int i = n, tmp; i; --i){
for(int j = 0; j <= n - i; ++j)for(int k = j; k <= n - i; ++k){
tmp = f[i + 1][j][k]; if(i > 1)upd(f[i][j][k], tmp);
for(int l = k + 1; l <= n - i + 1; ++l)upd(f[i][j + 1][l], tmp);
}
for(int j = 0; j <= n - i - sz[i] + 1; ++j)for(int k = j; k <= n - i - sz[i] + 1; ++k)
for(int x = 1; x <= min(sz[i], k - j); ++x)upd(f[i][j + x][k], Mul(Mul(C(k - j, x), g[i][x]), f[i + sz[i]][j][k]));
}
for(int i = 1; i <= n; ++i)wt(f[1][i][i]), pc(' '), hd[i] = 0; pc('\n');
}
return 0;
}
[集训队互测 2024] 基础 ABC 练习题 2025.8.18
题意
给定两个集合 \(S_1,S_2\),定义一个长度为 \(3n\) 且仅包含 ABC 三种字符的串 \(s\) 是好的,当且仅当存在一种方案将 \(s\) 划分成 \(n\) 个长度为 \(3\) 的子序列,且这 \(n\) 个子序列都是 ABC,BCA 或 CAB。设 \(n\) 个子序列中 ABC 的个数为 \(x\),BCA 的个数为 \(y\),还要求 \(x\in S_1,y\in S_2\)。
现在有一个长度为 \(3n\) 的字符串 \(s\),字符串中仅包含 ABC? 四种字符,你需要计算将所有 ? 都分别替换成 ABC 三个字符中的某一个的方案,使得串 \(s\) 是好的。
题解
神仙题,弱化版是 AGC055D。
首先观察数据范围,发现 \(n\) 很小于是考虑高维 dp 或者 状压 dp。在 dp 前我们需要去寻找一些性质,考虑怎么样的序列是合法的?我们尝试用一些必要条件去刻画它,这里引用某选手说过的一句话:必要条件多了就可以变成充要条件。
我们可以观察三个不同的子序列:ABC,BCA,CAB。因为这些子序列都有相同的字母但是这些字母的位置不同,所以我们尝试去研究不同字母之间的位置关系。能够发现:只有在 ABC 中,A 才会出现在 C 的前面,否则其在 C 的后面。对于其他两对字母也存在类似的关系。设 \(pre_a,pre_b,pre_c\) 分别表示前缀 A,B,C 的个数,我们可以发现对于任意前缀都有:\(pre_a-pre_c\le cnt_{ABC}\)。其他字母同理。
由此我们得到一个神秘的条件:
现在如果我能够通过上述条件构造出合法的解,那么这个条件就为充要条件,于是我们尝试构造一个合法的序列。首先对于一个序列,如果其合法,那么我们将下标平移不会影响其合法性。比如我们把最开始的字母移到最后,可以发现:这个字母所在的子序列进行了一次轮换,其余子序列不变。所以新的序列一定是合法的。于是我们考虑将序列连成一个环,下面给出构造方案:对于环上第 \(i\) 个 A,对应环上第 \(i+cnt_{BCA}\) 个 B 和环上第 \(i+cnt_{BCA}+cnt_{CAB}\) 个 C。
于是现在我们需要证明的是:对于任意的 \(i\),环上第 \(i+cnt_{BCA}+cnt_{CAB}\) 个 C 不在环上第 \(i\) 个 A 和环上第 \(i+cnt_{BCA}\) 个 B 之间。
证明是容易的。我们考虑从第 \(i\) 个 A 处断环成链,于是我们需要证明的是原第 \(i+cnt_{BCA}+cnt_{CAB}\) 个 C 的位置小等于 \(3n\)。考虑到原第 \(i+cnt_{BCA}+cnt_{CAB}\) 个 C 在新的序列中处于第 \(1+cnt_{BCA}+cnt_{CAB}\) 个 C,考虑最坏的情况是所有 C 都在 A,B 后面,因为有 \(cnt_{ABC}+cnt_{BCA}+cnt_{CAB}=n\) 并且因为新的序列开头是 A 所以 \(cnt_{ABC}>0\),进一步得到 \(1+cnt_{BCA}+cnt_{CAB}\le n\),于是得证。
对于弱化版,当 \(n\le15\) 时,我们可以直接 \(\mathcal O(n^6)\) dp 直接记录所有信息即可。具体的,设 \(f_{i,j,k,a,b,c}\) 维护前缀 A,B,C 的数量以及 \(cnt_{ABC},cnt_{BCA},cnt_{CAB}\) 然后 \(\mathcal O(1)\) 转移。
考虑 \(n\le 60\) 时,我们尝试干掉一维状态。因为有 \(cnt_{ABC}+cnt_{BCA}+cnt_{CAB}=n\) 所以我们可以枚举 \(cnt_{ABC}\) 和 \(cnt_{BCA}\) 从而确定 \(cnt_{CAB}\),但是考虑到直接做等于 \(cnt\) 的限制不太好 dp,于是我们改成做 \(\le cnt\) 的限制最后差分一下即可,时间复杂度 \(\mathcal O(n^5)\)。
代码
点击查看代码
/*
* @Author: Nekopedia
* @Date: 2025-07-15 18:58:24
* @Last Modified by: Nekopedia
* @Last Modified time: 2025-07-15 19:08:19
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 70;
int n;
unsigned f[N][N][N], ans;
char s[N << 2], ss[2][N << 2];
inline unsigned calc(int a, int b, int c){
if(min(a, b) < 0)return 0; memset(f, 0, sizeof f); f[0][0][0] = 1;
for(int i = 0; i <= n; ++i)for(int j = 0; j <= min(n, i + b); ++j)
for(int k = max(0, i - a); k <= min(n, j + c); ++k)if(i + j + k != 3 * n){
int pos = i + j + k;
if(s[pos] == '?' or s[pos] == 'A')f[i + 1][j][k] += f[i][j][k];
if(s[pos] == '?' or s[pos] == 'B')f[i][j + 1][k] += f[i][j][k];
if(s[pos] == '?' or s[pos] == 'C')f[i][j][k + 1] += f[i][j][k];
}
return f[n][n][n];
}
void sol(){
cin >> n >> ss[0] >> ss[1] >> s; ans = 0;
if(n != 60)return cout << - 1 << endl, void();
for(int a = 0; a <= n; ++a)for(int b = 0; a + b <= n; ++b){
int x = a, y = b; while(x <= n and ss[0][x] == '0')++x;
while(y <= n and ss[1][y] == '0')++y; int c = n - x - y;
if(c < 0)continue; ans += calc(a, b, c) - calc(a - 1, b, c) - calc(a, b - 1, c) + calc(a - 1, b - 1, c);
}cout << ans << endl;
}
signed main(){
ios :: sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int T, id; cin >> T >> id; while(T--)sol(); return 0;
}
催眠术 2025.11.19
前言
excat 给我推荐的题,好难!
题意
给定 \(n,m,V\),还有一个长为 \(m\) 的值域在 \([1,V]\) 中的整数序列 \(a\),再给定一个大小为 \(n \times (m+1)\) 的矩阵 \(c\)。定义一个整数序列是好的,当且仅当它的值域在 \([1,V]\) 中且所有值域在 \([1,V]\) 的长为 \(m\) 的整数序列都是它的子序列。定义一个好的整数序列 \(b\) 的价值为 \(\prod\limits_{i=1}^n c_{i,pre_i}\),其中 \(pre_i\) 为 \(a\) 的最长前缀长度使得 \(a_{1 \sim pre_i}\) 是 \(b_{1\sim i}\) 的一个子序列,若不存在则 \(pre_i = 0\)。求所有长度为 \(n\) 的好序列的价值和,答案对 \(10^9+7\) 取模。
题解
首先考虑一个序列是好的的条件,经过手玩可以发现貌似 \(mV\le n\) 就能构造出满足条件的序列,否则一定不行。考虑证明:
对于长 \(m\) 的序列,其每个位置 \(i\) 的取值为 \([1,V]\)。我们考虑构造一个长为 \(mV\) 的序列,其中每 \(V\) 个数分成一段,一段中依次填 \(1\sim V\) 即可。
考虑序列长度小于 \(mV\) 的时候,一定会有一个数 \(i\) 的出现次数小于 \(m\),这个由抽屉原理可得。显然这个序列就不包含子序列 \(A',A'={i,i,i,\dots,i}\),于是得证。
于是我们考虑对于段进行 dp,可以设 \(f_{i,j,k,p, q}\) 表示 \(b\) 序列填到了 \(i\),在 \(a\) 序列中已经匹配上前缀 \(j\),现在填完了 \(k\) 段,当前正在填的段已经有 \(p\) 种数是属于匹配过的,还有 \(q\) 种是没有匹配的数。这里状态看似是 \(\mathcal O(nm^2V^2)\) 的,但是注意当 \(n<mV\) 的时候是不合法的,所以我们不考虑这些状态,最后剩下的状态是 \(\mathcal O(n^3)\)。
现在考虑 dp 转移,转移我们考虑两种,一种是往后多匹配一个数,还有一种是结束一个段。但是当你具体去写转移式子的时候会遇到问题,就是说有的地方进行转移的时候其系数我们无法确定,就是对于同一种状态它可能会有不同情况导致不同系数,所以我们需要区分。解决的办法是将不同的转移分开维护,于是我们重新定义一些辅助转移的数组,设 \(g_{i,j,l,p,q,0/1}\) 表示我们考虑往后匹配的转移时要求匹配 \(a_j\),并且是否存在某个数不能出现的限制。设 \(h_{i,j,k,p}\) 表示我们考虑结束一个段的转移时的 dp 值,设 \(z_{i,j,p}\) 表示我们已经完成所有匹配,并且已经有完整的 \(j\) 段,当前段出现了 \(p\) 种数。其他未定义的字母含义与 \(f\) 中含义相同。
此时我们就把 \(f\) 的转移拆成了四个 dp 互相转移,具体可以在枚举的时候依次处理 \(f,g,h,z\) 的转移。考虑 \(f\) 会贡献到 \(g,h\),如果需要对没有匹配的数确定顺序那么乘上对应系数即可,如果是 \(g\) 进行转移,考虑 \(g\) 能转移到 \(f,g,z\),\(g\) 自己的转移就是去讨论是否需要限制某个数不能出现,对于这个限制考虑一个数上次出现的位置与这次出现的位置之间是否已经间隔了 \(p\) 种数,这是可以预处理的。\(g\) 对于 \(f\) 也是同样可以直接贡献的。最后就是 \(g\) 可以把剩下需要匹配的数全部匹配完,然后就能够贡献到 \(z\),此时需要考虑是否新开一段,乘上对应系数即可。\(h\) 只能转移到 \(f,h\),同理分讨是否新开一段即可。\(z\) 不再赘述。
具体的式子其实是很简单的,可以自行尝试推理。这些转移都是 \(\mathcal O(1)\) 的,于是时间复杂度为 \(\mathcal O(n^3)\)。
代码
点击查看代码
inline int fd(int x, int y){return x * (v + 1) + y;}
inline pii get(int x){return make_pair(x / (v + 1), x % (v + 1));}
inline bool ok(int x, int y, int z){return id[x][y] <= z;}
signed main(){
n = rd(), m = rd(); v = rd();
if(m * v > n)return puts("0"), 0;
for(int i = 1; i <= m; ++i)a[i] = rd();
for(int i = 1; i <= n; ++i)
for(int j = 0; j <= m; ++j)c[i][j] = rd();
fac[0] = ifac[0] = pw[0] = s[n + 1] = 1;
for(int i = 1; i <= n; ++i)fac[i] = Mul(fac[i - 1], i);
for(int i = 1; i <= n; ++i)pw[i] = Mul(pw[i - 1], v);
ifac[n] = Inv(fac[n]);
for(int i = n - 1; i; --i)ifac[i] = Mul(ifac[i + 1], i + 1);
for(int i = n; i; --i)s[i] = Mul(s[i + 1], c[i][m]);
for(int i = 1; i <= v; ++i)id[0][i] = v + 1;
for(int i = 1; i <= n; ++i){
pos[i][1] = a[i]; for(int j = 1, k = 1; j <= v; ++j)
if(pos[i - 1][j] != a[i])pos[i][++k] = pos[i - 1][j];
for(int j = 1; j <= v; ++j)id[i][j] = v + 1;
for(int j = 1; j <= v; ++j)
if(pos[i][j])id[i][pos[i][j]] = j;
}
int res = 0, o = 0, nn = fd(m, v);
f[0][fd(0, 0)][fd(0, 0)] = 1;
for(int i = 1; i <= n; ++i, o ^= 1){
for(int x = 0; x <= nn; ++x)for(int y = 0; y <= nn; ++y){
auto p1 = get(x), p2 = get(y); int t = f[o][x][y]; f[o][x][y] = 0;
int j = p1.first, p = p1.second, k = p2.first, q = p2.second;
if(t){
if(ok(j, a[j + 1], p))
ADD(g[o][x][y][1], t), ADD(h[o][fd(j, p + q)][k], Mul(t, fac[v - p]));
else{
if(q)ADD(g[o][fd(j, p + 1)][fd(k, q - 1)][1], Mul(t, q));
ADD(h[o][fd(j, p + q)][k], Mul(Mul(t, q), fac[v - p - 1]));
ADD(g[o][fd(j, p + 1)][y][0], t);
}
}
for(int w = 0; w < 2; ++w)if(t = g[o][x][y][w], g[o][x][y][w] = 0, t){
if(p + q + 1 + w <= v)ADD(g[! o][x][fd(k, q + 1)][w], Mul(t, c[i][j]));
ADD(g[! o][x][y][w], Mul(t, Mul(c[i][j], p + q - 1)));
if(j + 1 < m)
if(p + q < v)ADD(f[! o][fd(j + 1, p)][y], Mul(t, c[i][j + 1]));
else ADD(f[! o][fd(j + 1, 0)][fd(k + 1, 0)], Mul(t, Mul(c[i][j + 1], fac[q])));
else
if(p + q < v)ADD(z[! o][fd(k, p + q)], Mul(t, Mul(c[i][j + 1], fac[v - p])));
else ADD(z[! o][fd(k + 1, 0)], Mul(Mul(t, c[i][j + 1]), Mul(fac[v - p], fac[k + 1 == m ? 0 : v])));
}
}
for(int x = 0; x <= nn; ++x)for(int k = 0; k <= m; ++k){
auto p = get(x); int j = p.first, q = p.second, t = h[o][x][k]; h[o][x][k] = 0;
if(t){
if(q + 1 < v)ADD(h[! o][fd(j, q + 1)][k], Mul(t, c[i][j]));
ADD(h[! o][x][k], Mul(t, Mul(c[i][j], q - 1)));
if(q + 1 == v)ADD(f[! o][fd(j, 0)][fd(k + 1, 0)], Mul(t, c[i][j]));
}
}
for(int x = 0; x <= nn; ++x){
auto p = get(x); int j = p.first, q = p.second, t = z[o][x];
if(! t)continue; z[o][x] = 0; if(j == m){
ADD(res, Mul(Mul(t, pw[n - i + 1]), s[i])); continue;
} ADD(z[! o][x], Mul(Mul(t, c[i][m]), q));
if(q + 1 < v)ADD(z[! o][fd(j, q + 1)], Mul(t, c[i][m]));
if(q + 1 == v)ADD(z[! o][fd(j + 1, 0)], Mul(Mul(t, c[i][m]), fac[j + 1 == m ? 0 : v]));
}
}
return wt(Add(res, z[o][fd(m, 0)])), 0;
}

浙公网安备 33010602011771号