C. Tyler and Strings(组合数学,树状数组维护前缀和)(Codeforces Round #775 (Div. 1, based on Moscow Open Olympiad i)
对我来说比较困难的一题了,尝试着自己写了一下,调不出来遂放弃.
Codeforces Round #775 (Div. 1, based on Moscow Open Olympiad in Informatics)
https://codeforces.com/contest/1648/problem/C
C.Tyler and Strings
题意:给你字符串
s
,
t
s,t
s,t,你可以对
s
s
s任意排序,问你有多少种方案使得
s
s
s重排后字典序会比
t
t
t小.
思路:枚举每次在哪里断掉.比如说在
i
i
i的位置,令
s
j
<
t
j
s_j<t_j
sj<tj,那么合法的方案就是任意排序了.
比如说
s
=
1
,
2
,
3
,
4
,
t
=
4
,
3
,
2
,
1
s=1,2,3,4,t=4,3,2,1
s=1,2,3,4,t=4,3,2,1,第一个位置选择1,剩下的
2
,
3
,
4
2,3,4
2,3,4任意排列都是合法方案.
这种情况下就是
A
(
3
,
3
)
=
6
A(3,3)=6
A(3,3)=6.
可以预见,后面
对
于
t
来
说
,
后
续
元
素
[
i
+
1
,
n
]
对于t来说,后续元素[i+1,n]
对于t来说,后续元素[i+1,n]包含重复元素的全排列就是当前令
s
i
<
t
i
si<ti
si<ti的合法方案.
为什么是重复元素,因为你一个数字后面可能出现多次.
因为我们还可以选择不同的字符在当前位置
j
j
j,令
s
i
<
t
i
si<ti
si<ti.令所有的
字
符
i
,
s
i
<
t
i
字符i,s_i<t_i
字符i,si<ti组成一个集合.我们每次都要统计这个集合对答案的贡献.(枚举到
t
i
t_i
ti)的时候.
包含重复元素的全排列数目的计算公式是:
n
!
c
n
t
1
!
∗
c
n
t
2
!
∗
.
.
.
c
n
t
k
!
{n!}\over{cnt_1!*cnt_2!*...cnt_k!}
cnt1!∗cnt2!∗...cntk!n!
其中
n
n
n为总数目,k有多少种类,
c
n
t
k
cnt_k
cntk为不同种类的数目.
那么每一个
s
i
<
t
i
s_i<t_i
si<ti对应的贡献记为
a
d
d
i
add_i
addi(注意这里的n是指题目来说,实际上是
n
−
i
−
1
n-i-1
n−i−1)
a
d
d
i
=
(
n
−
i
)
!
c
n
t
1
!
∗
c
n
t
2
!
∗
.
.
.
.
∗
(
c
n
t
i
−
1
)
!
∗
.
.
∗
c
n
t
k
!
{add_i}= {(n-i)!\over{cnt_1!*cnt_2!*....*(cnt_i-1)!*..*cnt_k!}}
addi=cnt1!∗cnt2!∗....∗(cnti−1)!∗..∗cntk!(n−i)!
显然,每次统计答案的时候,我们还需要求add数组的前缀和.而且add数组也会因为
i
i
i的推进不断更改值,因为推进的时候,你是让
s
i
−
1
=
=
t
i
−
1
s_{i-1}==t_{i-1}
si−1==ti−1,保证了前i-1位是一个前缀,才能考虑第i位.
我们需要一种数据结构,支持单点改值,区间求和,使用树状数组/线段树.
但这并没有完结.不难发现,每次在i位使得
s
i
=
=
t
i
s_i==t_i
si==ti后,对应的
c
n
t
s
i
会
减
少
1
cnt_{si}会减少1
cntsi会减少1,造成整个
a
d
d
数
组
都
会
发
生
变
化
add数组都会发生变化
add数组都会发生变化.如果每次都要去更改一遍,无论是哪种数据结构都是难以接受的.
我们考虑写出每种元素
i
i
i,对应
a
d
d
i
发
生
的
变
化
,
寻
求
共
同
改
变
的
地
方
以
降
低
复
杂
度
add_i发生的变化,寻求共同改变的地方以降低复杂度
addi发生的变化,寻求共同改变的地方以降低复杂度
对于不被删去的元素我们称为
j
j
j.
删去元素
i
i
i后
a
d
d
j
=
(
n
−
i
−
1
)
!
c
n
t
1
!
∗
c
n
t
2
!
∗
.
.
.
.
∗
(
c
n
t
i
−
1
)
!
∗
.
.
∗
c
n
t
k
!
add_j={(n-i-1)!\over{cnt_1!*cnt_2!*....*(cnt_i-1)!*..*cnt_k!}}
addj=cnt1!∗cnt2!∗....∗(cnti−1)!∗..∗cntk!(n−i−1)!
a
d
d
i
=
(
n
−
i
−
1
)
!
c
n
t
1
!
∗
c
n
t
2
!
∗
.
.
.
.
∗
(
c
n
t
i
−
2
)
!
∗
.
.
∗
c
n
t
k
!
add_i={(n-i-1)!\over{cnt_1!*cnt_2!*....*(cnt_i-2)!*..*cnt_k!}}
addi=cnt1!∗cnt2!∗....∗(cnti−2)!∗..∗cntk!(n−i−1)!
等等,这不是几乎一模一样吗,只要我们在一开始给
a
d
d
i
add_i
addi数组赋值的时候,给自己赋值为
(
c
n
t
i
−
1
)
!
不
就
好
了
(cnt_i-1)!不就好了
(cnti−1)!不就好了
那么每次修改数组,就相当于
a
d
d
数
组
整
体
乘
上
一
个
变
量
,
称
为
v
a
r
add数组整体乘上一个变量,称为var
add数组整体乘上一个变量,称为var.
v
a
r
=
c
n
t
i
(
n
−
i
+
1
)
var={cnt_i\over(n-i+1)}
var=(n−i+1)cnti其中分子代表把
a
d
d
i
分
母
上
的
(
c
n
t
i
)
!
变
为
(
c
n
t
i
−
1
)
!
add_i分母上的(cnt_i)!变为(cnt_i-1)!
addi分母上的(cnti)!变为(cnti−1)!,分母代表
把
(
n
−
i
)
!
变
为
(
n
−
i
−
1
)
!
把(n-i)!变为(n-i-1)!
把(n−i)!变为(n−i−1)!.
接下来,统计答案就是挨个扫描
t
i
t_i
ti,令
x
=
t
i
x=t_i
x=ti,查询
[
1
,
x
−
1
]
的
a
d
d
前
缀
和
,
加
到
a
n
s
上
即
可
[1,x-1]的add前缀和,加到ans上即可
[1,x−1]的add前缀和,加到ans上即可.
题解的思想到这结束,然而我们会发现这些写很容易写错…我至少发现是这样的.
我们考虑继续优化
a
d
d
add
add数组.我们发现对于区间
[
1
,
x
−
1
]
[1,x-1]
[1,x−1]写出他们
a
d
d
add
add的求和形式.
s
u
m
1
i
=
(
n
−
i
)
!
(
c
n
t
1
−
1
)
!
∗
c
n
t
2
!
∗
.
.
.
.
!
∗
.
.
∗
c
n
t
k
!
+
(
n
−
i
)
!
(
c
n
t
1
)
!
∗
(
c
n
t
2
−
1
)
!
∗
.
.
.
.
∗
.
.
∗
c
n
t
k
!
+
(
n
−
i
)
!
(
c
n
t
1
)
!
∗
(
c
n
t
2
)
!
∗
.
.
.
.
∗
(
c
n
t
x
−
1
−
1
)
!
∗
.
.
∗
c
n
t
k
!
sum1i={(n-i)!\over{(cnt_1-1)!*cnt_2!*....!*..*cnt_k!}}+{(n-i)!\over{(cnt_1)!*(cnt_2-1)!*....*..*cnt_k!}}+{(n-i)!\over{(cnt_1)!*(cnt_2)!*....*(cnt_{x-1}-1)!*..*cnt_k!}}
sum1i=(cnt1−1)!∗cnt2!∗....!∗..∗cntk!(n−i)!+(cnt1)!∗(cnt2−1)!∗....∗..∗cntk!(n−i)!+(cnt1)!∗(cnt2)!∗....∗(cntx−1−1)!∗..∗cntk!(n−i)!
我们再观察以下形式.
s
u
m
2
i
=
(
n
−
i
)
!
(
c
n
t
1
)
!
∗
c
n
t
2
!
∗
.
.
.
.
!
∗
.
.
∗
c
n
t
k
!
+
(
n
−
i
)
!
(
c
n
t
1
)
!
∗
c
n
t
2
!
∗
.
.
.
.
!
∗
.
.
∗
c
n
t
k
!
+
(
n
−
i
)
!
(
c
n
t
1
)
!
∗
c
n
t
2
!
∗
.
.
.
.
!
∗
.
.
∗
c
n
t
k
!
sum2i={(n-i)!\over{(cnt_1)!*cnt_2!*....!*..*cnt_k!}}+{(n-i)!\over{(cnt_1)!*cnt_2!*....!*..*cnt_k!}}+{(n-i)!\over{(cnt_1)!*cnt_2!*....!*..*cnt_k!}}
sum2i=(cnt1)!∗cnt2!∗....!∗..∗cntk!(n−i)!+(cnt1)!∗cnt2!∗....!∗..∗cntk!(n−i)!+(cnt1)!∗cnt2!∗....!∗..∗cntk!(n−i)!
对于第一项需要乘以
c
n
t
1
,
第
二
项
需
要
c
n
t
2
.
.
.
第
x
−
1
项
需
要
乘
以
c
n
t
x
cnt_1,第二项需要cnt_2...第x-1项需要乘以cnt_x
cnt1,第二项需要cnt2...第x−1项需要乘以cntx
那么提取公因式(实际上
s
u
m
2
i
每
一
项
都
一
样
sum2i每一项都一样
sum2i每一项都一样)
s
u
m
2
i
∗
∑
j
=
1
x
−
1
c
n
t
j
=
s
u
m
1
i
sum2i*\sum_{j=1}^{x-1}cnt_j=sum1i
sum2i∗∑j=1x−1cntj=sum1i
也就是说,我们不再需要维护
a
d
d
i
add_i
addi数组,而是去维护
c
n
t
i
cnt_i
cnti数组即可
题解好像是去维护的
a
d
d
add
add,我搞半天搞不出来…
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5+5;
const int INF = 1e9+7;
const ll mod = 998244353;
typedef pair<int,int> pii;
ll f_pow(ll a,ll b){
ll ans = 1;
while(b){
if(b&1) ans = (ans*a)%mod;
a=(a*a)%mod;b>>=1;
}
return ans;
}
ll inv_fac[maxn];
ll inv[maxn];
ll fac[maxn];
void inv_table(ll n){
inv_fac[0]=1;
inv[1]=1;fac[1]=1;
for(int i=2;i<=n;i++){
inv[i] = (mod-mod/i)*inv[mod%i]%mod;
fac[i] = (fac[i-1]*i)%mod;
}
inv_fac[n] = f_pow(fac[n],mod-2);
for(ll i=n-1;i>=1;i--){
inv_fac[i] = (inv_fac[i+1] *(i+1)) %mod;
}
}
ll tree[maxn];
void add(int x,int val){
while(x<=maxn-5){
tree[x]=(tree[x]+val)%mod;
x+=(x&-x);
}
}
ll query(int x){
ll ans = 0;
while(x){
ans +=tree[x];
x-=(x&-x);
}
return ans;
}
int cnt[maxn];
int main(){
// freopen("1.txt","r",stdin);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
inv_table(200000);
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++){
int x;cin>>x;
cnt[x]++;
add(x,1);
}
ll var =fac[n];
for(int i=1;i<=maxn-5;i++){
var = var*inv_fac[cnt[i]]%mod;
}
ll ans = 0;
for(int i=1;i<=m;i++){
int x;cin>>x;
ll sum = query(x-1);
ans=(inv[n-i+1]*sum%mod*var%mod+ans)%mod;
if(cnt[x]==0) break;
var = var*cnt[x]%mod*inv[n-i+1]%mod;
cnt[x]--;
add(x,-1);
if(i==n&&i<m){
ans=(ans+1)%mod;
break;
//s用完了,t没用完,且s是t的前缀.
}
}
cout<<ans<<"\n";
}