杨图计数 + 对角线性质 + 轮廓线计数 + 差分:ICPC2025沈阳H

Cute Young Diagram Counting - Problem - QOJ.ac

我们首先思考一下,翻转操作中,什么是不变的。

假设一个格子在 (i,j)(i,j),我们让其贡献到 ti+jt_{i+j} 里。不难证明,tt 序列是始终不变的。

tt 序列的形式必然是:0,1,x,y,⋯ ,z,0,0,⋯0,1,x,y,\cdots,z,0,0,\cdots

接下来我们证明,所有满足此 tt 序列的杨图必然是可以构造出来的。

首先,所有操作都是可逆的,因此我们只需要证明所有 tt 序列相同的杨图可以通过某种方式调整到唯一一种杨图即可。

对于一个固定的 tt 序列,我们贪心使第一行越长越好,然后递归进入第二行,变成归纳了。

对于一个固定的杨图,我们也是逐行贪心,使第一行越长越好。因此两种方法构造出来的最大杨图是唯一的,得证。

接下来就变成计数问题了,对于一个固定的 tt 序列,如何统计有多少种合法的方案。

我们采用一种转化,把此计数问题改成轮廓线计数问题。

我们先把杨图的左边界和上边界无限延长

我们可以得到一些性质:

  1. 轮廓线必然是每个格子的外围

  2. 只有 tt 相邻的格子其轮廓线才可能相邻

    这个性质非常有用,它启示我们,我们只需要对相邻的轮廓线考虑即可。我

    更进一步,我们只需要考虑相邻两种轮廓线之间的排列方式!

  3. 对于每个 tit_i 的轮廓线数量,其实就是 2(ti−ti+1+1)2(t_i-t_{i+1}+1)。

    因此我们可以维护 tit_i 的差分序列,di=ti−ti+1d_i=t_i-t_{i+1}

接下来我们考虑两个相邻 tt 轮廓线的组合计数,我们取反对角线,如下图所示:

image-20261017150616784

(图中的 tt 理论上要整体+1)

其中5的的外边界是绿色,6的外边界是黄色。我们还能继续瞪出很多性质:

  1. 最外围的两段肯定是黄色
  2. 中间肯定是连续两段黄色,连续两段绿色的随便排布。而这种排布必然合法且双射。

如果我们把两段视为一块,那么中间就有 di−1+1d_{i-1}+1 块绿色,did_i 块黄色,它们可以随便乱排,因此方案数为 (di−1+di+1di)\large \binom {d_{i-1}+d_i+1}{d_i}

由于相邻块之间独立,所以答案为 ∏i(di−1+di+1di)\prod _i\binom {d_{i-1}+d_i+1}{d_i}

每次加入1行,只会导致4个 dd 发生变化,简单维护一下即可。

时间复杂度 O(n)O(n)

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
#include<bits/stdc++.h>
#include<cassert>
using namespace std;bool Mbe;
#ifdef LOCAL
#define debug(...) fprintf(stdout, ##__VA_ARGS__)
#else
#define debug(...) void(0)
#endif
#define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define Z(x) (x)*(x)
#define pb push_back
#define fi first
#define se second
#define mo 998244353
#define N 2000010
int pw(int a, int b) {
int ans = 1;
while(b) {
if(b & 1) ans *= a;
a *= a; b >>= 1;
ans %= mo; a %= mo;
}
return ans;
}
int pw(int a) { return pw(a, mo - 2); }
int fac[N], inv[N], ifac[N];
void init(int n) {
int i;
for(i = fac[0] = 1; i <= n; ++i) fac[i] = fac[i-1] * i % mo;
ifac[n] = pw(fac[n], mo-2);
for(i = n - 1; i >= 0; --i) ifac[i] = ifac[i+1] * (i+1) % mo;
for(i = 1; i <= n; ++i) inv[i] = ifac[i] * fac[i-1] % mo;
}
int C(int n, int m) {
if(m > n) return 0;
return fac[n] * ifac[m] % mo * ifac[n - m] % mo;
}
int iC(int n, int m) {
if(m > n) return 0;
return ifac[n] * fac[m] % mo * fac[n - m] % mo;
}
inline void Mod(int &a) { if(a >= mo || a <= -mo) a %= mo; if(a < 0) a += mo; }
inline void Add(int &a, int b) { a += b; Mod(a); }
inline void Mul(int &a, int b) { Mod(b); a *= b; Mod(a); }
const int iv2 = pw(2, mo - 2);
int n, m, i, j, k, T;
int ans, a[N], d[N];

void calc(int i, int o) {
if(d[i] == -1 || d[i + 1] == -1) return ;
if(o == -1) Mul(ans, iC(d[i] + d[i + 1] + 1, d[i + 1]));
if(o == 1) Mul(ans, C(d[i] + d[i + 1] + 1, d[i + 1]));
}

bool Med; signed main() {
#ifdef LOCAL
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
atexit([]{fprintf(stderr,"%.lfms\n%lfMB\n",clock()*1000./CLOCKS_PER_SEC,(&Mbe-&Med)/1024./1024);});
#endif
// srand(time(NULL));
// T = read();
// while(T--) {
//
// }
n = read(); init(2 * n);
for(i = 1; i <= n; ++i) a[i] = read();
ans = 1;
for(i = 1; i <= n; ++i) {
calc(i - 1, -1); calc(i, -1);
if(a[i] > 1) calc(i + a[i] - 1, -1);
calc(i + a[i], -1);
d[i]--; d[i + a[i]]++;
calc(i - 1, 1); calc(i, 1);
if(a[i] > 1) calc(i + a[i] - 1, 1);
calc(i + a[i], 1);
printf("%lld ", ans);
}
return 0;
}