Codeforces Round 979 (Div. 2) D/E
这场跨度好大
D. QED's Favorite Permutation
原题链接:https://mirror.codeforces.com/contest/2030/problem/D
思路:
\(L\) 只能与当前的前一个进行交换,\(R\) 只能与当前的后一个进行交换,容易发现当前一个字符与后一个字符分别为 \(L\) 与 \(R\) 时,以此为分割,前半部分与后半部分无法进行交换,所以若无法进行排序,则在此分割的前半段的最大值一定大于后半段的最小值。因为我们每次要对特定位置的字符进行修改,然后再去判断是否可以排序。一个可行的方案是:我们预处理前缀最大和后缀最小分别为 \(pre[]\) 和 \(suf[]\),然后找到所有 \(LR\),查看在此处是否非法,我们用\(cnt\)去记录这样的非法状态数,若非法则 \(cnt++\),然后每次查询我们只去查询和修改对应位置相关的位置,判断修改后 \(cnt\) 是否会发生变化,若修改后 \(cnt\) 仍然大于 \(0\),那么显然整个序列非法,否则合法输出 \(YES\)。
时间复杂度 O(n+q)
预处理和 \(cnt\) 计数都是 \(O(n)\) 的,每次查询都只会检查修改点前后两个位置为O(1),总体为O(q)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=2e5+10;
int n,q;
void solve(){
cin>>n>>q;
vector<int> a(n+1);
for(int i=1;i<=n;i++)cin>>a[i];
vector<int> pre(n+1,0),suf(n+2,1e9);
for(int i=1;i<=n;i++){
pre[i]=max(pre[i-1],a[i]);
}
for(int i=n;i;i--){
suf[i]=min(suf[i+1],a[i]);
}
string s;
cin>>s; s=" "+s;
int cnt=0;
for(int i=1;i<n;i++){
if(s[i]=='L'&&s[i+1]=='R'&&pre[i]>suf[i+1]){
cnt++;
}
}
while(q--){
int x;
cin>>x;
if(s[x-1]=='L'&&s[x]=='R'&&pre[x-1]>suf[x])cnt--;
if(s[x]=='L'&&s[x+1]=='R'&&pre[x]>suf[x+1])cnt--;
s[x]=s[x]=='L'?'R':'L';
if(s[x-1]=='L'&&s[x]=='R'&&pre[x-1]>suf[x])cnt++;
if(s[x]=='L'&&s[x+1]=='R'&&pre[x]>suf[x+1])cnt++;
if(cnt) cout<<"NO\n";
else cout<<"YES\n";
}
}
int main() {
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
int t=1;
cin>>t;
while(t--)solve();
}
E. MEXimize the Score
原题链接:https://mirror.codeforces.com/contest/2030/problem/E
看了这篇题解才会了:https://www.cnblogs.com/YipChipqwq/p/18498462/CF2030E
思路:
对于无法直接进行求解的题,一般要么正难则反,要么考虑每部分对答案的贡献。
在这里显然无法直接求所有子序列然后求解,也没法反方向进行考虑,只能计算每部分对答案的贡献了。
对于一个完美划分的序列的价值 \(sum\) 等价于 \(min(cnt0)\) + \(min(cnt0,cnt1)\) + ... + \(min(cnt0,cnt1,...,cntt)\),那么一个贪心的想法是把相同的数字分到不同的集合。
然后我们就可以构造一个包含数字 \(t\) 的集合,这个集合的价值可以通过构造尽可能长的 \(0、1、...、t\) 的排列得到,排列存在当且仅当 \(min(cnt0, cnt1, ..., cntt)>0\)。 我们发现排列中每个数字对答案的贡献恰好为 \(1\)。
那么对于一个特定的数字 \(t\),\(f(t)=\sum_{i=k}^{cnt_t} C(cnt_t,i)\),可以视为先从 \(cnt_t\) 中选出 \(k\) 个插入到 \(k\) 个 \(0 ~ t-1\) 的序列中,这部分对答案的贡献为 \(1\),然后再把剩余的 \(i-k\) 个加入到集合中,这部分起到增加集合状态数的作用,然后剩余的大于 \(t\) 且未选择的个数 \(r\),也可以产生 \(2^r\) 个集合,这些集合插入不会对 \(t\) 的贡献产生影响,也是起到增加集合种类数的作用,所以要在乘上这个数。
具体做法就是:每次选择 \(k\) 作为这种排列的个数,然后计算每种 \(t\) 对答案的贡献,并且有 \(min(cnt0, cnt1, ..., cntt) ≥ k\),对 \(0\) 来说拿出 \(k\) 个作为合法集合,共有 \(C(cnt_0,k)\) 种方案,在考虑将多余的 \(0\) 加入,这部分的贡献就是 \(f(0)\),然后后缀还有 \(suf[0]\) 种可能(未选个数的2的幂次),相乘后直接加入答案中。
到 \(t\) 时,前 \(t-1\) 已经处理完毕,共有 \(\prod_{i=0}^{t-1} f(i)\) 种集合,对于其后的元素是否加入还有 \(suf[i]\) 种可能,然后就可以将所有答案求出。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=2e5+10,mod=998244353;
int n;
ll pow2[N],fact[N],infact[N];
ll C(int a,int b){ return fact[a]*infact[b]%mod*infact[a-b]%mod; }
void solve(){
cin>>n;
vector<int> cnt(n,0);
int a;
for(int i=0;i<n;i++){
cin>>a;
cnt[a]++;
}
vector<ll> suf(n);
suf[n-1]=1;
for(int i=n-1;i;i--){
suf[i-1]=suf[i]*pow2[cnt[i]]%mod;
}
ll ans=0,p=0;
vector<int> f(n),g(n);
g=cnt;
for(int k=cnt[0];k;k--){
while(p<n&&cnt[p]>=k)p++;
ll res=1;
for(int i=0;i<p;i++){
while(g[i]>=k)(f[i]+=C(cnt[i],g[i]))%=mod,g[i]--; // 求选至少k时的组合数
res=res*f[i]%mod; // 乘法原则
ans=(ans+res*suf[i])%mod; //未选的部分加入影响集合的种类 但对i在排列中的贡献并不产生影响
}
}
cout<<ans<<endl;
}
// qmi
ll qmi(ll a,ll k){
ll res=1;
while(k){
if(k&1)res=res*a%mod;
a=a*a%mod;
k>>=1;
}
return res;
}
// 初始化预处理阶乘和2的幂次
void init(){
pow2[0]=fact[0]=infact[0]=1;
for(int i=1;i<N;i++){
pow2[i]=2ll*pow2[i-1]%mod;
fact[i]=fact[i-1]*i%mod;
}
infact[N-1]=qmi(fact[N-1],mod-2);
for(int i=N-1;i;i--){
infact[i-1]=infact[i]*i%mod;
}
}
int main() {
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
init();
int t=1;
cin>>t;
while(t--)solve();
}

浙公网安备 33010602011771号