1. 程式人生 > >$[ ZJOI 2006 ] Mahjong$

$[ ZJOI 2006 ] Mahjong$

def ostream val space ios dig 方案 string 否則


\(\\\)

\(Description\)


現有權值分別為\(1\text~100\)\(100\)種牌,分別給出每種排的張數\(A_i\),試判斷能否胡牌,胡牌需要將所有牌不重不漏地分成以下幾類:

  • 三張或四張相同的牌
  • 權值連續的三張牌
  • 兩張相同的牌,這一類必須要有,而且只能有一個

一組數據共需要\(N\)次判斷,胡了輸出“\(Yes\)”,否則輸出"\(No\)"。

  • \(N\in [1,100]\)\(A_i\in [0,100]\)

\(\\\)

\(Solution\)


沒有寫貪心,但是找到了兩種\(DP\)的方式,按照便於理解的順序介紹:

\(\\\)

\(\text O(N\times 100^3)\)

  • 註意到每張牌只會影響到周圍至多三種牌的出牌,而兩兩關系又可以互相導出,所以設\(f[i][j][k][0/1]\)表示,當前處理到第\(i\)種牌,第\(i-1\)種牌出了\(j\)張,第\(i\)種牌出了\(k\)張,有\(/\)沒有計算過對子,其余更靠前的牌全部出完是否可行,至於保證全部出完,後面再給證明。有顯然的初始化\(f[0][0][0][0]=1\)

  • 對於對子有特殊轉移:

     if(k>1) f[i][j][k][1]|=f[i][j][k-2][0];
  • 當前成立一個三張相同的牌,註意對子部分只能從相同的種類轉移:

    if(k>2) {f[i][j][k][1]|=f[i][j][k-3][1];f[i][j][k][0]|=f[i][j][k-3][0];}
  • 當前成立一個四張相同的牌,討論相同:

    if(k>3) {f[i][j][k][1]|=f[i][j][k-4][1];f[i][j][k][0]|=f[i][j][k-4][0];}
  • 註意到上面三種只對當前有要求,而成立三張連續的將會關系到前後的答案,因為確定後面的影響很不方便,我們不妨設三張連續的當前位置只考慮以當前的牌結束的部分。同樣的,因為不便於考慮出多少組三張連續的牌,但註意到轉移過程中會逐層轉移自之前的情況,所以最優組合方案無需枚舉,我們直接令當前的牌全部用於出連續三個一組的類型。同時為了保證除掉當前兩種前面的全部都出完,所以向前找狀態時默認第\(i-2\)種牌全部出完:

    if(j>=k&&a[i-2]>=k){
        f[i][j][k][1]|=f[i-1][a[i-2]-k][j-k][1];
        f[i][j][k][0]|=f[i-1][a[i-2]-k][j-k][0];
    }
  • 答案即為\(f[100][A_{99}][A_{100}][1]\)是否為真。

#include<cmath>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 1010
#define R register
#define gc getchar
using namespace std;

int a[N];
bool f[N][N][N][2];

inline int rd(){
  int x=0; bool f=0; char c=gc();
  while(!isdigit(c)){if(c=='-')f=1;c=gc();}
  while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
  return f?-x:x;
}

int main(){
  int t=rd();
  while(t--){
    for(R int i=1;i<=100;++i) a[i]=rd();
    memset(f,0,sizeof(f)); f[0][0][0][0]=1;
    for(R int i=1;i<=100;++i)
      for(R int j=0;j<=100;++j)
        for(R int k=0;k<=100;++k){
          if(k>1)f[i][j][k][1]|=f[i][j][k-2][0];
          if(k>2){f[i][j][k][1]|=f[i][j][k-3][1];f[i][j][k][0]|=f[i][j][k-3][0];}
          if(k>3){f[i][j][k][1]|=f[i][j][k-4][1];f[i][j][k][0]|=f[i][j][k-4][0];}
          if(j>=k&&a[i-2]>=k){
            f[i][j][k][1]|=f[i-1][a[i-2]-k][j-k][1];
            f[i][j][k][0]|=f[i-1][a[i-2]-k][j-k][0];
          }
        }
    puts(f[100][a[99]][a[100]][1]?"Yes":"No");
  }
  return 0;
}

\(\\\)

\(\Theta(N\times 100\times 3^3)\)

  • 思路相同,但轉化成了分別討論相同的牌和連續的牌不同的出法。

  • 註意到其實兩類之間是可以互相轉換的,具體地說,對於連續三張的出法,一樣的三張牌的組成超過三組就是沒意義的了,因為它完全可以拆成三張牌獨立出各自的一套,剩下的再組成三張連續出的方法,例如五組相同的組成\((5\times 3)\)就可以拆成三種牌各自出一套再加上兩組\((3\times 3\times 1+2\times3)\),或三種牌各自出一套四張一族的,再加上一組\((3\times 4\times 1+1\times 3)\)。我們發現,去掉所有的連續三張一組的部分以及一對的部分,如果剩下的每一類牌的數量都可以表示成\(3x+4y\)的形式,那麽這個方案就是合法的。

  • 於是狀態就能神奇的設計為(以下稱相同的牌三個或四個一組的為一套,稱連續三張一組的為順),\(f[i][0/1/2][0/1/2][0/1]\)表示當前處理到第\(i\)張牌,以第\(i-1\)張牌開始的順出了\(0/1/2\)個,以第\(i\)張牌開始的順出了\(0/1/2\)個,第\(i-1\)種和第\(i\)種其他的牌全部出成套,沒出\(/\)出過對子的狀態是否合法。同樣有顯然初始化\(f[0][0][0][0]=1\),註意預處理出所有能表示成形如\(3x+4y\)形式的數。

  • 轉移考慮以第\(i+1\)個數為開始出了\(s\)個順,註意要從合法的狀態擴展,此時第\(i+1\)種牌剩余出成套的數即為\(A_{i+1}-j-k-s\),若該數字查表合法則轉移。

  • 關於\(0/1\)狀態間的轉移,只需在每次枚舉到\(0\)狀態之後,討論兩種擴展即可。

#include<cmath>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 110
#define R register
#define gc getchar
using namespace std;

int a[N]={0};
bool val[100]={1},f[N][3][3][2];

inline int rd(){
  int x=0; bool f=0; char c=gc();
  while(!isdigit(c)){if(c=='-')f=1;c=gc();}
  while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
  return f?-x:x;
}

int main(){
  int t=rd();
  for(R int i=0;i<=35;++i)
    for(R int j=0;j<=25;++j)
      if(3*i+4*j<=100) val[3*i+4*j]=1;
  while(t--){
    memset(f,0,sizeof(f));
    f[0][0][0][0]=1;
    for(R int i=1;i<=100;++i) a[i]=rd();
    for(R int i=0;i<=99;++i)
      for(R int j=0;j<=2;++j)
        for(R int k=0;k<=2;++k){
          if(f[i][j][k][0])
            for(R int s=0;s<=2;++s){
              int num=a[i+1]-j-k-s;
              if(num>=0&&val[num]) f[i+1][k][s][0]=1;
              if(num-2>=0&&val[num-2]) f[i+1][k][s][1]=1;
            }
          if(f[i][j][k][1])
            for(R int s=0;s<=2;++s){
              int num=a[i+1]-j-k-s;
              if(num>=0&&val[num])f[i+1][k][s][1]=1;
            }
        }
    puts(f[100][0][0][1]?"Yes":"No");
  }
  return 0;
}

$[\ ZJOI\ 2006\ ]\ Mahjong$