牛客CSP-S提升組賽前集訓營1

牛客CSP-S提升組賽前集訓營1

比賽連接c++

官方題解算法

before:T1觀察+結論題,T2樹形Dp,能夠換根或up&down,T3正解妙,轉化爲圖上問題。題目質量不錯,但數據太水了~。數組

A-倉鼠的石子游戲

題目描述

一共n個石子堆,每一個石子堆有ai個石子,兩人輪流對石子塗色(先手塗紅,後手塗藍),且須要保證當前回合塗的石子顏色不能和它相鄰的兩個同色,誰塗不下去誰輸。一共T個詢問,對於每一個詢問輸出先手必勝仍是後手必勝。測試

\(1<=n<=10^3,1<=ai<=10^9,1<=T<=10^2\)優化

題解

標籤:模擬,猜結論spa

當n=1時,模擬一下能夠發現,除非石子數=1,不然後手必勝。當增長多堆石子時,假設當前局面是P必敗,則P確定要去新的一堆裏塗,則對於新的一堆石子P成爲了先手,假設新的一堆石子數>1,則P任然必敗,反之能夠轉移,P的另外一方成爲了下一堆的先手。也就是說只有數量爲1的石子堆可以改變勝負狀態。調試

因此,只需統計石子數爲1的堆的數量\(cnt1\),根據\(cnt1\)的奇偶性來判斷。若爲奇,則先手必勝;反之先手必敗。code

#include<bits/stdc++.h>
using namespace std;
inline int read(){}
int main(){
     int T=read();
     while(T--){
        int n=read();
        bool o=0;
        for(register int i=1;i<=n;i++){
            int x=read();
            if(x==1)o^=1;  
        }
        if(o==0)puts("hamster");
        else puts("rabbit");
     }
}

B- 乃愛與城市擁擠程度

題目描述

見原題面blog

題解

標籤:樹形Dp,換根法,Up and Down遊戲

暴力作法是\(O(N^2)\)的,將每一個點都提作根跑一遍樹形dp。優化很明顯是去換根

因爲標程給的換根法寫的很是板子(雖然有點長),因此這裏按標程的思路走一遍,整理一下換根法的大體思路。

step1:預處理dfs

隨便找一個節點做爲根,進行dfs/樹形Dp,處理出全部節點此時的答案(只考慮本身子樹下的答案)。

step2:換根

平時作題打換根都是直接去統計,加加減減,雖然碼量小但不一樣的題目細節不一樣,調試起來不太方便。標程中將換根分爲兩部分。第一部分\(cut\),先分別消除彼此的影響,逆操做;第二部分\(link\),對更改後的數組正操做一遍。

在本題中有兩個詢問。弄兩個數組分別針對題目的兩個詢問,第一個比較好理解,第二個比較抽象,根據代碼y一下。

\(dp[x][i]\):換根前,只考慮x的子樹時,與x距離爲i的子孫數量。

\(dp2[x][i]\):換根前,只考慮x的子樹時,當影響範圍爲i時,x影響到的子孫的擁擠程度之乘積。

將節點1做爲根,預處理dfs一遍。下面的操做都是將\(sonx\)的貢獻加給\(x\)正操做

void dp_dfs(int x,int fa){
    dp[x][0]=1;
    for(int i=0;i<=k;++i)dp2[x][i]=1;
    for(int i=0;i<G[x].size();++i){
        if(G[x][i]!=fa){
            dp_dfs(G[x][i],x);
            for(int j=1;j<=k;++j){
                dp[x][j]+=dp[G[x][i]][j-1];
                dp2[x][j]=dp2[x][j]*dp2[G[x][i]][j-1]%mod;
            }
        }
    }
    long long sum=0;
    for(int i=0;i<=k;++i){
        sum+=dp[x][i];
        dp2[x][i]=dp2[x][i]*sum%mod;
    }
}

這一遍dfs後,此時的根節點1已經能夠直接統計出答案了,但對於其餘非根節點來講,它們只考慮了本身子樹內的貢獻,還需考慮子樹外的貢獻。

接下來再dfs一遍,換根。

void change_root(int root1,int root2) {
    //原來的根:root1 如今要換爲:root2,且root1以前是root2的父節點
    cut_dp(root1,root2);//逆操做
    link_dp(root2,root1);//正操做,從新統計一遍
}

逆操做、正操做都與以前的預處理dfs相似。

標程的換根法代碼

無根樹Dp還能夠up and down去作。網上關於這一塊內容很少,並且不少blog都說和換根法差很少。

