title : 2021ICPC澳门站 个人题解
date : 2022-10-6
tags : ACM,题解,练习记录
author : Linno
2020ICPC澳门站 个人题解
题目链接:https://codeforces.com/gym/103119
补题进度:8/12
A-Accelerator
式子展开为: a n s = ∏ i = 1 n a p i + ∏ i = 2 n a p i + . . . + ∏ i = n n a p i ans=\prod_{i=1}^n a_{p_i}+\prod_{i=2}^n a_{p_i}+...+\prod_{i=n}^n a_{p_i} ans=∏i=1napi+∏i=2napi+...+∏i=nnapi,对于任意一个排列期望是其其答案乘以概率,长度为i的排列概率显然为 i ! × ( n − i ) ! n ! \frac{i!×(n-i)!}{n!} n!i!×(n−i)!。设S为从n个数中取i个的所有排列的集合,设 f i = ∑ p ∈ S ∏ j = 1 i a p j f_i=\sum_{p\in S}\prod_{j=1}^i a_{p_j} fi=∑p∈S∏j=1iapj,答案期望为 E ( x ) = ∑ i = 1 n f i × i ! × ( n − i ) ! n ! E(x)=\frac{\sum_{i=1}^n f_i × i! × (n-i)!}{n!} E(x)=n!∑i=1nfi×i!×(n−i)!
我们从 f i = ∑ p ∈ S ∏ j = 1 i a p j f_i=\sum_{p\in S}\prod_{j=1}^i a_{p_j} fi=∑p∈S∏j=1iapj,有 f i = ∑ j = 1 i f i − j × f j f_i=\sum_{j=1}^i f_{i-j}×f_j fi=∑j=1ifi−j×fj
这是一个自己卷自己的分治FFT模板,复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)
//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define int long long
#define rep(i,x,y) for(int i=x;i<=y;++i)
#define repd(i,x,y) for(int i=x;i>=y;--i)
using namespace std;
const int N=1e6+7;
const int mod=998244353;
int t,n,m,a[N];
int lim,L,G=3,rev[N];
int f[N<<1],g[N<<1],h[N<<1],inv[N<<1],frac[N<<1],ifrac[N<<1];
int read(){ int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
void write(int x){if(x>9) write(x/10);putchar(x%10+'0');}
inline int fpow(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
void NTT(int *A,int limit,int type){
for(int i=0;i<limit;++i) if(i<rev[i]) swap(A[i],A[rev[i]]);
for(int mid=1;mid<limit;mid<<=1){
int wn=fpow(G,(mod-1)/(mid<<1));
for(int pos=0;pos<limit;pos+=(mid<<1)){
int w=1;
for(int k=0;k<mid;k++,w=1ll*w*wn%mod){
int x=A[pos+k],y=1ll*w*A[pos+mid+k]%mod;
A[pos+k]=(x+y)%mod;
A[pos+k+mid]=(x-y+mod)%mod;
}
}
}
if(type==1) return;
for(int i=1;i<limit/2;++i) swap(A[i],A[limit-i]);
int inv=fpow(limit,mod-2);
for(int i=0;i<limit;++i) A[i]=1ll*A[i]*inv%mod;
}
void NTT_mul(int *f,int *g,int *h,int n,int m){
int lim=1,L=0;
while(lim<=n+m) lim<<=1,L++;
for(int i=n+1;i<lim;++i) f[i]=0;
for(int i=m+1;i<lim;++i) g[i]=0;
for(int i=0;i<lim;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<(L-1));
NTT(f,lim,1);NTT(g,lim,1);
for(int i=0;i<lim;++i) h[i]=1ll*f[i]*g[i]%mod;
NTT(h,lim,-1);
}
vector<int>solve(int l,int r){
vector<int>res;
if(l==r) return {1,a[l]};
int mid=((l+r)>>1);
int len1=(mid-l+1),len2=r-mid;
auto res1=solve(l,mid);
auto res2=solve(mid+1,r);
for(int i=0;i<=len1;++i) f[i]=res1[i];
for(int i=0;i<=len2;++i) g[i]=res2[i];
NTT_mul(f,g,h,len1,len2);
res.resize(len1+len2+1);
for(int i=0;i<len1+len2+1;++i) res[i]=h[i];
return res;
}
signed main(){
inv[0]=inv[1]=1;
for(int i=2;i<N;++i) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
frac[0]=ifrac[0]=1;
for(int i=1;i<N;++i){
frac[i]=1ll*frac[i-1]*i%mod;
ifrac[i]=1ll*ifrac[i-1]*inv[i]%mod;
}
t=read();
while(t--){
n=read();
for(int i=1;i<=n;++i) a[i]=read();
auto res=solve(1,n);
int ans=0;
for(int i=1;i<=n;++i){
ans+=1ll*res[i]*frac[i]%mod*frac[n-i]%mod;
if(ans>=mod) ans-=mod;
}
ans=1ll*ans*ifrac[n]%mod;
write(ans);putchar('\n');
}
return 0;
}
C-Club Assignment
从高位到低位,将0和1分到不同的集合,如果有超过3个是相同的数位,就对答案没有贡献,直接分治到下一层。对于长度为4以下的区间可以直接暴力枚举两边异或最小值最大的方案,然后向上合并即可。
#include<bits/stdc++.h>
#define mk make_pair
#define pii pair<int,int>
#define F first
#define S second
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
typedef long long ll;
const int N=3e5+7;
pii a[N];
int t,n,ans[N];
ll solve(int l,int r,int pos){ //求从pos位向下分[l,r]区间可得到的答案
if(r-l+1==1){ //只有一个数的集合直接返回
ans[a[l].S]=1;
return inf;
}else if(r-l+1<=4){ //如果区间长度在4以内直接枚举
if(r-l+1==2){
ans[a[l].S]=1;ans[a[r].S]=2;
return inf;
}
vector<pii>s1,s2;
ll mx=0;
for(int i=l;i<=r;++i){
for(int j=i+1;j<=r;++j){
vector<pii>A,B;
A.push_back(a[i]);
A.push_back(a[j]);
for(int k=l;k<=r;++k){ //将a[i],a[j]放在A集合,其他放在B集合
if(k==i||k==j) continue;
B.push_back(a[k]);
}
ll va=inf,vb=inf;
va=A.front().F^A.back().F;
if(B.size()>1) vb=B.front().F^B.back().F;
if(min(va,vb)>=mx){ //替换原来的答案
s1=move(A);
s2=move(B);
mx=min(va,vb);
}
}
}
for(auto p:s1) ans[p.S]=1;
for(auto p:s2) ans[p.S]=2;
return mx;
}
if(pos<0){
if(r-l+1>=3){
for(int i=l;i<=r;++i) ans[a[i].S]=1;
return 0;
}
if(r-l+1==2){
ans[a[l].S]=1;
ans[a[r].S]=2;
}else ans[a[l].S]=1;
return inf;
}
int p=l;
while(p<=r){//如果有两个以上在这一位上是1,那么就等于失效了,找下一位,否则将左右分到不同集合
if((a[p].F&(1<<pos))) break;
++p;
}
ll mi=inf;
if(p>l) mi=min(mi,solve(l,p-1,pos-1));
if(p<=r) mi=min(mi,solve(p,r,pos-1));
return mi;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>t;
while(t--){
cin>>n;
for(int i=1,x;i<=n;++i){
cin>>x;
a[i]=mk(x,i);
}
sort(a+1,a+1+n);
cout<<solve(1,n,30)<<"\n";
for(int i=1;i<=n;++i) cout<<ans[i];cout<<"\n";
}
return 0;
}
D-Artifacts
签到题,队友getline写完的,用Python写了一遍发现真的好写。
import sys
import math
atk=0.0
ar=0.0
cr=5.0
cdr=50.0
for i in range(5):
for j in range(5):
str=input()
if(str[0]=='A'):
if(str[4]=='R'):
tmp=float(str.split('+')[-1].split('%')[0])
ar+=tmp
else:
tmp=float(str.split('+')[-1])
atk+=tmp
elif(str[0]=='C'):
if(str[5]=='R'):
tmp=float(str.split('+')[-1].split('%')[0])
cr+=tmp
else:
tmp=float(str.split('+')[-1].split('%')[0])
cdr+=tmp
if(cr>100):
cr=100
atk=(1500.0)*(1+ar/100.0)+atk
ans=atk*(1.0-cr/100.0)+atk*(1.0+cdr/100.0)*(cr/100.0)
print(ans)
F-Fixing Networks
这题很烦,特判条件很多。直接的结论是 d + 1 d+1 d+1个点组成一个完全图是最合理的,那么剩下的点要满足条件肯定也是完全对称的。可以想象连一个环,先前连 l e n 2 \frac{len}{2} 2len长度的点,向后相同,其中len为环的长度。如果 d d d是奇数时,如果环不对称显然无解,因此可以环长为偶数时,向对面再连一个点,就完全对称了。一些需要的特判:
① n < c ∗ ( d + 1 ) n<c*(d+1) n<c∗(d+1),即上述方式抽不出c个团。
② d 是奇数且 n 是奇数 d是奇数且n是奇数 d是奇数且n是奇数,那么最后会剩出一个奇环,环上不可能每个点不能连奇数个点。
③ d = 0 d=0 d=0,团数=点数,没有边。
④ d = 1 d=1 d=1,偶数个点两两相连。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
int n,d,c;
vector<int>ans[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>d>>c;
if(n<(d+1)*c||((d&1)&&(n&1))){
cout<<"No\n";
return 0;
}else if(d==0){
if(c==n) cout<<"Yes\n";
else cout<<"No\n";
return 0;
}else if(d==1){
if(2*c==n){
cout<<"Yes\n";
for(int i=1;i<=n;++i) cout<<(i+((i&1)?1:-1))<<"\n";
}else cout<<"No\n";
}else{
int L=1;
for(int p=1;p<=c-1;p++,L+=d+1){ //c-1团
int R=L+d;
for(int i=L;i<=R;++i){
for(int j=L;j<=R;++j){
if(i!=j) ans[i].emplace_back(j);
}
}
}
if(L<=n){ //剩下[L~n]的点
int len=n-L+1; //这里有一个环
if((len&1)&&(d&1)){
cout<<"No\n";
return 0;
}
for(int i=L;i<=n;++i){
for(int j=1;j<=d/2;++j){
ans[i].emplace_back(L+(i-L+j+len)%len);
}
for(int j=1;j<=d/2;++j){
ans[i].emplace_back(L+(i-L-j+len)%len);
}
if(d&1){
int tmp=i+len/2;
if(tmp>n) tmp-=len;
ans[i].emplace_back(tmp);
}
}
}
cout<<"Yes\n";
for(int i=1;i<=n;++i){
stable_sort(ans[i].begin(),ans[i].end());
for(auto j:ans[i]) cout<<j<<" ";
cout<<"\n";\
}
}
return 0;
}
G-Game on Sequence
容易发现所有位置上的胜负状态都是可以由最后一个位置出现的该数的胜负状态表示的。记录好每个数字最后出现的位置,对于操作1就直接加入,然后重新扫一遍所有数字的胜负态,由最后出现的数字向前开始搜SG数组。对于操作2分类讨论:①如果这个数不是最后出现的那一个,肯定是必胜的(可以转移到最后一次出现的相同数字或者其能转移的最后一个数);②否则我们已经提前处理过该数字的胜负了,直接输出。
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+7,M=300;
int n,m,a[N],lst[M],sg[M];
bool cmp(int A,int B){
return lst[A]>lst[B];
}
inline void work(){
memset(sg,1,sizeof(sg));
vector<int>tmp;
for(int i=0;i<M;++i) if(lst[i]) tmp.emplace_back(i);
sort(tmp.begin(),tmp.end(),cmp);
for(auto num:tmp){
bool flag=false;
for(int i=0;i<8;++i){
if(!sg[num^(1<<i)]){
flag=1;
break;
}
}
if(flag) sg[num]=1;
else sg[num]=0;
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>a[i],lst[a[i]]=i;
work();
for(int i=1,op,x;i<=m;++i){
cin>>op>>x;
if(op==1){
a[++n]=x;
lst[a[n]]=n;
work();
}else{
if(lst[a[x]]==x){
if(sg[a[x]]) cout<<"Grammy\n";
else cout<<"Alice\n";
}else cout<<"Grammy\n";
}
}
return 0;
}
I-Nim Cheater
对于所有操作,可以构成一棵树,我们在树上dp可以得到每个操作的答案。答案转移就是简单的dp式子 d p [ i ⊕ v a l [ x ] ] = m i n ( d p [ i ] + c o s t [ p ] ) dp[i\oplus val[x]]=min(dp[i]+cost[p]) dp[i⊕val[x]]=min(dp[i]+cost[p])
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int N=20001;
int ans[N],fa[N],val[N],cost[N];
vector<int>G[N];
int sz;
bool hson[N];
int dfs(int p){ //重链剖分
int cnt=1,mx=0,son=-1;
for(int to:G[p]){
int res=dfs(to);
if(res>mx){
mx=res;
son=to;
}
cnt+=res;
}
if(son>=0) hson[son]=1;
return cnt;
}
int res[N];
void solve(int p,int tot){ //树上dp,先遍历轻儿子,然后遍历重儿子
int *bk;
if(!hson[p]){
bk = new int[N];
for(int i=0;i<N;++i) bk[i]=res[i];
}
for(int i=0;i<N;++i){
res[i^val[p]]=min(res[i^val[p]],res[i]+cost[p]);
}
ans[p]=res[tot^val[p]];
int son=-1;
for(int to:G[p]){
if(hson[to]){
son=to;
continue;
}
solve(to,tot^val[p]);
}
if(son>=0) solve(son,tot^val[p]);
if(!hson[p]){
for(int i=0;i<N;++i) res[i]=bk[i];
delete [] bk;
}
}
int num;
void printans(int p){
if(!num) return;
if(p){
cout<<ans[p]<<"\n";
num--;
}
for(int to:G[p]){
if(!num) return;
printans(to);
if(!num) return;
cout<<ans[p]<<"\n";
num--;
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
int n,cur=0;
cin>>n;
string op;
for(int i=1;i<=n;++i){ //所有操作可以构成一棵树
cin>>op;
if(op=="ADD"){
++sz;
cin>>val[sz]>>cost[sz];
G[cur].emplace_back(sz);
fa[sz]=cur;
cur=sz;
}else{
cur=fa[cur];
}
}
memset(res,inf,sizeof(res));
res[0]=0;
dfs(0);
solve(0,0);
num=n;
printans(0);
return 0;
}
J-Jewel Grab
#include<bits/stdc++.h>
#define inf 1e12
using namespace std;
typedef long long ll;
const int N=2e5+7;
int n,m,c[N],pre[N],nxt[N],lst[N];
ll v[N],mvc[N];
set<int>pos[N];
struct Nod{ //线段树上结点记录区间的权值和以及最右边的颜色前驱
int l,r,mx;
ll s;
}tr[N<<2];
#define ls (p<<1)
#define rs (p<<1|1)
#define mid ((tr[p].l+tr[p].r)>>1)
void pushup(int p){
tr[p].mx=max(tr[ls].mx,tr[rs].mx);
tr[p].s=tr[ls].s+tr[rs].s;
}
void build(int p,int l,int r){
tr[p].l=l;tr[p].r=r;
if(l==r){
tr[p].mx=pre[l];
tr[p].s=v[l];
return;
}
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos){
if(tr[p].l==tr[p].r){
tr[p].mx=pre[tr[p].l];
tr[p].s=v[tr[p].l];
return;
}
if(pos<=mid) modify(ls,pos);
else modify(rs,pos);
pushup(p);
}
ll query(int p,int ql,int qr){
if(ql<=tr[p].l&&tr[p].r<=qr) return tr[p].s;
ll res=0;
if(ql<=mid) res=query(ls,ql,qr);
if(qr>mid) res+=query(rs,ql,qr);
return res;
}
int find(int p,int st,int k){ //找第一个前驱在st之后的结点
if(tr[p].l==tr[p].r) return tr[p].l;
int pos=-1;
if(tr[ls].mx>=st&&k<=mid) pos=find(ls,st,k);
if(pos!=-1) return pos;
if(tr[rs].mx>=st) pos=find(rs,st,k);
return pos;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>c[i]>>v[i];
pos[c[i]].insert(i);
pre[i]=lst[c[i]];
lst[c[i]]=i;
}
for(int i=n;i>=1;--i) if(pre[i]) nxt[pre[i]]=i;
build(1,1,n);
for(int i=1,op,x,y,k;i<=m;++i){
cin>>op;
if(op==1){ //对于修改操作而言,就是在链表和线段树上单点修改
cin>>x>>y>>k;
pos[c[x]].erase(x);
if(lst[c[x]]==x) lst[c[x]]=pre[x];
c[x]=y;v[x]=k;
if(nxt[x]){
if(pre[x]){
nxt[pre[x]]=nxt[x];
pre[nxt[x]]=pre[x];
modify(1,nxt[x]);
}else{
pre[nxt[x]]=0;
modify(1,nxt[x]);
}
}else{
if(pre[x]) nxt[pre[x]]=0;
}
int p;
auto it=pos[y].lower_bound(x);
if(it==pos[y].end()){
nxt[x]=-1;
pre[x]=lst[y];
if(pre[x]!=-1) nxt[pre[x]]=x;
modify(1,x);
}else{
p=*it;
if(pre[p]){
nxt[pre[p]]=x;
pre[x]=pre[p];
modify(1,x);
}else{
pre[x]=0;
modify(1,x);
}
nxt[x]=p;
pre[p]=x;
modify(1,p);
}
pos[y].insert(x);
lst[y]=max(lst[y],x);
}else{
vector<int>col;
col.clear();
cin>>x>>k;
int cur=x;ll ans=0;
for(int j=0;j<=k&&cur<=n;++j){ //筛选k个前缀在s之前的结点
int p=find(1,x,cur);
if(p==-1){
ans+=query(1,cur,n); //如果没有颜色重复
break;
}else{
col.emplace_back(c[p]);
if(j<k){
if(mvc[c[p]]==0) mvc[c[p]]=v[pre[p]];
if(v[p]>mvc[c[p]]){ //记录该颜色经过筛选后的最大权值
ans-=mvc[c[p]];
ans+=v[p];
mvc[c[p]]=v[p];
}
}
ans+=query(1,cur,p-1);
cur=p+1;
for(auto u:col) mvc[u]=0;
}
}
cout<<ans<<"\n";
}
}
return 0;
}
L-Random Permutation
推出来的式子是 n ! ∗ n ! n n \frac{n!*n!}{n^n} nnn!∗n!
import sys
import math
n=int(input())
ans=math.factorial(n)*math.factorial(n)/(n**n)
print(ans)