Small Permutation Problem (Easy Version)
算法
考虑转化
每个点 \(p_i\) 在一个平面直角坐标系中表示为点 \((i, p_i)\)
于是转化为一个棋盘问题, 即每一个点不能在 同一行 / 同一列
\(a\) 数组的限制相当于在左下角为 \((0, 0)\), 右上角为\((i, i)\) 中的正方形中, 有 \(a_i\) 个棋子
于是在每一次加入的时候, 都只能在一个 L 型区域的符合要求的地方加入

例如此时就只能在红色区域加入
于是可以分类讨论(此处 \(a_i\) 均为差分数组)
- 当 \(a_i = 0\), 则代表没有增加任何点对, 答案不变
- 当 \(a_i = 1\), 则代表增加了一个点对, 有 \(2 \times i - 1 - 2 \times a_{i - 1}\) 种可能的选择, 将其乘入答案
- 当 \(a_i = 2\), 则代表增加了两个点对, 每个点有 \(i - 1 - a_{i - 1}\) 种可能的选择, 将其的平方乘入答案
代码
#include <bits/stdc++.h>
#define int long long
const int MAXN = 2e5 + 20;
const int MOD = 998244353;
int T;
int n;
int a[MAXN];
int d[MAXN];
int Ans = 1;
void solve()
{
Ans = 1;
for (int i = 1; i <= n; i++)
{
if(d[i] == 0)
{
continue;
}else if(d[i] == 1)
{
Ans = ((Ans % MOD) * ((2 * i - 1 - 2 * a[i - 1]) % MOD)) % MOD;
}else if(d[i] == 2)
{
Ans = (Ans % MOD) * ((i - 1 - a[i - 1]) % MOD) * ((i - 1 - a[i - 1]) % MOD) % MOD;
}
}
}
signed main()
{
scanf("%lld", &T);
while(T--)
{
scanf("%lld", &n);
for (int i = 1; i <= n; i++)
{
scanf("%lld", &a[i]);
d[i] = a[i] - a[i - 1];
}
solve();
printf("%lld\n", Ans);
}
return 0;
}
分析问题
给定长度为 \(n\) 的序列 \(a\), \(0 \leq a_i \leq n\)
你需要求出有多少个长度为 \(n\) 的排列 \(p\), 满足 \(\forall i \in [1, n], \sum_{j = 1}^{i} [p_j \leq i] = a_i\)
显然是一类约束计数问题
考虑直接 \(\rm{dp}\) 做不了
找点性质, 这种类前缀问题考虑先差分一下变为 \(d_i\), 发现差分数组显然有 \(d_i \geq 0\)
方法 \(1\): 转化二维图
每次 \(i \gets i + 1\), 之前如果存在 \(p_j = i + 1\), 那么就有 \(1\) 的贡献, 否则就是 \(0\), 如果当前 \(p_i \leq i\), 那么就有 \(1\) 的贡献, 否则就是 \(0\)
考虑如何处理上面的约束
不妨对一个位置 \(i\) 定义四种状态
- \(0\): 之前不存在 \(p_j = i\), \(p_i > i\)
- \(1_1\): 之前不存在 \(p_j = i\), \(p_i \leq i\)
- \(1_2\): 之前存在 \(p_j = i\), \(p_i > i\)
- \(2\): 之前存在 \(p_j = i\), \(p_i \leq i\)
考虑什么情况下一个状态序列 \(s_i \in \{0, 1_1, 1_2, 2\}\) 是合法的
考虑排列是一个完全配对 \(\{i, p_i\}\), 尝试画到二维图上处理

