U558844 小苯的排列计数
题目信息
- 题目:U558844 小苯的排列计数
- 来源:洛谷
- 链接:U558844 小苯的排列计数
- 标签:数学、计数、前缀最小值
- 模数:
题意简述
给定一个长度为 的数组 。
其中:
也就是说, 表示排列 的前 个元素中的最小值。
现在需要统计有多少个 的排列 ,能够生成给定的前缀最小值数组。
如果不存在符合条件的排列,输出 。
样例理解
以样例:
4
2 2 1 1为例。
前缀最小值为:
第一位的前缀最小值是 ,说明:
第三位时,前缀最小值从 下降到 ,说明:
剩下的位置不能改变当前前缀最小值,因此只能填入比当前最小值更大的数字。
符合条件的排列有:
2 4 1 3
2 3 1 4所以答案为 。
核心思路
这题的关键是理解:
前缀最小值下降的位置是固定位置;前缀最小值不变的位置是自由位置。
规律一:最后一个前缀最小值必须是 1
因为 是一个 的排列,所以整个排列的最小值一定是 。
因此:
如果:
那么一定不存在合法排列,答案为 。
例如:
2 2长度为 的排列一定包含数字 ,最终前缀最小值不可能还是 ,所以无解。
规律二:前缀最小值数组必须单调不增
随着位置向后移动,前缀范围只会变大,最小值不可能变大。
因此必须满足:
如果出现:
则说明前缀最小值变大了,这是不可能的,答案为 。
规律三:下降位置的数字是固定的
如果:
说明第 个位置产生了新的前缀最小值。
那么当前位置只能填:
例如:
6 2 2 1 1 1 1其中:
所以:
然后:
所以:
再然后:
所以:
这些位置都已经被固定。
规律四:不下降的位置只能填更大的未使用数字
如果:
说明当前位置没有产生新的最小值。
设当前前缀最小值为 ,那么当前位置填入的数字必须满足:
否则如果填入:
前缀最小值就会下降,与给定的 不符。
同时, 还必须是没有使用过的数字。
计数方式
我们先找出所有出现在 中的数字。
这些数字中,第一次出现的位置都是固定位置,不能当作自由数字使用。
剩下没有出现在 中的数字,才可以填入那些前缀最小值不变的位置。
设:
表示:
大于 ,并且没有出现在 中的数字数量。
即:
当扫描到一个重复位置:
设:
那么当前位置可以选择的数字数量为:
其中:
表示前面已经用掉的自由数字数量。
每处理一个重复位置,就将答案乘上当前可选数量。
算法步骤
- 读入 数组;
- 记录哪些数字出现在了 中;
- 判断 是否单调不增;
- 判断 是否等于 ;
- 预处理 ;
- 从左到右扫描 ;
- 遇到相等位置时计算可选数字数量;
- 累乘答案并取模。
复杂度分析
设当前测试点的长度为 。
预处理 需要 。
扫描 也需要 。
因此总时间复杂度为:
空间复杂度为:
由于题目保证所有测试数据的 之和不超过 ,该复杂度可以通过。
参考代码
/**
* Problem: U558844 小苯的排列计数
* URL: https://www.luogu.com.cn/problem/U558844
* Memory Limit: 512 MB
* Time Limit: 1000 ms
*/
#if defined(__APPLE__) && defined(__MACH__)
#include <iostream>
#include <vector>
#include <algorithm>
#else
#include <bits/stdc++.h>
#endif
using namespace std;
using LL = long long;
const LL MOD = 998244353;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<int> prefix(n + 1);
vector<bool> appeared(n + 1, false);
// 读取 prefix,并记录哪些数字在 prefix 中出现过
for (int i = 1; i <= n; i++) {
cin >> prefix[i];
appeared[prefix[i]] = true;
}
bool ok = true;
// prefix 必须单调不增
for (int i = 2; i <= n; i++) {
if (prefix[i] > prefix[i - 1]) {
ok = false;
}
}
// 整个排列的最小值一定是 1
if (prefix[n] != 1) {
ok = false;
}
/**
* freeGreater[x] 表示:
* 大于 x,且没有出现在 prefix 中的数字数量。
*
* 这些数字可以被用在 prefix 不下降的位置。
*/
vector<int> freeGreater(n + 2, 0);
for (int x = n - 1; x >= 1; x--) {
freeGreater[x] = freeGreater[x + 1];
int v = x + 1;
if (!appeared[v]) {
freeGreater[x]++;
}
}
LL ans = 1;
// 已经使用过的自由数字数量
LL usedFree = 0;
for (int i = 2; i <= n; i++) {
// prefix 不变,说明当前位置需要填入一个大于当前最小值的自由数字
if (prefix[i] == prefix[i - 1]) {
int x = prefix[i];
LL choices = freeGreater[x] - usedFree;
if (choices <= 0) {
ok = false;
break;
}
ans = ans * choices % MOD;
usedFree++;
}
}
cout << (ok ? ans : 0) << '\n';
}
return 0;
}易错点
1. 最后一个值必须是 1
排列一定包含数字 ,所以最终前缀最小值一定是 。
如果没有判断:
就会把一些不合法情况算进去。
2. 前缀最小值只能不增
前缀范围扩大后,最小值不可能变大。
所以必须判断:
3. 数组要开到 n + 1
因为数字范围是:
所以像 appeared、freeGreater 这类数组都应该至少开到 n + 1。
不要写成:
vector<bool> appeared(n, false);否则访问 appeared[n] 时会越界。
4. 不要用双重循环暴力统计可选数字
如果每次遇到重复位置,都重新扫描一遍 ,最坏复杂度会退化成:
数据范围最大时会超时。
应该预处理:
让每次查询变成 。
复盘
这题表面上是在还原排列,实际上是一个计数问题。
关键不是枚举排列,而是区分两类位置:
- 前缀最小值下降的位置:数字已经固定;
- 前缀最小值不变的位置:需要从可用自由数字中选择。
只要能正确统计每个自由位置的可选数字数量,就可以直接累乘得到答案。
核心公式是:
其中 是当前前缀最小值,usedFree 是前面已经用掉的自由数字数量。