好比對於這題的詢問一,要求樹中離x距離小於等於k的節點數目,能夠分解爲這樣兩個問題,①x子樹中距離小於等於k的節點數目+②x子樹外距離小於等於k的節點數目,對這兩個問題分別dfs一遍,問題①用兒子更新本身,問題②用本身更新兒子,最後將兩部分的貢獻合起來。好像確實和換根差很少,但實現起來感受up&down的思路更清晰些。

下面是up&down的代碼

#include<bits/stdc++.h>
#define Mod 1000000007
#define int long long
using namespace std;
const int N=1e5+10;
vector<int>g[N];
int num[N][11],dp[N][11];
int outnum[N][11],outdp[N][11];
int n,k,ans1[N],ans2[N];
namespace UpandDown{
    int ksm(int x,int d){
        int res=1;
        while(d){
            if(d&1)res=res*x%Mod;
            x=x*x%Mod;d>>=1;
        }
        return res;
    }
    void dfs(int x,int fa){
        for(int i=0;i<g[x].size();i++)if(g[x][i]!=fa)dfs(g[x][i],x);
        num[x][0]=1,dp[x][0]=1;
        for(int i=1;i<=k;++i){
            num[x][i]=1,dp[x][i]=1;
            for(int j=0;j<g[x].size();++j){
                int y=g[x][j];if(y==fa)continue;
                num[x][i]+=num[y][i-1];
                dp[x][i]=dp[x][i]*dp[y][i-1]%Mod;
            }
            dp[x][i]=dp[x][i]*num[x][i]%Mod;        
        }
    }
    void redfs(int x,int fa){
        if(x==1){
            ans1[x]=num[x][k];
            ans2[x]=dp[x][k];
        }
        else{
            ans1[x]=num[x][k]+outnum[x][k];
            ans2[x]=dp[x][k]*ksm(num[x][k],Mod-2)%Mod*(num[x][k]+outnum[x][k])%Mod*outdp[x][k]%Mod;
        }
        for(int i=0;i<g[x].size();i++){
            int y=g[x][i];if(y==fa)continue;
            outnum[y][1]=1,outdp[y][1]=1;
            for(int j=2;j<=k;j++){
                outnum[y][j]=outnum[x][j-1]+num[x][j-1]-num[y][j-2];
                outdp[y][j]=outdp[x][j-1]*dp[x][j-1]%Mod*ksm(dp[y][j-2],Mod-2)%Mod*
                ksm(num[x][j-1],Mod-2)%Mod*(num[x][j-1]-num[y][j-2]+outnum[x][j-1])%Mod;
            }
            redfs(y,x);
        }
    }
    void solve(){
        for(int i=1;i<=k;i++)for(int j=1;j<=k;j++)outdp[i][j]=1;
        dfs(1,0),redfs(1,0);
        for(int i=1;i<=n;i++)printf("%lld ",ans1[i]);
        printf("\n");
        for(int i=1;i<=n;i++)printf("%lld ",ans2[i]);
    }
}
signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<n;i++){
        int u,v;scanf("%lld%lld",&u,&v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    
    UpandDown::solve();
    return 0;
}

C- 小w的魔術撲克

題目描述

小w有\(k\)張魔術撲克,第i張魔術撲克有兩面數值\(ai,bi\)(可能相等),牌面的數值在\([1,n]\)。如今有q個詢問,每一個詢問有兩個整數\(l,r\),詢問是否能用若干張魔術撲克湊成\(l,l+1,..,r-1,r\)這個順子,每張魔術撲克只能使用一面的值。

對於30%測試點,\(1<=n<=11\)\(1<=k<=10\)\(1<=q<=100\)

對於60%測試點,\(1<=n,k<=50\)\(1<=q<=500\)

對於100%測試點,\(1<=n,k,q<=10^5\)\(1<=l<=r<=n\)

題解

標籤:思惟題,圖論,並查集,離線詢問,樹狀數組

30pts

二進制枚舉。\(O(2^n*n)\)

60pts

這種一一匹配的關係容易聯想到二分圖。令左邊爲\([1,n]\)的牌面,右邊爲魔術撲克的編號。創建兩兩聯通關係,對於每一個牌面l(\(l∈[1,n]\)),維護一個\(Ma[l]\),表示若能打出l,則能打出的連續最右位置爲Ma[l],那麼對於每一個詢問\((l,r)\),只用判斷\(Ma[l]>=r?可行:不可行\)

如何維護?上面已經提到二分圖了,若是直接枚舉l,Ma[l],而後再用匈牙利算法判一下,複雜度比較大。因爲這個Ma顯然具備單調性,考慮一邊尺取,一邊跑匈牙利算法

代碼以下,時間複雜度爲\(O(N^3)\)可是因爲垃圾數據賽時是能夠A了這題的

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,bf[N],gf[N];
int mark[N],Ma[N];
vector<int>g[N];
bool grab(int x){
    if(mark[x])return 0;
    mark[x]=1;
    for(int i=0;i<g[x].size();i++){
        int y=g[x][i];
        if(!bf[y]||grab(bf[y])){
            bf[y]=x;
            gf[x]=y;
            return 1;   
        }
    }
    return 0;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y;scanf("%d%d",&x,&y);
        g[x].push_back(i);
        g[y].push_back(i);
    }
    for(register int l=1,r=0;l<=n;l++){
        bf[gf[l-1]]=0;
        r=max(r,l-1);
        memset(mark,0,sizeof(mark));
        while(r<n&&grab(r+1)){
            r++;
            memset(mark,0,sizeof(mark));
        }
        Ma[l]=r;
    }
    int q,l,r;scanf("%d",&q);
    for(register int i=1;i<=q;i++){
        scanf("%d%d",&l,&r);
        if(Ma[l]>=r)puts("Yes");
        else puts("No");
    }
}

