AtCoder Beginner Contest 242 題解
- C - 1111gal password
- D - ABC Transform
- E - (∀x∀)
- F - Black and White Rooks
- G - Range Pairing Query
- Ex - Random Painting
C - 1111gal password
C 題可不可以算給 Beginner 打開了數數的大門啊。
定義狀態 \(f_{i,j}\) 表示長度為 \(i\) 的符合題目要求的數字,結尾數位是 \(j\) 的有多少個。那麼遞推式就是:
\[f_{i,j} = f_{i-1,j-1} + f_{i-1,j} + f_{i-1,j+1} \\ f_{i,0} = f_{i,10} = 0 \\ f_{1,j} = 1 \]然後就可以數數了。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int mod=998244353; int n; ll f[1000005][11]; int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=1;i<=9;i++)f[1][i]=1; for(int j=2;j<=n;j++){ for(int i=1;i<=9;i++)f[j][i]=(f[j-1][i-1]+f[j-1][i]+f[j-1][i+1])%mod; } cout<<accumulate(f[n]+1,f[n]+10,0ll)%mod; return 0; }
D - ABC Transform
很有趣味!
首先可以看出一個字元執行了 \(t\) 次操作以後的長度是好計算的,所以我們先把問題變成一個字元執行 \(t\) 次操作之後的第 \(k\) 個是什麼。
上面的東西可以遞迴處理,定義函式 \(f(c,t,k)\) 表示字元 \(c\) 執行了 \(t\) 次操作得到的字串的第 \(k\) 個是什麼,只是有一個問題:\(t\) 太大了。
觀察到每次操作會把字串長度加倍,所以執行很少操作後,就變成了對 \(f(c,t,1)\) 求值。然而操作三次以後第一個字元等價於沒有改變,所以可以直接得到結果。
#include<bits/stdc++.h> using namespace std; typedef long long ll; int n,q; char s[100005]; void deal(int c,ll t,ll k){ if(t==0){ cout<<char(c+'A')<<'\n'; return; } if(1ll<<t-1>=k)deal((c+1)%3,t-1,k); else deal((c+2)%3,t-1,k-(1ll<<t-1)); } int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>s+1; n=strlen(s+1); cin>>q; while(q--){ ll t,k; cin>>t>>k; ll lgk=1; while(1ll<<lgk<k)lgk++; if(t>lgk){ t-=(t-lgk)/3*3; } for(int i=1;;i++){ if(k>1ll<<t)k-=1ll<<t; else{ deal(s[i]-'A',t,k); break; } } } return 0; }
E - (∀x∀)
這個題目的標題是怎麼回事啊。
我們可以列舉兩個字串相同的字首有多長,然後在後一個位置給串 \(X\) 填更小的字元,再之後的位置可以隨便填了。
特殊地,需要判斷 \(X\) 的前半段全部和 \(S\) 相同時,後半段是否小於等於 \(S\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=998244353;
const int N=1e6;
int n,pw[N+5];
char s[N+5];
void mian(){
cin>>n>>s+1;
int ans=0;
bool any=1;
for(int i=1;i<=(n+1)/2;i++){
int a=s[i]-'A',b=s[n+1-i]-'A';
ans=(ans+(ll)a*pw[(n+1)/2-i])%mod;
any&=a<=b;
any|=a<b;
}
cout<<(ans+any)%mod<<'\n';
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
pw[0]=1;
for(int i=1;i<=N;i++)pw[i]=pw[i-1]*26ll%mod;
int T;
cin>>T;
while(T--)mian();
return 0;
}
F - Black and White Rooks
題目標題讓我想起了一個叫 black_white_tony 的人。
首先可以考慮把 \(R\) 行 \(C\) 列分配給黑棋,\(P\) 行 \(Q\) 列分配給白棋的方案數,顯然是 \(\binom N R \binom M C \binom {N-R} P \binom {M-C} Q\)。
然後我們考慮在 \(R\) 行 \(C\) 列裡填入 \(N\) 個棋子,允許有空著的行或者空著的列的方案數,是 \(\binom {R C} N\)。
當然,上面的列舉分配給黑白棋子的行列要求不能有空著的行列,那麼我們可以嘗試從 \(\binom {R C} N\) 裡去掉存在空著的行或者列的方案數。
記錄沒有空著的行列的方案數為 \(f(R,C,N)\)。
\[f(R,C,N) = \binom {R C} N - \sum_{r=1}^R \sum_{c=1}^C \binom R r \binom C c f(r,c,N)[r \ne R \or c \ne C] \]思路是從允許空著的方案數裡面減去強制一些行和列空著,然後給剩下的行列放入 \(N\) 個的方案數。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=998244353;
const int precLen=2500;
ll qpow(ll a,ll n){
ll res=1;
while(n){
if(n&1)res=res*a%mod;
a=a*a%mod;
n>>=1;
}
return res;
}
int fact[precLen+5],inv[precLen+5];
ll binom(int n,int r){
if(n<r||r<0)return 0;
return (ll)fact[n]*inv[n-r]%mod*inv[r]%mod;
}
void initialization(){
fact[0]=1;
for(int i=1;i<=precLen;i++){
fact[i]=(ll)fact[i-1]*i%mod;
}
inv[precLen]=qpow(fact[precLen],mod-2);
for(int i=precLen-1;i>=0;i--){
inv[i]=(ll)inv[i+1]*(i+1)%mod;
}
}
ll mem[55][55][2505];
ll put(int r,int c,int n){
if(r*c<n||max(r,c)>n)return 0;
ll &res=mem[r][c][n];
if(res)return res;
res=binom(r*c,n);
for(int i=1;i<=r;i++)for(int j=n/i;j<=c;j++)if(i!=r||j!=c){
res=(res-binom(r,i)*binom(c,j)%mod*put(i,j,n)%mod+mod)%mod;
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
initialization();
int n,m,b,w;
cin>>n>>m>>b>>w;
ll ans=0;
for(int i=1;i<n;i++)for(int j=1;j<m;j++){
ll coef1=binom(n,i)*binom(m,j)%mod*put(i,j,b)%mod;
for(int k=1;i+k<=n;k++)for(int l=1;j+l<=m;l++){
ll coef2=binom(n-i,k)%mod*binom(m-j,l)%mod*put(k,l,w)%mod;
ans=(ans+coef1*coef2)%mod;
}
}
cout<<ans<<'\n';
return 0;
}
G - Range Pairing Query
莫隊很厲害!
把詢問 \((l,r)\) 離線下來,按照 \((l/\sqrt N,r)\) 升序排序,然後從前往後列舉排序好的詢問區間,並且維護實際計算入的區間的左右端點、每個值有出現多少次和答案。在考慮一個新的詢問區間時,移動左右端點,就可以計算出答案。
由於右端點一直上升,只有 \(O(\sqrt N)\) 次會從大變小,所以右端點移動次數是 \(O(N \sqrt N)\) 的。
左端點一直在 \(\sqrt N\) 的塊內移動,只有 \(O(\sqrt N)\) 次會從一個塊走到另一個,所以移動次數也是 \(O(N \sqrt N)\) 的。
移動左右端點,其實就是把當前考慮的值的集合加入或刪除一個。維護每個值的出現次數,在增加到偶數時給答案加一,減少到奇數時給答案減一即可。
#include<bits/stdc++.h>
using namespace std;
const int B=316;
int n,q,a[100005],c[100005],res,ans[1000005];
pair<pair<int,int>,int> qs[1000005];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
cin>>q;
for(int i=1;i<=q;i++){
cin>>qs[i].first.first>>qs[i].first.second;
qs[i].second=i;
}
sort(qs+1,qs+1+q,[&](const pair<pair<int,int>,int>& a,const pair<pair<int,int>,int>& b){
if(a.first.first/B!=b.first.first/B)return a.first.first/B<b.first.first/B;
if(a.first.second!=b.first.second)return a.first.second<b.first.second;
return a.second<b.second;
});
int l=1,r=0;
for(int i=1;i<=q;i++){
int ql,qr;
tie(ql,qr)=qs[i].first;
while(r<qr){
++r;
res+=c[a[r]]&1;
++c[a[r]];
}
while(l>ql){
--l;
res+=c[a[l]]&1;
++c[a[l]];
}
while(r>qr){
--c[a[r]];
res-=c[a[r]]&1;
--r;
}
while(l<ql){
--c[a[l]];
res-=c[a[l]]&1;
++l;
}
ans[qs[i].second]=res;
}
for(int i=1;i<=q;i++)cout<<ans[i]<<'\n';
return 0;
}
Ex - Random Painting
賽場上不會 Min-Max 容斥也不會 EGF 推算的我真的好蠢啊。
給定一個隨機元素的集合 \(S\),每個元素都有一個隨機取值 \(x\)。那麼記最小值的期望值為 \(Min(S)\),最大值的期望值為 \(Max(S)\),有結論:
\[Max(S) = \sum_{T\sube S} (-1)^{|T|+1} Min(T) \]也能理解成一組集合中每個元素都有一個單位時間被選中的概率,\(Min(S)\) 為至少選中一個的期望時間,\(Max(S)\) 為全部選完的期望時間。
那麼我們在本題中,可以求出每種位置集合,當中至少一個位置被選中的期望時間,就可以用 Min-Max 容斥變成所有位置都被選中的期望時間了。
我們可以 \(f_{i,j,k}\) 表示確定了前 \(i\) 個位置選或者不選,第 \(i\) 個一定選,沒有覆蓋選了的位置的線段有 \(j\) 個,選的位置的個數的奇偶性是 \(k\) 的方案數。現在就可以列舉上一個選擇的位置是什麼,然後轉移。最後使用虛擬狀態 \(f_{n+1,i,0/1}\) 計算答案。
#include<bits/stdc++.h>
#include<atcoder/modint>
using namespace std;
typedef long long ll;
typedef atcoder::modint998244353 mint;
mint qpow(mint a,ll n){
mint res=1;
while(n){
if(n&1)res*=a;
a*=a;
n>>=1;
}
return res;
}
int n,m,sum[405][405];
mint f[405][405][2];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
sum[l][r]++;
}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)sum[i][j]+=sum[i][j-1];
f[0][0][1]=1;
for(int i=1;i<=n+1;i++){
int s=0;
for(int j=i-1;j>=0;j--){
s+=sum[j+1][i-1];
for(int k=s;k<=m;k++){
for(int pa=0;pa<2;pa++){
f[i][k][pa]+=f[j][k-s][pa^1];
}
}
}
}
mint ans;
for(int i=0;i<m;i++){
ans+=f[n+1][i][1]*m/(m-i);
ans-=f[n+1][i][0]*m/(m-i);
}
cout<<ans.val()<<'\n';
return 0;
}