10.18 正睿模擬賽題解
T1
根據排序不等式,正序最大,所以這個值就是排序後相加即可。而交換次數可以用置換環個數來實現。這是一個經典套路。
注意,只有當數兩兩不同的時候,我們才能夠數置換環。一開始出題人沒有說兩兩不同,我多耽誤了 \(30\) 分鐘。
程式碼:
#include<bits/stdc++.h> #define dd double #define ld long double #define ll long long #define uint unsigned int #define ull unsigned long long #define N 300010 #define M number using namespace std; const int INF=0x3f3f3f3f; const int mod=998244353; template<typename T> inline void read(T &x) { x=0; int f=1; char c=getchar(); for(;!isdigit(c);c=getchar()) if(c == '-') f=-f; for(;isdigit(c);c=getchar()) x=x*10+c-'0'; x*=f; } inline int TwoPow(int x){return 1ll*x*x%mod;} int n,T,a[N],b[N],c[N],d[N]; int li[N]; bool vis[N]; int main(){ // freopen("my.in","r",stdin); // freopen("my.out","w",stdout); read(n);read(T); for(int i=1;i<=n;i++){read(a[i]);c[i]=a[i];} for(int i=1;i<=n;i++){read(b[i]);d[i]=b[i];} sort(c+1,c+n+1);sort(d+1,d+n+1); ll ans=0; for(int i=1;i<=n;i++){ ans=(ans+TwoPow(c[i]-d[i]))%mod; } printf("%lld ",ans); if(T==0) exit(0); int len=unique(c+1,c+n+1)-c-1; for(int i=1;i<=n;i++) a[i]=lower_bound(c+1,c+len+1,a[i])-c; len=unique(d+1,d+n+1)-d-1; for(int i=1;i<=n;i++) b[i]=lower_bound(d+1,d+len+1,b[i])-d; for(int i=1;i<=n;i++){ li[a[i]]=b[i]; } int cnt=0; for(int i=1;i<=n;i++){ if(vis[i]) continue; cnt++;int now=i; while(!vis[now]){ vis[now]=1;now=li[now]; } } printf("%d\n",n-cnt); return 0; }
T2
對於所有主教,我們先計算其主對角線的覆蓋區間,對於副對角線,不難發現其與主對角線的焦點形成了一個區間,直接維護字首和即可。
這個題考試的時候沒有想出來,思考過如何維護交點,沒有發現後面的性質,考試的時候錯誤的用線段樹維護了交點,不僅提高了程式碼複雜度,而且還寫假了。一定要充分考慮對錯之後在動筆。
程式碼:
#include<bits/stdc++.h> #define dd double #define ld long double #define ll long long #define int long long #define uint unsigned int #define ull unsigned long long #define N 1000100 #define M number using namespace std; const int INF=0x3f3f3f3f; const int base=1000000; template<typename T> inline void read(T &x) { x=0; int f=1; char c=getchar(); for(;!isdigit(c);c=getchar()) if(c == '-') f=-f; for(;isdigit(c);c=getchar()) x=x*10+c-'0'; x*=f; } int sum[N<<1],n,m; ll ans; typedef pair<int,int> P; P a[N]; bool vis[N<<1]; //id=0 是上面,id=1 是下面。 inline P GetPoint(int x,int y,int id){ int R=x-y; if(R<0){ if(id==0) return make_pair(1,1-R); else if(id==1) return make_pair(n+R,n); } else{ if(id==0) return make_pair(R+1,1); else if(id==1) return make_pair(n,n-R); } assert(0); } signed main(){ // freopen("my.in","r",stdin); // freopen("my.out","w",stdout); read(n);read(m); for(int i=1;i<=m;i++){ // printf("i=%lld\n",i); int x,y;read(x);read(y); if(!sum[x+y]){ sum[x+y]=1; if(x+y>n+1) ans+=(2*(n+1)-x-y-1); else ans+=x+y-1; } a[i]=make_pair(x,y); // printf("ans=%lld\n",ans); } // printf("ans=%lld\n",ans); for(int i=2;i<=(n<<1);i++){ sum[i]+=sum[i-2]; } for(int i=1;i<=m;i++){ if(vis[a[i].first-a[i].second+base]) continue; vis[a[i].first-a[i].second+base]=1; P up=GetPoint(a[i].first,a[i].second,0); P down=GetPoint(a[i].first,a[i].second,1); int l=up.first+up.second,r=down.first+down.second; ans-=sum[r]-sum[l-2]; ans+=down.first-up.first+1; } printf("%lld",1ll*n*n-ans); return 0; }
T3
這個概率題比較巧妙,是屬於那種看起來轉移帶環,但實際上不帶環的那種。
首先考慮這張圖是基環樹森林,首先考慮樹上的情況:
\[f_i=p_i+(1-p_i)(1-\prod\limits_{(j,i)\in E}f_jq_j) \]這個式子一開始沒有理解,認為後面不需要乘上 \(1-p_i\),但事實上,我們需要把情況分的很清楚,可以通過劃分集合來理解,第一個集合是隕石能感染 \(i\),第二種情況是隕石感染不了 \(i\),這就是上面的轉移。
也就是說,我們需要把情況分的很細,必須要劃分清除。
實際上在做的時候我們也可以這樣轉移:
\[f_i:=1-(1-f_i)(1-f_jq_j) \]這樣正確性也是顯然的,並且更好轉移,\(f_i\)
然後我們考慮環,不難發現,這個轉移是不帶環的。我們考慮一個環:
\[1-2-3-4-...-n-1 \]我們關注 \(1\) 號節點感染的概率,顯然在做完樹形 dp 之後我們可以把 \(f_1\) 看做 \(1\) 號節點被自身感染(即不被環上的點感染的概率)的概率直接代入轉移。
轉移式為:
\[f_1:=f_1+(1-f_1)f_2q_1+(1-f_1)(1-f_2)f_3q_2q_1+.... \]這個轉移是正確性比較顯然,只需要注意我們上面所說的集合劃分,我們要考慮優先順序,不難理解為什麼要乘上 \((1-f_1),(1-f_1)(1-f_2)\),如果暴力對每個點做的話,複雜度將會是 \(O(n^2)\),這是因為環的長度可以到達 \(O(n)\)。我們考慮快速對每個點做上面這個事情。
不難發現,上面這個式子其實是一個迭代形式,它可以轉化成:
\[((f_n\times (1-f_{n-1})q_{n-1}+f_{n-1})\times (1-f_{n-2})+f_{n-3})\times ...+f_1 \]如果我們把 \(f_2,f_3\) 的式子也寫出來,不難發現也是一個迭代形式,只不過順序不同而已,而且我們發現,這個順序好像是有規律的。
我們考慮這個迭代順序怎麼做。
不難發現,乘法和加法可以用矩陣乘法來維護,只要適當構造即可,那麼所謂順序不同,就是矩陣乘法不同而已。
我們維護矩陣乘法字尾積,字首積,就可以做完這道題了。
今天學到了矩陣乘法加速迭代,可以方便改變迭代順序。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=998244353;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
inline int ksm(int a,int b,int mod){
int res=1;while(b){if(b&1) res=1ll*res*a%mod;a=1ll*a*a%mod;b>>=1;}return res;
}
struct Matrix{
int a[2][2];
inline void Clear(){memset(a,0,sizeof(a));}
inline Matrix operator * (const Matrix &b)const{
Matrix c;c.Clear();
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
for(int k=0;k<=1;k++) c.a[i][j]=(c.a[i][j]+1ll*a[i][k]*b.a[k][j]%mod)%mod;
return c;
}
}ma[N],E;
inline int Frac(int a,int b){
return 1ll*a*ksm(b,mod-2,mod)%mod;
}
int p[N],q[N],t[N],n,rd[N];
bool vis[N];
queue<int> qu;
inline void Init(){
read(n);
for(int i=1;i<=n;i++){
int a,b;read(a);read(b);
p[i]=Frac(a,b);
}
for(int i=1;i<=n;i++){read(t[i]);rd[t[i]]++;}
for(int i=1;i<=n;i++){
int a,b;read(a);read(b);
q[i]=Frac(a,b);
}
}
inline void Bfs(){
for(int i=1;i<=n;i++) if(!rd[i]) qu.push(i);
while(qu.size()){
int top=qu.front();qu.pop();
vis[top]=1;
rd[t[top]]--;if(!rd[t[top]]) qu.push(t[top]);
p[t[top]]=(1-1ll*(1-1ll*p[t[top]])*(1-1ll*p[top]*q[top]%mod)%mod)%mod;
}
}
inline Matrix Dfs(int k,Matrix val){
if(vis[k]) return E;vis[k]=1;
Matrix now=ma[k]*Dfs(t[k],val*ma[k]);
p[k]=(now*val).a[1][0];return now;
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
// cout<<(ll)1ll*11*ksm(16,mod-2,mod)%mod;
Init();Bfs();
for(int i=1;i<=n;i++){
ma[i].a[0][0]=1ll*(1-(ll)p[t[i]])*q[i]%mod;
ma[i].a[0][1]=0;ma[i].a[1][1]=1;
ma[i].a[1][0]=p[t[i]];
}E.a[0][0]=E.a[1][1]=1;
for(int i=1;i<=n;i++) if(!vis[i]) Dfs(i,E);
for(int i=1;i<=n;i++){
printf("%lld ",(p[i]%mod+mod)%mod);
}
return 0;
}
T4
首先我們發現正著不好做,我們考慮做補集,於是問題轉化成了給定一張有向圖,求點集的匯出子圖為 \(DAG\) 的方案數。
點數很少,邊數很多,考慮狀壓。因為是 \(DAG\),我們考慮其度數為 \(0\) 節點構成的集合 \(T\)。
設 \(f_S\) 為當點集為 \(S\) 的時候的構成 \(DAG\) 方案數。
如果我們強制欽定 \(T\) 集合中的點度數都為 \(0\),那麼從集合 \(T\) 到 \(S-T\) 可以有邊,但是從 \(S-T\) 到 \(T\) 不能有邊,所以我們有一個式子:
\[f_S=\sum\limits_{T\subseteq S,T\not=\varnothing}2^{way(T,S-T)}f_{S-T} \]但是不難發現上面這個方案會算重,因為 \(f_{S-T}\) 中的某些方案會讓 \(S-T\) 中的點的度數為 \(0\),這並不是我們想要的。我們考慮容斥。
令 \(q_T=2^{way(T,S-T)}f_{S-T}\),令 \(S\) 點集中恰好有集合 \(R\) 中的點為 \(S\) 中度數為 \(0\) 的點,即 \(S\) 中度數為 \(0\) 的點恰好是 \(R\) 中的點的方案數為 \(p_R\),那麼我們有:
\[q_T=\sum\limits_{T\subseteq R}p_R \]注意這裡需要滿足 \(R\subseteq S\)。
稍微解釋一下上面的這個式子,也就是說,對於 \(q_T\) 中的每一種方案,我們把 \(T\) 和 \(S-T\) 中的 \(0\) 度點全部放進 \(R\),這樣,每一種方案就會在 \(p_R\) 中計數一次。這個式子的正確性是顯然的,注意 \(T\) 是不會變的。
運用二項式反演的子集集合,我們可以得到:
\[p_T=\sum\limits_{T\subseteq R}q_R\times (-1)^{|R|-|T|} \]而在本題中,我們要求的相當於:
\[\sum\limits_{T\subseteq S,T\not=\varnothing} p_T=\sum\limits_{T\subseteq S,T\not=\varnothing}\sum\limits_{T\subseteq R\subseteq S}q_R\times (-1)^{|R|-|T|}\\ =\sum\limits_{R\subseteq S,R\not= \varnothing}\sum\limits_{T\subseteq R,T\not= \varnothing}q_R\times (-1)^{|R|-|T|}\\ =\sum\limits_{R\subseteq S,R\not= \varnothing}q_R\sum\limits_{T\subseteq R,T\not= \varnothing} (-1)^{|R|-|T|}\\ =\sum\limits_{R\subseteq S,R\not= \varnothing}q_R\sum\limits_{k=1}^{|R|} (-1)^{|R|-k}\dbinom{|R|}{k}\\ =\sum\limits_{R\subseteq S,R\not= \varnothing}q_R\sum\limits_{k=0}^{|R|-1} (-1)^{k}\dbinom{|R|}{k}\\ =\sum\limits_{R\subseteq S,R\not= \varnothing}q_R([|R|=\varnothing]+(-1)^{|R|-1})\\ =\sum\limits_{R\subseteq S,R\not= \varnothing}q_R\times (-1)^{|R|-1} \]所以我們有:
\[f_S=\sum\limits_{R\subseteq S,R\not=\varnothing}f_{S-R}\times 2^{way(R,S-R)}\times (-1)^{|R|-1} \]然後就可以轉移 \(S\) 了。注意我們如何預處理 \(way(R,S-R)\) 這個東西,我們考慮對每個 \(S\) 都重新預處理,不難發現,我們只需要每次增加一個點就可以轉移。因為 \(way(R,S-R)\) 的預處理,我們需要注意一下 dp 順序,我們令 \(g_R=way(R,S-R)\),我們考慮 \(R\) 要從小到大列舉。
程式碼:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 20
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
inline int lowbit(int x){return x&(-x);}
int lg2[1<<N],Cnt[1<<N],Pre[N],Next[N],n,m,g[1<<N],f[1<<N],Pow[N*N];
inline int sgn(int a){if(a&1) return -1;return 1;}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(m);
//點的編號是 0 到 n-1
for(int i=1;i<=m;i++){
int from,to;read(from);read(to);
from--;to--;
Pre[to]|=(1<<from);Next[from]|=(1<<to);
}
lg2[0]=-1;Pow[0]=1;
for(int i=1;i<1<<N;i++) lg2[i]=lg2[i>>1]+1;
for(int j=1;j<1<<N;j++) Cnt[j]=Cnt[j>>1]+(j&1);
for(int i=1;i<=m;i++) Pow[i]=1ll*Pow[i-1]*2%mod;
f[0]=1;
for(int S=1;S<1<<n;S++){
// printf("S=%d\n",S);
for(int R=S&(S-1);;R=S&(R-1)){
int T=S-R,lb=lowbit(T);
//我們這裡列舉 R=S-T
//從大到小列舉 R,就是從小到大列舉 T。
g[T]=g[T-lb]-Cnt[Pre[lg2[lb]]&T]+Cnt[Next[lg2[lb]]&R];
// printf("g[%d]=g[%d]-Cnt[Pre[%d]&%d]+Cnt[Next[%d]&%d]\n",T,T-lowbit(T),lg2[lowbit(T)]+1,T,lg2[lowbit(T)]+1,R);
// printf("%d=%d-%d+%d\n",g[T],g[T-lowbit(T)],Cnt[Pre[lg2[lowbit(T)]]&T],Cnt[Next[lg2[lowbit(T)]]&R]);
// printf("g[%d]=%d\n",T,g[T]);
f[S]=(f[S]+1ll*sgn(Cnt[T]-1)*Pow[g[T]]*f[R]%mod)%mod;
if(!R) break;
}
// printf("f[%d]=%d\n",S,f[S]);
}
printf("%lld\n",((Pow[m]-f[(1<<n)-1])%mod+mod)%mod);
return 0;
}