100pts

數據範圍加到\(10^5\)。把問題拎到圖上去,建模。

對於一張魔術撲克\((a,b)\),將編號爲\((a,b)\)的節點先連一條無向邊,這樣會連出共\(k\)條邊,可能會有重邊,可能會造成多個聯通塊,每一個聯通塊可能包含環也可能只是一棵樹。如今對於詢問\((l,r)\),你須要將其中的若干條無向邊變成有向邊,也就是指定這條邊指向兩個端點中的一個,使得\((l,...,r)\)中的點的入度均不爲0。

如今這個問題就是原問題套件衣服,不過如今到了圖上看起來比較可作

引理1:對於無向連通圖S。試將全部無向邊改成有向邊,當且僅當S中存在至少一個環(包括自環)時,可以使得S中的全部節點入度不爲0。

證實:若是不存在環,那就是一棵普通的樹,對於每條邊,最優排布時應該是讓每條邊都指向深度較大的那個(指向兒子),但最後對於根節點,它的入度爲0,不合法。若是這時候樹上出現了環,由人類智慧可知,會有多餘的邊剩給根節點,此時必定合法。

也就是說,當咱們詢問的\((l,r)\)時,只要存在一個不含環的聯通塊,知足這個聯通塊內的節點編號均被l,r覆蓋,則不存在可行解能打出\([l,r]\)的順子。

這就變成了一個簡單的線段覆蓋問題。接下來作法就不少了樹狀數組,set之類的,能夠參考官方題解。下面給出一種較簡潔的作法。

理一遍流程,在輸出魔術撲克\((a,b)\)時,利用並查集維護聯通塊的信息,若是此前\(a,b\)已經在同一聯通塊,則合併這條邊後,這個聯通塊就有了環,標記一下。連完邊後,找出全部不含環的聯通塊,記該聯通塊內編號最大的節點爲\(ma\),編號最小的節點爲\(mi\),則視\([mi,..,ma]\)爲一條線段,對於詢問直接看\((l,r)\)會不會把某條線段覆蓋了,若是會覆蓋某條線段則輸出No,反之Yes。看會不會把某條線段覆蓋能夠離線詢問,而後對詢問和原有線段都排個序掃一遍就行了。

時間複雜度爲\(O(nlogn)\)

代碼以下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
int fa[N],mi[N],ma[N],huan[N],cover[N];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
struct seg{
    int l,r,id;
}que[N];
vector<seg>g;
inline bool cmp(seg p,seg q){return p.r<q.r;}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)fa[i]=mi[i]=ma[i]=i;
    for(int i=1;i<=m;i++){
        int x,y;scanf("%d%d",&x,&y);
        int A=find(x),B=find(y);
        if(A==B)huan[A]=1;
        else{
            fa[A]=B;
            huan[B]|=huan[A];
            ma[B]=max(ma[B],ma[A]);
            mi[B]=min(mi[B],mi[A]);
        }
    }
    for(int i=1;i<=n;i++){
        if(find(i)==i&&!huan[i])g.push_back((seg){mi[i],ma[i],0});
    }
    int q;
    scanf("%d",&q);
    for(int i=1,a,b;i<=q;i++){
        scanf("%d%d",&a,&b);
        que[i]=(seg){a,b,i};
    }
    sort(g.begin(),g.end(),cmp);
    sort(que+1,que+q+1,cmp);
    for(int i=1,cur=0,maxl=0;i<=q;i++){
        while(cur<g.size()&&g[cur].r<=que[i].r){
            maxl=max(maxl,g[cur].l);cur++;
        }
        if(maxl>=que[i].l)cover[que[i].id]=1;
    }
    for(int i=1;i<=q;i++)puts(cover[i]?"No":"Yes");
}
相關文章
相關標籤/搜索