Loading

Small Permutation Problem (Easy Version)

算法

考虑转化
每个点 \(p_i\) 在一个平面直角坐标系中表示为点 \((i, p_i)\)

于是转化为一个棋盘问题, 即每一个点不能在 同一行 / 同一列
\(a\) 数组的限制相当于在左下角为 \((0, 0)\), 右上角为\((i, i)\) 中的正方形中, 有 \(a_i\) 个棋子

于是在每一次加入的时候, 都只能在一个 L 型区域的符合要求的地方加入
pAYuBY6.png

例如此时就只能在红色区域加入

于是可以分类讨论(此处 \(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\}\), 尝试画到二维图上处理
pEos0VU.png
其中蓝色表示一定存在一个, 红色表示一定不存在
考虑动态维护, 形如上面处理即可, 这个算法因为并不重要因此不管了, 详情直接看 洛谷题解 即可

方法 \(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\), 如果不填就归类到「之前的数字」和「之前的位置」, 然后维护即可

也就是「之前的数字」一定填到当前的位置, 当前的数字一定填到「之前的位置」, 这样就易于线性处理了
动态维护「之前的数字」和「之前的位置」的个数, 然后根据当前位置的贡献进行处理即可

posted @ 2024-10-11 16:23  Yorg  阅读(39)  评论(0)    收藏  举报