其中蓝色表示一定存在一个, 红色表示一定不存在
考虑动态维护, 形如上面处理即可, 这个算法因为并不重要因此不管了, 详情直接看 洛谷题解 即可
方法 \(2\): 贡献问题
注意到
每次 \(i \gets i + 1\), 之前如果存在 \(p_j = i + 1\), 那么就有 \(1\) 的贡献, 否则就是 \(0\), 如果当前 \(p_i \leq i\), 那么就有 \(1\) 的贡献, 否则就是 \(0\)
等价于对于 \(i\), 其在差分数组 \(d\) 上的贡献为在 \(\max(i, p_i)\) 处 \(+1\)
具体的, 如果 \(p_i \leq i\), 那么显然在扫到位置 \(i\) 时统计贡献, 否则在扫到位置 \(p_i\) 时统计贡献
本质上是把对每个位置 \(i\) 计算当前位置的贡献转化成了对每个位置 \(i\) 计算它在哪里产生贡献
然后 \(a_i\) 的含义就是前 \(i\) 个位置上, 已经有 \(a_i\) 个被 \(1 \sim i\) 位置上的数填过了
特判无解
- 存在 \(c_i < 0\)
- 存在 \(a_i > i\)
- 存在 \(a_n \neq n\)
- 存在 \(c_i > 2\)
设我们填到了 \(i\), 在 \(1 \sim i\) 中有 \(s\) 个数是还没有决定填在哪里的
三种情况
- \(d_i = 0\) 时无需操作, 因为本次操作不存在对 \(i\) 的贡献
- \(d_i = 1\) 时
- 情况一: 将 \(i\) 放到 \(1 \sim i-1\) 的某个位置, 方案数为 \((i-1 - a_i)\)
- 情况二: 从剩余的 \(s\) 个数中选择一个放到i上, 方案数为 \(s\)
- \(d_i = 2\)
- 将 \(i\) 放到 \(1 \sim i-1\) 的某个位置(方案数: \(i-1 - a_i\))
- 从剩余的 \(s-1\) 个数(排除已放的 \(i\))中选择一个放到 \(i\) 上(方案数: \(s-1\))
注意每次操作后需更新 \(s\) 的值
说了这么多也没有一种好理解的方法, 重新来
首先发现贡献是一个后缀, 做差分为 \(d_i\)
也就是要求了一个位置上要有多少贡献
考虑贡献是如何产生的
发现对于 \(\{i, p_i\}\), 其贡献就是在 \(\max(i, p_i)\) 处 \(+1\)
这样一个位置最多产生一个贡献, 更好处理
先判断无解
- 存在 \(c_i < 0\)
- 存在 \(a_i > i\)
- 存在 \(a_n \neq n\)
- 存在 \(c_i > 2\)
考虑位置 \(i\) 的贡献 \(d_i\) 如何产生, 不妨在处理 \(i\) 时, 只考虑 \(1 \sim i\) 位置上, \(p_i \leq i\) 的填数情况
不难发现 \(a_i\) 等价于 \(1 \sim i\) 位置上, 有多少个数在 \(1 \sim i\) 中已经填过了
- 当 \(d_i = 0\) 时, 没有影响, 该咋填咋填
- 当 \(d_i = 1\) 时, 有两种情况
- 情况一: 将 \(i\) 放到 \(1 \sim i - 1\) 的某个位置产生贡献, 方案数为 \(i - 1 - a_{i - 1}\), 意为任意放一个位置
- 情况二: 从剩下的 \(i - a_{i - 1}\) 个数中选择一个放到 \(i\) 上, 方案数为 \(i - a_{i - 1}\)
- 当 \(d_i = 2\) 时, 只能先从剩下的 \(i - 1 - a_{i - 1}\) 个数中选择一个放到 \(i\) 上, 再将 \(i\) 放到 \(1 \sim i - 1\) 的某个位置产生贡献, 方案数为 \(i - 1 - a_{i - 1}\)
关键之处在于, 发现位置 \(i\) 的贡献只来源于 \(1 \sim i\) 位置上, 且 \(p_i \leq i\) 的填数情况, 更具体的, 一定来源于 \(i\) 位置上或者 \(i\) 数字, 过了就没贡献了
于是直接先考虑这些产生贡献的位置的方案数, 每次考虑位置 \(i\) 就变成了填数问题, 而每次可能产生的贡献来源于 \(i\) 填到前面或者 \(\leq i\) 的数放到 \(i\) 上
不难发现直接渐进填数即可, 每次根据位置 \(i\) 上产生的贡献维护当前 \(1 \sim i\) 中有多少个数没有使用, 以及有多少个 \(1 \sim i\) 中的位置已经被填过了
注意到没什么约束, 直接推不用 \(\rm{dp}\)
总结
一类很特殊的计数, 需要动态填数
本题中 \(i\) 位置的贡献只来自把之前的数字填到 \(i\) 上或者把 \(i\) 这个数字填到之前的位置上
这种问题, 可以动态填数, 每次只考虑数 \(i\), 如果不填就归类到「之前的数字」和「之前的位置」, 然后维护即可
也就是「之前的数字」一定填到当前的位置, 当前的数字一定填到「之前的位置」, 这样就易于线性处理了
动态维护「之前的数字」和「之前的位置」的个数, 然后根据当前位置的贡献进行处理即可

浙公网安备 33010602011771号