1. 程式人生 > 實用技巧 >NOIP2015提高組Day1-T3鬥地主——DFS+模擬

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;
}

完結撒花!