1. 程式人生 > 其它 >10.18 正睿模擬賽題解

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\)

的初始值就是 \(p_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;
}