NOIP2015提高組Day1-T3鬥地主——DFS+模擬
NOIP2015 Day1 T3 鬥地主-題解
1.題目傳送門:https://www.luogu.com.cn/problem/P2668
2.題目分析:題目的演算法一般是由題目的資料範圍決定的。舉個例子,如果N<=20且包含決策的內容,一般就是狀態壓縮DP。這道題的資料範圍如圖所示:
如圖所示,本體的資料範圍非常小,T<=10,N<=23,所以我們可以使用一些比較基礎的“笨”演算法。這道題的題意我們都理解了,我們需要在每個牌組中尋找最優的出牌組合,所以我們可以DFS搜尋每一種情況,時間複雜度為O(能過)。
3.講解本題:
首先提出一個易錯點,這道題雖然有T組資料,但每組的卡牌的數量都是n,所以T和n都要在最前面輸入。此外,這道題資料量較大,所以推薦使用read()快速讀入,這裡提供一個模板:
int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
首先我們可以注意到,這道題中花色對答案沒有任何影響,可以直接忽略。
在讀入時,我們使用sum陣列來記錄卡牌張數,其中sum[i]=j表示點數為i的卡牌有j張。我們注意到大小王的點數都是0,所以
if(w==0) sum[15]++;
sum[15]來表示大小王的數量。特殊的,如果輸入的點數是1,也就是A,把它存到
else if(w==1) sum[14]++;
else sum[w]++;
在DFS函式中我們存一個變數x來表示當前經歷了多少個回合,所以我們從DFS(0)開始。
solve(0);
printf("%d\n",ans);
好了現在開始寫DFS函式。
在函式的開端部分,我們加一個退出條件
if(x>=ans) return ;
好了現在開始正式的部分。
對已每一種出牌組合,我們都寫一個程式碼模組來解決問題。讓我們從順子開始。
第一模組:單順子
單順子的出牌條件是有至少5張能形成順子的牌。同時注意A和大小王是不能參與順子的。我們放出一個dsz變數來記錄當前有多少張牌能形成順子。我們的迴圈變數i從3開始(避開2(2牌)),到14結束(避開15(大小王))。然後如果在找順子的過程中有一個點數的牌的數量不夠1張,那麼這組順子就構造失敗。如果dsz成功達到了5,那我們就可以愉快的打出去了。但是值得注意的是,這樣打出來並不一定是最優的,所以這樣只是一個嘗試。j迴圈對所有點數的牌都減掉1張,進入下一輪DFS,然後再回溯。
int dsz=0;
for(int i=3;i<15;++i)
{
if(sum[i]<1) dsz=0;
else
{
dsz++;
if(dsz>=5)
{
for(int j=i-dsz+1;j<=i;j++) sum[j]--;
solve(x+1); //進入下一回合的遊戲
for(int j=i-dsz+1;j<=i;j++) sum[j]++;//回溯
}
}
}
第二模組:雙順子
雙順子和單順子的流程其實是一樣的。只不過有以下幾點變化:
1.因為是雙順子,所以如果一個點數的牌不夠兩張就要continue。
2.雙順子並不需要5個點數,3個就夠了,所以如果長度變數ssz大於等於3就可以打出去。
int ssz=0;
for(int i=3;i<15;++i)
{
if(sum[i]<2) ssz=0;
else
{
ssz++;
if(ssz>=3)
{
for(int j=i-ssz+1;j<=i;j++) sum[j]-=2;
solve(x+1);//進行下一回合的遊戲
for(int j=i-ssz+1;j<=i;j++) sum[j]+=2;//回溯
}
}
}
第三模組:三順子
三順子也是同理:
1.因為是三順子,所以如果一個點數的牌不夠三張就要continue。
2.雙順子並不需要3個點數,2個就夠了,所以如果長度變數sasz大於等於2就可以打出去。
int sasz=0;
for(int i=3;i<15;++i)
{
if(sum[i]<3) sasz=0;
else
{
sasz++;
if(sasz>=2)
{
for(int j=i-sasz+1;j<=i;j++) sum[j]-=3;
solve(x+1);
for(int j=i-sasz+1;j<=i;j++) sum[j]+=3;
}
}
}
到目前為止,順子牌我們就已經都處理完了,接下來解決帶牌問題。
第四模組:三帶X
題目中有三帶一和三帶二。
這裡我們使用加法原理“分步”的思想。第一步查詢誰來帶,因為是三帶X,所以應該至少有3張。
在找到合法的點數牌後,我們就sum[i]-=3。
然後進行第二步,查詢X是誰。這裡要注意的細節是這兩個牌的點數是不可以相同的。3張3帶1張3是不合法的。所以要特判i==j的情況。
三帶二同理。
程式碼如下:
for(int i=2;i<15;i++)
{
if(sum[i]<3) continue;
sum[i]-=3;
for(int j=2;j<=16;j++)
{
if(i==j||sum[j]<1) continue;
sum[j]--;
solve(x+1);
sum[j]++;
}
for(int j=2;j<=16;j++)
{
if(i==j||sum[j]<2) continue;
sum[j]-=2;
solve(x+1);
sum[j]+=2;
}
sum[i]+=3;
第五模組:四帶二
首先如果sum[i]多於3張,那麼i就有能力成為這個帶牌的人員。然後使用和三帶X一樣的思路分步列舉後面的被帶的2張牌,就可以完美解決。
if(sum[i]>3)
{
sum[i]-=4;
for(int j=2;j<=15;j++)
{
if(sum[j]<1||i==j) continue;
sum[j]--;
for(int k=2;k<=15;k++)
{
if(sum[k]<1||k==i||(k==j&&k!=15)) continue;
sum[k]--;
solve(x+1);
sum[k]++;
}
sum[j]++;
}
for(int j=2;j<=14;j++)
{
if(sum[j]<2||i==j) continue;
sum[j]-=2;
for(int k=2;k<=14;k++)
{
if(sum[k]<2||k==i||k==j) continue;
sum[k]-=2;
solve(x+1);
sum[k]+=2;
}
sum[j]+=2;
}
sum[i]+=4;
}
}
最後,如果最後還是由一些牌沒法經過組合打出去,那我們就不得不使用單牌的方式打出去。
for(int i=2;i<=15;i++) if(sum[i]) x++;
ans=min(ans,x);
然後把ans輸出就好了。
接下來給出完整的程式碼:
#include<bits/stdc++.h>
#define MAXN 30000
#define inf 1000000007
using namespace std;
int T,n,ans;
int sum[MAXN];
int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
void solve(int x)
{
if(x>=ans) return ;
//part1: 單順子
int dsz=0;
for(int i=3;i<15;++i)
{
if(sum[i]<1) dsz=0;
else
{
dsz++;
if(dsz>=5)
{
for(int j=i-dsz+1;j<=i;j++) sum[j]--;
solve(x+1);
for(int j=i-dsz+1;j<=i;j++) sum[j]++;
}
}
}
//part2: 雙順子
int ssz=0;
for(int i=3;i<15;++i)
{
if(sum[i]<2) ssz=0;
else
{
ssz++;
if(ssz>=3)
{
for(int j=i-ssz+1;j<=i;j++) sum[j]-=2;
solve(x+1);
for(int j=i-ssz+1;j<=i;j++) sum[j]+=2;
}
}
}
//part3: 三順子
int sasz=0;
for(int i=3;i<15;++i)
{
if(sum[i]<3) sasz=0;
else
{
sasz++;
if(sasz>=2)
{
for(int j=i-sasz+1;j<=i;j++) sum[j]-=3;
solve(x+1);
for(int j=i-sasz+1;j<=i;j++) sum[j]+=3;
}
}
}
//part4: 帶牌部分
for(int i=2;i<15;i++)
{
if(sum[i]<3) continue;
sum[i]-=3;
for(int j=2;j<=16;j++)
{
if(i==j||sum[j]<1) continue;
sum[j]--;
solve(x+1);
sum[j]++;
}
for(int j=2;j<=16;j++)
{
if(i==j||sum[j]<2) continue;
sum[j]-=2;
solve(x+1);
sum[j]+=2;
}
sum[i]+=3;
if(sum[i]>3)
{
sum[i]-=4;
for(int j=2;j<=15;j++)
{
if(sum[j]<1||i==j) continue;
sum[j]--;
for(int k=2;k<=15;k++)
{
if(sum[k]<1||k==i||(k==j&&k!=15)) continue;
sum[k]--;
solve(x+1);
sum[k]++;
}
sum[j]++;
}
for(int j=2;j<=14;j++)
{
if(sum[j]<2||i==j) continue;
sum[j]-=2;
for(int k=2;k<=14;k++)
{
if(sum[k]<2||k==i||k==j) continue;
sum[k]-=2;
solve(x+1);
sum[k]+=2;
}
sum[j]+=2;
}
sum[i]+=4;
}
}
for(int i=2;i<=15;i++) if(sum[i]) x++;
ans=min(ans,x);
}
int main()
{
T=read();
n=read();
while(T--)
{
memset(sum,0,sizeof(sum));
ans=inf;
for(int i=1;i<=n;++i)
{
int w,c;
w=read();
c=read();
if(w==0) sum[15]++;
else if(w==1) sum[14]++;
else sum[w]++;
}
solve(0);
printf("%d\n",ans);
}
return 0;
}