NOIP2015提高組Day1

一、神奇的幻方

數據:對於100%,n∈[1,39],且n必爲奇數
模擬題。沒什麼好講的。

重點是,noip2017的提高組初賽是這道題……

二、信息傳遞

數據:對於60%,n∈[1,2500]
對於100%,n∈[1,200000]

題目裏給的序列可以變成一張圖,根據題目意思在自己畫一個有向圖,發現就是求一個圖上的環的長度。而且只有n條邊,那麼只有一個環。如果學了強聯通,那麼就是裸題了。但是不會,只能換方法。

因爲只有一個環,環是連通的,想想以前的壓縮路徑。並查集,把每個點都壓起來,壓起來後如果找到的父親是一樣的。那麼就是說環變成了點,那麼此時再dfs就行了。

這個比暴力的快就是在判斷什麼時候dfs。暴力是每個點都dfs,而這個只dfs一次。這樣就從n^2變到n了。

#include<bits/stdc++.h>
#define M 200005
using namespace std;
int n,A[M],ans=1e9,H[M],Fa[M];
int Find(int x){return Fa[x]==x?x:Fa[x]=Find(Fa[x]);}
bool Q[M];
void f(int x,int t){//dfs
    Q[x]=1;H[x]=t;
    if(Q[A[x]]==0)f(A[x],t+1);
    else ans=min(ans,H[x]-H[A[x]]+1);
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&A[i]);
    for(int i=1;i<=n;i++)Fa[i]=i;
    for(int i=1;i<=n;i++){
        if(Find(i)!=Find(A[i])){//當父親不相同的時候,就一直壓縮
            int x=Find(i),y=Find(A[i]);
            Fa[x]=y;
        }else{
            memset(Q,0,sizeof Q);
            f(i,1);
        }
    }
    printf("%d\n",ans);
    return 0;
}

三、鬥地主

數據:
這裏寫圖片描述

考試肯定先打暴力。前30分if語句就足夠了。

然後寫法很多:深搜寫得好有100分(我一分沒有);廣搜有80分;dp的預處理加深搜有100分。

講講dp的預處理加深搜。

其實很好理解。如果沒有順子這種情況,那麼現在手上的牌的大小和花色都沒有任何影響。唯一有影響的只是每種牌的張數。那麼就可以先預處理。dp[i][j][k][l] 表示當有i個一張,j個兩張,k個三張,l個四張時打完要的次數(不包括順子的情況)。轉移也很簡單,只是情況有點多。對於一個dp[i][j][k][l],當滿足條件的時候(即某種張數的牌有的時候),可以合併一些,如四代二,三代一,三代二之類的。還可以把四張的分成三張加一張,兩張加兩張(爲了組合其他的可能性),三張變成兩張加一張,兩張編織成一張加一張。

總的來說dp的轉移分成三大類:
1、最初始的情況,多一個一、二、三、四張,往前要值
2、合併一些,3+2,3+1,4+1+1,4+2+2
3、拆分一些,2=1+1,3=2+1,4=3+1,4=2+2

預處理好了就是深搜的事了。既然除了順子的都已經處理好了,那麼只要考慮各種順子的情況就行了。順子分成單張、兩張和三張三種情況,分類一下就行了,相比一開始的暴力深搜,這簡單多了。

Code:

#include<bits/stdc++.h>
#define M 25
using namespace std;
int cas,n,dp[25][25][25][25],cnt[20],ans,Cnt[10];
void Init(){
    memset(dp,63,sizeof dp);
    dp[0][0][0][0]=0;
    for(int l=0;l<=n;l++)
        for(int k=0;k<=n;k++)
            if(l*4+k*3<=n)for(int j=0;j<=n;j++)
                if(l*4+k*3+j*2<=n)for(int i=0;i<=n;i++)
                    if(l*4+k*3+j*2+i<=n){
                        //第一類,多一張,向以前要值
                        if(i)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i-1][j][k][l]+1);
                        if(j)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j-1][k][l]+1);
                        if(k)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j][k-1][l]+1);
                        if(l)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j][k][l-1]+1);
                        //第二類,合併
                        if(l>=2)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j][k][l-2]+1);//4+4,特殊情況,炸彈看成兩個對子
                        if(k&&i)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i-1][j][k-1][l]+1);//3+1
                        if(k&&j)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j-1][k-1][l]+1);//3+2
                        if(l&&i>=2)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i-2][j][k][l-1]+1);//4+2+2
                        if(l&&j>=2)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j-2][k][l-1]+1);//4+1+1
                        if(l&&j)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j-1][k][l-1]+1);//4+1+1,特殊情況,對子看成兩個單張
                        //第三類,拆分
                        if(j)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i+2][j-1][k][l]);//2=1+1
                        if(k)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i+1][j+1][k-1][l]);//3=2+1
                        if(l)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i+1][j][k+1][l-1]);//4=3+1
                        if(l)dp[i][j][k][l]=min(dp[i][j][k][l],dp[i][j+2][k][l-1]);//4=2+2
                    }
}
void f(int res){//res是現在的各類順子的個數
    if(res>ans)return;//最優性剪枝
    memset(Cnt,0,sizeof Cnt);
    for(int i=0;i<=14;i++)Cnt[cnt[i]]++;//Cnt記錄現在有多少組一二三四的牌
    ans=min(ans,dp[Cnt[1]][Cnt[2]][Cnt[3]][Cnt[4]]+res);
    for(int i=3;i<=14;i++){
        for(int j=5;j<=13;j++){//單順子
            int p1=1;
            for(int k=i;k<=i+j-1;k++)if(cnt[k]<1)p1=0;
            if(!p1)break;
            for(int k=i;k<=i+j-1;k++)cnt[k]--;
            f(res+1);
            for(int k=i;k<=i+j-1;k++)cnt[k]++;
        }
        for(int j=3;j<=13;j++){//雙順子
            int p1=1;
            for(int k=i;k<=i+j-1;k++)if(cnt[k]<2)p1=0;
            if(!p1)break;
            for(int k=i;k<=i+j-1;k++)cnt[k]-=2;
            f(res+1);
            for(int k=i;k<=i+j-1;k++)cnt[k]+=2;
        }
        for(int j=2;j<=13;j++){//三順子
            int p1=1;
            for(int k=i;k<=i+j-1;k++)if(cnt[k]<3)p1=0;
            if(!p1)break;
            for(int k=i;k<=i+j-1;k++)cnt[k]-=3;
            f(res+1);
            for(int k=i;k<=i+j-1;k++)cnt[k]+=3;
        }
    }
}
int main(){
    scanf("%d%d",&cas,&n);
    Init();
    while(cas--){
        memset(cnt,0,sizeof cnt);
        for(int i=1;i<=n;i++){
            int x,y;
            scanf("%d%d",&x,&y);
            if(x==1)cnt[14]++;
            else cnt[x]++;
        }
        ans=n;
        f(0);
        printf("%d\n",ans);
    }
    return 0;
}

其實還好,只是考試的時候一般是想不出來的。思路理清楚了還是可以的。

對於第二題還有一個,對於所有圖、樹的題,弗洛伊德肯定要打,暴力但是準確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章