Counting 练习
AT_arc193_c [ARC193C] Grid Coloring 3
对着其他题解想了两个上午加一个下午。
正着染色肯定是不好做的,我们考虑时光倒流。
现在问题变为有多少种网格,每次删颜色相同的一行一列,有多少种删除序列最后能删完。
不妨把删除操作看作用 \(0\) 号颜色覆盖这一行和一列,同时 \(0\) 号颜色可以看作是任意颜色。
进一步观察,第一次给一行和一列打上标记后,后面你删的每一行和每一列都至少有一行和一列是 \(0\) 号颜色和其配对形成十字形。
所以问题转化为第一次给一行和一列覆盖 \(0\) 号颜色后,后面任意选若干行或若干列进行染色操作。
我们称第一次给一行和一列的初始颜色为主色,为了不重不漏,第一次应该把所有的行为主色和列为主色的都覆盖 \(0\) 号颜色。为什么?考虑交换两个操作后都可以完成,所以会算重。
考虑怎么 \(\text{DP}\),有一种想法是设 \(f_{i,j,t}\) 表示删除 \(i\) 行 \(j\) 列,是否有限制某一行或某一列不能全部为某一种颜色,记为钦定色。初始的钦定色就是主色。
为了方便转移,我们钦定只对行操作,把这个矩阵旋转即得到对列操作。
发现这个状态是有点问题的,如果删除行和删除列会变得可以互换,导致同一个最终状态被多次计数。
为了避免上述重复,我们人为规定一种规范:当矩阵每一行内部同色时,我们只允许一种删除顺序,比如只允许先删行,而禁止先删列的情况。
于是我们多开一维状态 \(f_{i,j,t,l}\),其中 \(l\) 表示反转后矩阵中是否禁止所有列的颜色完全相同。这等价于反转前每一行的内部颜色是否相同。
枚举每次删除 \(r\) 行,有转移:
为什么是从 \(f_{j,i-r,0,1}\) 转移过来?
如果 \(t=1\),考虑删除 \(r\) 行不为钦定色的颜色,那下一次考虑列的时候就不需要考虑有一列全为钦定色的情况。
如果 \(t=0\),显然。
\(f_{i,j,t,1}\) 这个转移分为两种,\(r\) 行颜色不全部相同和 \(r\) 行颜色全部相同。
为什么从 \(f_{j,i-r,0,1}\) 转移过来?考虑这 \(r\) 行颜色不全部想等,所以后续如果有操作,无法通过交换这两次操作而使其他操作序列不变,所以我们对钦定色没有限制。
跟这个上面的解释反着来,后续如果有操作和此操作颜色相同,可以交换两个操作而使其他操作序列不变,所以我们要钦定后续不能用这次操作的颜色。
最后统计答案,枚举第一次删除 \(i\) 行和 \(j\) 列,考虑先对行做还是先对列做,贡献为
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const ll mod = 998244353;
const int kN = 410;
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
int n, m, c;
ll f[kN][kN][2][2], C[kN][kN], pw[2][kN];
ll DP(int n, int m, int t, int l){
if(f[n][m][t][l] != -1) return f[n][m][t][l];
ll res = pw[t][n];
if(l) res = (res + mod - (c - t)) % mod;
for(int r = 1; r < n; r++){
if(l == 0){
Add(res, DP(m, n - r, 0, 1) * pw[t][r] % mod * C[n][r] % mod);
}
else{
Add(res, DP(m, n - r, 0, 1) * ((pw[t][r] - (c - t) + mod) % mod) % mod * C[n][r] % mod);
Add(res, DP(m, n - r, 1, 1) * 1ll * (c - t) % mod * C[n][r] % mod);
}
}
return f[n][m][t][l] = res;
}
signed main(){
File();
cin >> n >> m >> c;
pw[0][0] = pw[1][0] = 1;
for(int i = 1; i <= max(n, m); i++) pw[0][i] = pw[0][i - 1] * 1ll * c % mod, pw[1][i] = pw[1][i - 1] * 1ll * (c - 1) % mod;
C[0][0] = 1;
for(int i = 1; i <= max(n, m); i++){
C[i][0] = 1;
for(int j = 1; j <= i; j++){
C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
}
}
memset(f, -1, sizeof(f));
ll ans = 1;
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
ll res = C[n][i] * C[m][j] % mod;
Add(ans, res * add(DP(n - i, m - j, 1, 0), DP(m - j, n - i, 1, 1)) % mod);
}
}
cout << ans * 1ll * c % mod << '\n';
return 0;
}
AT_arc184_d [ARC184D] Erase Balls 2D
直接数最终剩下的球的集合不好做,考虑数选择的球的集合。
有一个问题是,一个最终剩下的球的集合可能对应多个选择的球的集合,这时候我们应当强化选择的球的集合的限制,使其一一对应。
我们看题解发现如果选择的球的集合满足:若选择集合外的任意元素会导致最终剩下的球的集合改变,那这就能保证一一对应。这是显然的但是我没有想到。
如何判断,暴力做即可。
复杂度 \(\Theta(n^3)\)。
正如 \(\text{zlt}\) 所言,本题难点在于 转化计数对象 和 增加限制。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int kN = 303;
const ll mod = 998244353;
void File(){
freopen("1.in", "r", stdin);
}
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
ll f[kN];
struct node{
int x, y;
}a[kN], b[kN];
int n, pre[kN], suf[kN];
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
int m = n;
a[++m] = {0, n + 1}, a[++m] = {n + 1, 0};
sort(a + 1, a + m + 1, [&](node x, node y){return (x.x != y.x) ? x.x < y.x : x.y > y.y;});
f[1] = 1;
for(int i = 2; i <= m; i++){
for(int j = 1; j < i; j++){
if(a[j].y < a[i].y) continue;
int tot = 0;
for(int k = j + 1; k < i; k++){
if(a[j].y > a[k].y && a[k].y > a[i].y) b[++tot] = a[k];
}
pre[0] = n + 2, suf[tot + 1] = -1;
for(int k = 1; k <= tot; k++) pre[k] = min(pre[k - 1], b[k].y);
for(int k = tot; k >= 1; k--) suf[k] = max(suf[k + 1], b[k].y);
bool fg = 1;
for(int k = 1; k <= tot; k++){
if(pre[k - 1] > b[k].y && suf[k + 1] < b[k].y) fg = 0;
}
if(fg) Add(f[i], f[j]);
}
}
cout << f[m] << '\n';
return 0;
}
AT_arc163_d [ARC163D] Sum of SCC
不知道结论,输麻了。
一开始一直在想如何竞赛图强连通分量个数如何判定,事实上,对于竞赛图,有一个结论:
竞赛图的强连通分量个数等于 把竞赛图中的点划分到 \(A\)、\(B\) 两个集合,满足所有 \(A\) 中的点都有边直接连向 \(B\) 中的所有点的划分数 \(-1\),\(A\) 和 \(B\) 可以为空集。
证明:
对于任意竞赛图,缩点后依然是一个竞赛图,且满足它是一个拓扑序唯一的 \(\text{Dag}\) 图。假设 \(p_i\) 代表的是拓扑序为第 \(i\) 个的 \(\text{SCC}\),总共有 \(k\) 个 \(\text{SCC}\),那么我们可以把任意前缀放入 \(A\),剩余的放入 \(B\),总共有 \(k+1\) 种划分方案。其他方案都是不可行的,为什么?如果把 \(\text{SCC}\) 分别放入 \(A\) 和 \(B\) 肯定不行,如果拓扑序不连续也不行。
接下来就可以 \(\text{DP}\) 了。设 \(f_{i,j,k}\) 表示前 \(i+j\) 个点,\(|A|=i\),\(|B|=j\),总共有 \(k\) 条边且符合条件的划分的方案数。
转移看第 \(i+j+1\) 个点放入哪个集合:
时间复杂度 \(\Theta(n^3\times m)\)
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const ll mod = 998244353;
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
const int kN = 33;
const int kM = 903;
int n, m;
ll f[kN][kN][kM], C[kM][kM];
signed main(){
File();
cin >> n >> m;
C[0][0] = 1;
for(int i = 1; i <= 900; i++){
C[i][0] = 1;
for(int j = 1; j <= i; j++){
C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
}
}
f[0][0][0] = 1;
for(int i = 0; i < n; i++){
for(int j = 0; j < n - i; j++){
for(int k = 0; k <= m; k++){
for(int x = 0; x <= i; x++) Add(f[i + 1][j][k + x], f[i][j][k] * C[i][x] % mod);
for(int x = 0; x <= j; x++) Add(f[i][j + 1][k + i + x], f[i][j][k] * C[j][x] % mod);
}
}
}
ll ans = mod - C[n * (n - 1) / 2][m];
for(int i = 0; i <= n; i++) Add(ans, f[i][n - i][m]);
cout << ans << '\n';
return 0;
}
AT_agc053_c [AGC053C] Random Card Game
我们设两堆牌分别为 \(A\) 堆和 \(B\) 堆,发现 \(2n\) 这个数一定不会被删掉,我们钦定这个数在 \(B\) 堆,那么最终被删空的一定是 \(A\) 堆。
接下来我们需要知道得分最小是多少。
将 \(A\) 和 \(B\) 从上到下编号,设 \(p_i\) 为满足 \(A_i>B_j\) 的最小的 \(j\),则 \(B\) 至少要删掉 \(\max{(0,p_i-i)}\) 个元素才能让 \(A_i\) 删掉,答案下界就是 \(n+\max_{i=1}^n{p_i-i}\)。因为 \(p_1\ge 1\),所以对 \(0\) 取 \(\max\) 被省略掉了。
我们猜测这个答案下界一定能被取到。我们令 \(p_i\) 变成它的前缀 \(\max\) 后发现不影响答案,然后我们可以按照顺序删除必须删的 \(B\)。
那么现在问题变为了如何求这个东西的期望。
对于任意 \(0\le d < n\),设 \(p(d)\) 为最小取牌次数 \(\le n+d\) 的概率,不妨令 \(p(-1)=0\),答案为
现在我们需要快速求 \(p(d)\),钦定 \(2n\) 在 \(B\) 中,则答案 \(\le n + d\) 当且仅当 \(\forall i\ge 1\),满足 \(b_1,\dots,b_{\min(i+d,n)}\) 中存在一个值 \(>a_i\),考虑类似排列插入的技巧,只要 \(a_i\) 不是已经插入 \(A\) 和 \(B\) 的 \(i+\min(i+d,n)\) 中个数中最大的就行,概率为 \(\frac{i+\min(i+d,n)-1}{i+\min(i+d,n)}\)。为什么?
假设已经插入 \(A\) 和 \(B\) 中 \(i+\min(i+d,n)\) 中个数中最大的数为 \(x\)。
若 \(x\) 在 \(B\) 中成立,若 \(x\) 在 \(A\) 中,则考虑到 \(i-1\) 时成立,此时肯定成立,而前一项成立的概率已经算了,所有没有问题。
发现取 \(\min\) 太麻烦了,直接分两部分算。
这已经可以预处理了,复杂度 \(\Theta(n)\)。
#include<bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const ll mod = 1e9 + 7;
const int kN = 2e6 + 7;
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
ll ans, inv[kN], jc[kN], jcinv[kN];
int n;
ll calc(int d){
ll res = inv[n] * (2ll * n - d) % mod;
res = res * jc[2 * (n - d) + d - 1] % mod;
if(d) res = res * jcinv[d - 1] % mod;
res = res * jcinv[2 * (n - d) + d] % mod;
res = res * jc[d] % mod;
return res;
}
signed main(){
File();
cin >> n;
inv[0] = inv[1] = jc[0] = jc[1] = jcinv[0] = jcinv[1] = 1;
for(int i = 2; i <= n * 2; i++) inv[i] = inv[mod % i] * (mod - mod / i) % mod;
for(int i = 2; i <= n * 2; i++) jc[i] = jc[i - 2] * 1ll * i % mod;
for(int i = 2; i <= n * 2; i++) jcinv[i] = jcinv[i - 2] * 1ll * inv[i] % mod;
ans = n << 1;
for(int d = 0; d <= n - 1; d++) Add(ans, mod - calc(d));
cout << ans << '\n';
return 0;
}
AT_agc019_f [AGC019F] Yes or No
待
qoj #414. 一般难度的推式子题
前置知识 双线反射容斥
这题有个技巧,对于一棵无标号树,无标号的欧拉序与树的形态一一对应。换言之,将第一次视作左括号,第二次视作右括号,那么树的形态转化为为长度为 \(2\times n\) 的括号序列,且第一个是左括号,最后一个是右括号。
我们将第一个和最后一个删掉,转化为长度为 \(2\times n -2\) 的括号序列。套路地,将左括号视作 \(+1\),右括号视作 \(-1\),原问题中的高度等价于括号序列中最大的前缀和。
我们对每个高度 \(k\) 统计,\(ans=\sum_{k=1}^{n-1} cnt[h=k] \times k\),我们将上面的形式画在网格图上,考虑有什么限制,首先肯定任意前缀非负,即不能经过直线 \(y=x+1\),然后必须经过直线 \(y=x-k\)。
上面的是不好做的,我们考虑转化一下,记 \(f_i\) 表示 \(cnt[h\le k]\),则答案为 \(ans=\sum_{k=1}^{n-1} (f_i-f_{i-1})\times k\),接下来我们只需要计算 \(f_i\) 即可。
还是一样的,考虑有哪些限制,不能经过直线 \(y=x+1\);\(x-y\le k\),即不能经过直线 \(y=x-k-1\),然后我们可以直接套用双线反射容斥做,复杂度由调和级数可得为 \(\Theta(n \ln n)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 2e5 + 7;
int n, b1, b2;
ll mod, jc[kN], jcinv[kN], f[kN];
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
ll Pow(ll a, int b){
ll res = 1; a %= mod;
for(; b; b >>= 1){
if(b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
ll C(int n, int m){
return jc[n] * jcinv[m] % mod * jcinv[n - m] % mod;
}
void L(int &x, int &y, int b){
int t = x;
x = y - b;
y = t + b;
return ;
}
void Solve1(int n, int m, ll &ans){
bool fg = 1;
L(n, m, b1);
while(n >= 0 && m >= 0){
if(fg) Add(ans, mod - C(n + m, m)), L(n, m, b2);
else Add(ans, C(n + m, m)), L(n, m, b1);
fg ^= 1;
}
return ;
}
void Solve2(int n, int m, ll &ans){
bool fg = 1;
L(n, m, b2);
while(n >= 0 && m >= 0){
if(fg) Add(ans, mod - C(n + m, m)), L(n, m, b1);
else Add(ans, C(n + m, m)), L(n, m, b2);
fg ^= 1;
}
return ;
}
signed main(){
File();
cin >> n >> mod;
if(n == 1){
cout << 0 << '\n';
return 0;
}
if(n == 2){
cout << 1 << '\n';
return 0;
}
jc[0] = jcinv[0] = 1;
for(int i = 1; i <= n * 2; i++) jc[i] = jc[i - 1] * 1ll * i % mod;
jcinv[n * 2] = Pow(jc[n * 2], mod - 2);
for(int i = n * 2 - 1; i >= 1; i--) jcinv[i] = jcinv[i + 1] * 1ll * (i + 1) % mod;
for(int k = 1; k < n; k++){
b1 = 1, b2 = -k - 1;
Add(f[k], C(2 * n - 2, n - 1));
Solve1(n - 1, n - 1, f[k]);
Solve2(n - 1, n - 1, f[k]);
} ll ans = 0;
for(int i = n - 1; i >= 1; i--) Add(f[i], mod - f[i - 1]);
for(int i = 1; i < n; i++) Add(ans, 1ll * f[i] * i % mod);
cout << ans << '\n';
return 0;
}
qoj #349. 彩绘
对每个 \(i\) 单独考虑贡献,\(i\) 的左边有 \(i-1\) 次合并,\(i\) 的右边有 \(n-i\) 次合并。左右两边是独立的,单独考虑,假设现在在对左边操作。按合并顺序给 \(1\) 到 \(i-1\) 标号,发现和 \(i\) 的合并次数为后缀最大值的数量,最后合并有一个 \(\frac{1}{2^k}\) 的系数。于是我们现在需要计算出长度为 \(n\) 的排列的不同排列的系数的和。记为 \(f_n\),考虑转移。采用 插入最小值的方法 转移,当 \(1\) 放到最后面,有 \(\frac{1}{2}f_{i-1}\) 的贡献,否则没有贡献,直接继承。至此,容易得到转移:
因为左右两边独立,转成期望直接相乘,则答案为:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll mod = 998244353;
const int kN = 1e5 + 7;
void File(){freopen("1.in", "r", stdin);}
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
ll Pow(ll a, int b){
a %= mod; ll res = 1;
for(; b; b >>= 1){
if(b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
int n;
ll inv[kN], f[kN], r[kN], g[kN], b[kN], jc[kN], jcinv[kN];
ll C(int n, int m){
return jc[n] * jcinv[n - m] % mod * jcinv[m] % mod;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
jc[0] = jcinv[0] = inv[1] = 1;
for(int i = 1; i <= n; i++) jc[i] = jc[i - 1] * 1ll * i % mod;
for(int i = 2; i <= n; i++) inv[i] = (mod - mod / i) * inv[mod % i] % mod;
jcinv[n] = Pow(jc[n], mod - 2);
for(int i = n - 1; i >= 1; i--) jcinv[i] = jcinv[i + 1] * 1ll * (i + 1) % mod;
for(int i = 1; i <= n; i++) cin >> r[i] >> g[i] >> b[i];
f[0] = 1;
for(int i = 1; i <= n; i++) f[i] = add(1ll * (i - 1) * f[i - 1] % mod, inv[2] * f[i - 1] % mod);
for(int i = 1; i <= n; i++){
ll v = f[i - 1] % mod * f[n - i] % mod * jcinv[i - 1] % mod * jcinv[n - i] % mod;
r[i] = v * r[i] % mod, g[i] = v * g[i] % mod, b[i] = v * b[i] % mod;
if(i > 1) Add(r[i], r[i - 1]), Add(g[i], g[i - 1]), Add(b[i], b[i - 1]);
}
cout << r[n] << ' ' << g[n] << ' ' << b[n] << '\n';
return 0;
}
uoj #667. 【UNR #5】提问系统
首先对栈操作序列,我们需要建一棵 树结构:初始只有一个节点,且有一个指针指在该节点。遍历每个操作,遍历到 push 操作时在当前指针处新加入一个儿子,并将指针指向新加入的节点处;遍历到 pop 操作时,将指针移动到当前指针指的节点的父节点处。可以发现这棵树很好地描述了栈的状态:每个时刻栈里的元素对应树上从根到某个节点的一条链,那么限制就是每条链上 R 和 B 节点的数量不能超过 \(c_r\) 和 \(c_b\),注意根节点不需要选择 R 或 B。
考虑 转化每个方案的贡献。设 \(col_i\) 表示节点 \(i\) 的颜色,容易发现 \(p_rp_b^2=\sum_{col_i=R}1\sum_{col_i=B}1\sum_{col_i=B}1\),也就是有序地选出 \(1\) 个 R 和 \(2\) 个 B 的方案数。
接下来我们可以 DP 了。设 \(f_{i,j,k,0/1,0/1,0/1}\) 表示以 \(i\) 为根的子树中,向下的链中 R 个数最大为 \(j\),B 最大个数为 \(k\),是否选出 \(1\) 个 R 和 \(2\) 个 B 的方案数。
用 背包合并 这些维度,并且在每个节点确定颜色时决定是否选这个颜色。这样可以做到 \(\Theta(k^3)\),不能通过。
接下来最巧妙的一步出现了,我们 翻转状态记录的维度,更改 \(j\) 和 \(k\) 的定义为 \(i\) 的祖先节点中有多少个颜色分别为 R 和 B,我们惊奇地发现 \(j+k=dep_i\),于是我们可以舍掉其中一维,得到了一个 \(\Theta(k^2)\) 的做法。
这道题的实现有一些细节可以大幅度地减少码量,所以 阅读一些高水平选手的代码一定程度上能提升自己的代码能力和改善代码习惯。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll mod = 998244353;
const int kN = 2503;
void File(){freopen("1.in", "r", stdin);}
void Add(ll &x, ll y){(x += y) >= mod ? x -= mod : 0;}
ll add(ll x, ll y){Add(x, y); return x;}
int n, cr, cb, fa[kN], dep[kN];
ll f[kN][kN][8], g[8];
vector<int> ve[kN];
void dfs(int x){
int down = max(0, dep[x] - cb), up = min(dep[x], cr);
if(down > up) return ;
for(int v : ve[x]) dfs(v);
for(int k = down; k <= up; k++){
memset(g, 0, sizeof(g)); g[0] = 1;
for(int v : ve[x]){
for(int i = 7; ~i; i--){
g[i] = g[i] * f[v][k][0] % mod;
for(int j = (i - 1) & i; j != i; j = (j - 1) & i){
Add(g[i], g[j] * f[v][k][i ^ j] % mod);
}
}
}
if(x != 1){
if(k){
for(int i = 7; ~i; i--){
for(int j : {0, 4}){
if((i & j) == 0 && k) Add(f[x][k - 1][i | j], g[i]);
}
}
}
if(k < dep[x]){
for(int i = 7; ~i; i--){
for(int j : {0, 1, 2, 3}){
if((i & j) == 0) Add(f[x][k][i | j], g[i]);
}
}
}
}
else for(int i = 7; ~i; i--) f[x][k][i] = g[i];
}
return ;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> cr >> cb;
int cur = 1, cnt = 1; string str;
for(int i = 1; i <= 2 * n; i++){
cin >> str;
if(str == "pop") cur = fa[cur];
else{
fa[++cnt] = cur; dep[cnt] = dep[cur] + 1;
ve[cur].push_back(cnt);
cur = cnt;
}
}
dfs(1);
cout << f[1][0][7] << '\n';
return 0;
}
P16433 [APIO 2026 中国赛区] 上升
记 \(l_i\) 为前缀 \([1,i]\) 中最后一个有值的 \(p_i\),我们只关心已经填的数值中比 \(l_i\) 小的数的个数,在记一维表示上一个数的大致位置。
我们考虑 延后钦定技巧,钦定比 \(l_i\) 大的数延后确定具体数值。
记 \(f_{i,j,k}\) 表示填了前 \(i\) 个数,\(p_i\le l_i\),且 \(<l_i\) 的数还有 \(j\) 个没用过,\(p_i\) 在 \(<l_i\) 的数中有 \(k\) 个空位比 \(p_i\) 小。
记 \(g_{i,j,k}\) 表示填了前 \(i\) 个数,\(p_i>l_i\),且 \(<l_i\) 的数还有 \(j\) 个没用过,\(p_i\) 在 \(>l_i\) 的钦定的数中相对排名为 \(k\)。
考虑填表法转移,记 \(lst=l_{i-1}\),\(cnt=i-(lst-j)\),\(cnt\) 表示有 \(cnt\) 个 \(p_i=0\) 的钦定它大于 \(lst\)。
对于 \(p_i\ne 0\),有两种情况:
由于题目保证 \(p_i\ne 0\) 的单调递增,所以我们考虑枚举 \(t\),钦定大于 \(lst\) 的前 \(t\) 小的数都在 \((lst,p_i)\) 内。
记 \(len\) 表示 \(p_i-lst-1\)。
- \(p_{i-1}\le lst\):
- \(p_{i-1}>lst\):
对于 \(p_i = 0\),有四种情况:
-
\(p_{i-1}\le lst,p_i\le lst\):
枚举 \(t\) 表示有 \(t\) 个空位 \(<p_i\),
\[f_{i-1,j,k}\times \begin{cases} w_{i-1} & k\le t \\ 1 & k>t \end{cases}\to f_{i,j-1,t} \] -
\(p_{i-1}\le lst,p_i>lst\):
枚举 \(t\) 表示 \(p_i\) 在 \(>lst\) 的数中相对排名为 \(t\)。
\[f_{i-1,j,k}\times w_{i-1}\to g_{i,j,t} \] -
\(p_{i-1}>lst,p_i\le lst\):
枚举 \(t\) 表示有 \(t\) 个空位 \(<p_i\)
\[g_{i-1,j,k}\to f_{i,j-1,t} \] -
\(p_{i-1}>lst,p_i>lst\):
枚举 \(t\) 表示 \(p_i\) 在 \(>lst\) 的数中相对排名为 \(k\)。
\[g_{i-1,j,k}\times \begin{cases} w_{i-1} & k < t \\ 1 & k\ge t \end{cases}\to g_{i,j,t} \]
直接做是 \(\Theta(n^4)\),然后用前缀和优化可以做到 \(\Theta(n^3)\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int kN = 503;
int mod;
int n, p[kN], w[kN], l[kN], C[kN][kN];
int f[2][kN][kN], g[2][kN][kN];
int sf[kN][kN], sg[kN][kN];
void Add(int &x, int y){(x += y) >= mod ? x -= mod : 0;}
int add(int x, int y){Add(x, y); return x;}
int ascend(int cid, int N, int M, std::vector<int> P, std::vector<int> W){
mod = 1ll * M;
n = N;
for(int i = 0; i < n; i++) p[i] = P[i];
for(int i = 0; i < n - 1; i++) w[i] = W[i];
C[0][0] = 1;
for(int i = 1; i <= n; i++){
C[i][0] = 1;
for(int j = 1; j <= i; j++) C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
} int x = 0, lst = 0;
f[0][0][0] = 1;
for(int i = 0; i < n; i++){
for(int j = 0; j <= max(0, lst - 1); j++){
for(int k = 0; k <= n; k++){
sf[j][k] = f[x][j][k], sg[j][k] = g[x][j][k];
if(k) Add(sf[j][k], sf[j][k - 1]), Add(sg[j][k], sg[j][k - 1]);
}
}
x ^= 1;
memset(f[x], 0, sizeof(f[x]));
memset(g[x], 0, sizeof(g[x]));
ll curW = (i ? w[i - 1] : 1ll);
if(p[i] != 0){
for(int j = 0; j <= max(0, lst - 1); j++){
int len = p[i] - lst - 1, cnt = i - lst + j;
for(int t = 0; t <= min(len, cnt); t++){
Add(f[x][j + len - t][j + len - t], 1ll * sf[j][j] * curW % mod * C[len][t] % mod);
Add(f[x][j + len - t][j + len - t], 1ll * sg[j][t] * curW % mod * C[len][t] % mod);
Add(f[x][j + len - t][j + len - t], 1ll * (mod + sg[j][cnt] - sg[j][t]) % mod * C[len][t] % mod);
}
}
lst = p[i];
}
else{
for(int j = 0; j <= max(0, lst - 1); j++){
int len = n - lst, cnt = i - lst + j;
if(j){
for(int t = 0; t < j; t++){
Add(f[x][j - 1][t], 1ll * sf[j][t] * curW % mod);
Add(f[x][j - 1][t], 1ll * (mod + sf[j][j] - sf[j][t]) % mod);
Add(f[x][j - 1][t], 1ll * sg[j][min(cnt, len)]);
}
}
for(int t = 1; t <= min(cnt + 1, len); t++){
Add(g[x][j][t], 1ll * sf[j][j] * curW % mod);
Add(g[x][j][t], 1ll * sg[j][t - 1] * curW % mod);
Add(g[x][j][t], 1ll * (mod + sg[j][min(cnt, len)] - sg[j][t - 1]) % mod);
}
}
}
}
int ans = f[x][0][0];
for(int i = 1; i <= n; i++) Add(ans, g[x][0][i]);
return (int)ans;
}

浙公网安备 33010602011771号