难度
省 选 / N O I − \color{purple}省选/NOI- 省选/NOI−
− 971 2.28 k -\frac{971}{2.28k} −2.28k971
题意 + 数据范围 + 样例解释
- 给你一个 0 ∼ 9 0\sim9 0∼9数字的多重集排列,比如 1020 1020 1020
问你该排列是该多重集全排列的字典序第几排列 ( b a s e 0 ) (base\ 0) (base 0)。
多重集合大小 ≤ 50 \le 50 ≤50
答案保证不会超过 2 63 − 1 2^{63}-1 263−1,所以答案不取模。 - 对于多重集合 { 0 , 0 , 1 , 2 } \{0,0,1,2\} {
0,0,1,2},按字典序的全排列为:
0012 , 0021 , 0102 , 0120 , 0201 , 0210 , 1002 , 1020 , 1200 , 2001 , 2010 , 2100 0012,0021,0102,0120,0201,0210,1002,1020,1200,2001,2010,2100 0012,0021,0102,0120,0201,0210,1002,1020,1200,2001,2010,2100
所以 1020 1020 1020 是第 7 7 7 个排列。( 0012 0012 0012 是第 0 0 0 个排列)
康托展开
- 例题:【模板】康托展开:洛谷 P5367
- 康托展开就是求 1 ∼ N 1\sim N 1∼N的全排列的字典序是第几小,其实与原题的区别就是不是多重集。
公式:
- 字典序号 = ∑ n i = 1 S i × ( n − i ) ! =\underset{i=1}{\overset{n}{\sum}}S_i\times (n-i)! =i=1∑nSi×(n−i)!
其中 S i S_i Si 表示目前还没有用过的所有数里面,小于给定排列第 i i i 个数的个数。 - 公式什么鬼?初学肯定一脸蒙蔽,但是推导过程其实很简单:
考虑全排列 3 , 4 , 1 , 2 3,4,1,2 3,4,1,2。这里 3 3 3 是排列第一位, 1 1 1 是第四位。
- 你现在要构造一个新的全排列,你希望算出所有比原排列小的全排列个数。
- 如果你第一位选择了 1 1 1 或者 2 2 2 ,那么你后面三个数无论怎么排列,构造出的全排列一定比给的排列要字典序更小。个数 = 2 × ( 4 − 1 ) ! =2\times (4-1)! =2×(4−1)!
- 然后你第一位只能选择 3 3 3 了,如果选择 4 4 4 是不可能构造出比原排列小的全排列的。
- 接下来第二位,要想后面随便排列,你这一位能选 1 1 1 或 2 2 2,因为数字3已经被你选过了。个数 = 2 × ( 4 − 2 ) ! =2\times (4-2)! =2×(4−2)!
- 接下来第三位,没有比 1 1 1 还小的数,那就个数 = 0 × ( 4 − 3 ) ! =0\times(4-3)! =0×(4−3)!,这一位只能选 1 1 1。
- 最后一位,没有比 2 2 2 还小的没有选过的数。个数 = 0 × ( 4 − 4 ) ! =0\times(4-4)! =0×(4−4)!
- 最后总的答案等于上面所有个数相加 = 16 =16 =16,即该全排列是第 16 16 16 个全排列。
优化:
- 如果你直接找有多少个数是目前没有用过的,那时间复杂度 O ( N 2 ) O(N^2) O(N2)
我们可以用树状数组或者线段树记录 [ 0 , x ] [0,x] [0,x] 中还有多少个数没有用。
记 q u e r y ( 0 , x ) query(0,x) query(0,x) 表示 [ 0 , x ] [0,x] [0,x] 范围内没有用过的数的个数,则
字典序号 = ∑ n i = 1 q u e r y ( 0 , a [ i ] − 1 ) × ( n − i ) ! =\underset{i=1}{\overset{n}{\sum}}query(0,a[i]-1)\times (n-i)! =i=1∑nquery(0,a[i]−1)×(n−i)!
时间复杂度 O ( N log N ) O(N\log N) O(NlogN)
核心代码:
/*
_ __ __ _ _
| | \ \ / / | | (_)
| |__ _ _ \ V /__ _ _ __ | | ___ _
| '_ \| | | | \ // _` | '_ \| | / _ \ |
| |_) | |_| | | | (_| | | | | |___| __/ |
|_.__/ \__, | \_/\__,_|_| |_\_____/\___|_|
__/ |
|___/
*/
const int MAX = 1e6+50;
const ll MOD = 998244353;
ll fac[MAX];
ll aa[MAX];
ll cc[MAX];
int n;
ll lowbit(ll x){
return x & (-x);
}
ll upd(ll x,ll c){
while(x <= n){
cc[x] += c;
x += lowbit(x);
}
}
ll query(ll x){
ll res = 0;
while(x){
res += cc[x];
x -= lowbit(x);
}
return res;
}
void init(int x){
fac[0] = 1;
for(int i = 1;i <= x;++i){
fac[i] = fac[i-1] * i % MOD;
}
}
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;++i){
scanf("%d",&aa[i]);
upd(i,1);
}
init(n);
ll ans = 1;
for(int i = 1;i <= n;++i){
ans = (ans + query(aa[i] - 1) * fac[n - i] % MOD) % MOD;
upd(aa[i],-1);
}
cout << ans;
return 0;
}
有限多重集的全排列
- 想要知道这题的解法,这个知识需要掌握。
- 假设有 n n n 种不同元素,第 i i i 种元素的个数为 c n t i cnt_i cnti
则该有限多重集的全排列方案数为:
a n s = ( ∑ n i = 1 c n t i ) ! ∏ n i = 1 ( c n t i ! ) ans=\frac{(\underset{i=1}{\overset{n}{\sum}}cnt_i)!}{\underset{i=1}{\overset{n}{\prod}}(cnt_i!)} ans=i=1∏n(cnti!)(i=1∑ncnti)! - 但是这题不能取模,这么算明显会爆 L L LL LL,我们可以使用朴素做法:
一共 s h u = ∑ c n t i shu=\sum cnt_i shu=∑cnti 种元素,一开始选择 c n t 1 cnt_1 cnt1 个位置填充一号元素,方案数 C s h u c n t 1 C_{shu}^{cnt_1} Cshucnt1
然后在余下的 s h u − c n t 1 shu-cnt_1 shu−cnt1个位置选择 c n t 2 cnt_2 cnt2 个位置填充二号元素,方案数 C s h u − c n t 1 c n t 2 C_{shu-cnt_1}^{cnt_2} Cshu−cnt1cnt2
⋮ \qquad\qquad\qquad\vdots ⋮
最后在 s h u − c n t 1 − ⋯ − c n t n − 1 shu-cnt_1-\cdots-cnt_{n-1} shu−cnt1−⋯−cntn−1 个位置选择 c n t n cnt_n cntn 个位置填充 n n n 号元素
则该有限多重集的全排列方案数为:
a n s = C s h u c n t 1 × C s h u − c n t 1 c n t 2 × ⋯ × C s h u − c n t 1 − ⋯ − c n t n − 1 c n t n ans=C_{shu}^{cnt_1}\times C_{shu-cnt_1}^{cnt_2}\times\cdots\times C_{shu-cnt_1-\cdots-cnt_{n-1}}^{cnt_n} ans=Cshucnt1×Cshu−cnt1cnt2×⋯×Cshu−cnt1−⋯−cntn−1cntn
多重集康托展开
- 其实思路和原来一样,写成公式比较麻烦,但是写成算法会比较简单。
- 对于第 i i i 位,如果我们放置字典序小于原排列的元素,那么后面的所有元素的排列数就是多重集的全排列。
接下来,我们第 i i i 位放置和原排列相同的元素,再递推到 i + 1 i+1 i+1位,重复这个步骤。 - 然后就……没了?确实没了。
核心代码
时间复杂度: O ( 1 0 2 × n ) O(10^2\times n) O(102×n), 10 10 10是因为这里数字只有 0 ∼ 9 0\sim 9 0∼9
/*
_ __ __ _ _
| | \ \ / / | | (_)
| |__ _ _ \ V /__ _ _ __ | | ___ _
| '_ \| | | | \ // _` | '_ \| | / _ \ |
| |_) | |_| | | | (_| | | | | |___| __/ |
|_.__/ \__, | \_/\__,_|_| |_\_____/\___|_|
__/ |
|___/
*/
#define ll long long
const int MAX = 50+50;
ll CC[MAX][MAX];
string ss;
int n;
ll C(int n,int m){
/// 记忆化组合数
/// Cnm=Cn-1m+Cn-1m-1
if(CC[n][m])return CC[n][m];
if(n < m)return 0;
if(m == 1)return n;
if(m == 0 || n == m)return 1;
CC[n][m] = C(n-1,m) + C(n-1,m-1);
return CC[n][m];
}
int cnt[15]; /// 记录每个数字的出现次数
ll solve(){
/// 求有限多重集全排列个数。
int shu = 0;
for(int i = 0;i < 10;++i)shu += cnt[i];
ll res = 1LL;
for(int i = 0;i < 10;++i){
if(cnt[i]){
res = res * C(shu,cnt[i]);
shu -= cnt[i];
}
}
return res;
}
int main()
{
cin >> ss;
n = ss.size();
for(int i = 0;i < n;++i){
cnt[ss[i]-'0']++;
}
ll ans = 0;
for(int i = 0;i < n;++i){
for(char j = '0';j < ss[i];++j){
if(cnt[j-'0']){
/// 放一个比原排列该位置字典序小的元素
cnt[j-'0']--;
ans = ans + solve();
cnt[j-'0']++;
}
}
cnt[ss[i]-'0']--; /// 放一个原排列该位置的元素
}
cout << ans;
return 0;
}