P2540 [NOIP 2015 提高组] 斗地主 加强版 题解

P2540 [NOIP 2015 提高组] 斗地主 加强版

更好的阅读体验?

强烈建议在 cnblogs 上查看代码。

不知道是啥做法。

可能是双向广搜或者 IDA?


Solution:

首先将 \(1\) 点变为 \(14\) 点,其余不变。

发现题目要求我们求最小出牌次数。

因为 BFS 天然满足如果搜到即为最优解的性质,所以考虑 BFS 解决。

我们思考一下或参考题解,发现该题状态集合具有对称性,所以这其实是双向 BFS。

发现点数最大只有 \(14\),每点牌数最多只有 \(4\),考虑用 \(5\) 进制存下牌集,每一位表示点数为这一位的牌的数量。

发现更新状态比较复杂,考虑大力 DFS。

对于每一个还未打出的点数,我们需要分情况讨论:


考虑不构成顺子情况。

若有 \(\ge 4\) 张牌,则考虑:直接打炸弹、四带两个一、四带两个对、拆成三、拆成两个对。

若有 \(\ge 3\) 张牌,则考虑:直接打三、三带一、三带二。

若有 \(\ge 2\) 张牌,则考虑:直接打对、四带两个对、三带二、四带一对(四带两个单)。

若有 \(\ge 1\) 张牌,则考虑:直接打单、四带两个单,三带一。


考虑可能构成顺子情况。

若有 \(\ge 4\) 张牌,则考虑:无。

若有 \(\ge 3\) 张牌,则考虑:若能算上这对牌打出三顺,则更新打出去时的最小次数。

若有 \(\ge 2\) 张牌,则考虑:若能算上这对牌打出对顺,则更新打出去时的最小次数。

若有 \(\ge 1\) 张牌,则考虑:若能算上这对牌打出单顺,则更新打出去时的最小次数。


上述情况中,若能打出符合条件的牌,就更新打出去时的最小次数,将新状态推进队尾。并向下 DFS 继续尝试打出后面牌后能否符合上述情况。

若在更新答案时发现未打出的牌集有最小次数,那么更新最终答案,和记录现在打出的牌的最小次数。我们必须要把所有的次数与最小次数相等的牌集都考虑进去,否则答案可能不是最优。

由于每次更新答案时次数只会加一,而答案又有对称性,所以这份代码加一点剪枝即可通过本题。

代码实现时比较复杂,需要考虑很多细节。

Code:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
 
inline int read()
{
    int x = 0, c = getchar(), f = 0;
    for (; c > '9' || c < '0'; f = c == '-', c = getchar())
        ;
    for (; c >= '0' && c <= '9'; c = getchar())
        x = (x << 1) + (x << 3) + (c ^ 48);
    return f ? -x : x;
}
 
unordered_map<ll, int> mp;
// mp[s] :  所有出过的牌的状态为 s 时,达成这个状态的最小次数。
int a[30]; // 排集
int n;
 
const ll ksm[15] = {1, 5, 25, 125, 625, 3125, 15625, 78125, 390625, 1953125, 9765625, 48828125, 244140625, 1220703125, 6103515625};
// 5 进制状压
// 每一位存
 
int t[16] = {};
// 存每个点数出现了多少张牌
int st[30] = {}, tail = 0;
// 存手里还剩的牌的点数
 
int ans = 0, ans_cnt = 0;
// ans: 最小次数 ; ans_cnt:最小次数由哪个 cnt 被更新过来
 
struct Node
{
    int cnt;  // 现在打了多少次牌
    ll use;   // use:所有出过的牌的状态
    ll unuse; // unuse:所有没出的牌的状态
};
queue<Node> q; // BFS
 
// 更新答案
inline void get(const int &cnt, ll use, ll unuse)
{
    if (mp.count(use))
        return;                                       // 广搜一定是更优的状态先被搜到,所以之前搜到过则直接返回
    mp[use] = mp[use] == 0 ? cnt : min(cnt, mp[use]); // 更新打出牌状态为 use 的最小次数
    if (mp.count(unuse))                              // 如果
        ans = (ans == 0 ? cnt + mp[unuse] : min(ans, cnt + mp[unuse])), ans_cnt = cnt;
    else
        q.push({cnt, use, unuse});
}
 
/*
    一共打了 cnt 次,
    现在要选择打或不打 st[pos] 这张牌,
    到现在出的牌的状态为 use,没出过的牌的状态为 unuse,
    一共打了
     cnt1 个单,
     cnt2 个对
     cnt3 个三
     cnt4 个四
    ,
    上一个打出的牌是 last,若没有则为 -114514,
    pd 记录可否构成顺子
*/
 
void dfs(const int &cnt, int pos, ll use, ll unuse, int cnt1, int cnt2, int cnt3, int cnt4, int last, bool pd)
{
    if (ans && cnt != ans_cnt)
        return; // 非最优,返回
    if (pos > tail)
        return; // 没牌返回
 
    int num = st[pos];
    int c = t[num];
    if (num == 0)
    {
        get(cnt, use + 1, unuse - 1); // 0
        if (c == 2)                   // 0+0
            get(cnt, use + 2, unuse - 2);
 
        dfs(cnt, pos + 1, use + 1, unuse - 1, 1, cnt2, cnt3, cnt4, 0, 0); // 0 + ?
        dfs(cnt, pos + 1, use, unuse, cnt1, cnt2, cnt3, cnt4, last, pd);  // ?
        return;
    }
 
    if (c >= 4)
    {
        if (cnt1 == 2 && !cnt2 && !cnt3 && !cnt4) // 4+1+1
            get(cnt, use + ksm[num] * 4, unuse - ksm[num] * 4);
        if (!cnt1 && cnt2 == 2 && !cnt3 && !cnt4) // 4+2+2
            get(cnt, use + ksm[num] * 4, unuse - ksm[num] * 4);
        if (!cnt1 && !cnt2 && !cnt3 && !cnt4) // 4+0
            get(cnt, use + ksm[num] * 4, unuse - ksm[num] * 4);
 
        if (cnt1 == 1 && !cnt2 && !cnt3 && !cnt4) // 4+1+?
            dfs(cnt, pos + 1, use + ksm[num] * 4, unuse - ksm[num] * 4, cnt1, cnt2, cnt3, cnt4 + 1, num, 0);
        if (!cnt1 && cnt2 == 1 && !cnt3 && !cnt4) // 4+2+?
            dfs(cnt, pos + 1, use + ksm[num] * 4, unuse - ksm[num] * 4, cnt1, cnt2, cnt3, cnt4 + 1, num, 0);
        if (!cnt1 && !cnt2 && !cnt3 && !cnt4)
        {
            dfs(cnt, pos + 1, use + ksm[num] * 4, unuse - ksm[num] * 4, cnt1, cnt2, cnt3, cnt4 + 1, num, 0); // 4+?
            dfs(cnt, pos + 1, use + ksm[num] * 4, unuse - ksm[num] * 4, cnt1, cnt2 + 2, cnt3, cnt4, num, 0); // 2+2+?
        }
    }
 
    if (c >= 3)
    {
        if (cnt1 == 1 && !cnt2 && !cnt3 && !cnt4) // 3+1
            get(cnt, use + ksm[num] * 3, unuse - ksm[num] * 3);
        if (!cnt1 && cnt2 == 1 && !cnt3 && !cnt4) // 3+2
            get(cnt, use + ksm[num] * 3, unuse - ksm[num] * 3);
        if (!cnt1 && !cnt2 && !cnt3 && !cnt4) // 3
            get(cnt, use + ksm[num] * 3, unuse - ksm[num] * 3);
 
        if (!cnt1 && !cnt2 && !cnt3 && !cnt4 && (!pd || (pd && (last < 0 || last == 2 || num != last + 1)))) // 3+? 且不可能构成顺子(剪枝)
            dfs(cnt, pos + 1, use + ksm[num] * 3, unuse - ksm[num] * 3, cnt1, cnt2, cnt3 + 1, cnt4, num, 0);
    }
    if (c >= 2)
    {
        if (!cnt1 && cnt2 == 1 && !cnt3 && cnt4 == 1) // 2+4+2
            get(cnt, use + ksm[num] * 2, unuse - ksm[num] * 2);
        if (!cnt1 && !cnt2 && cnt3 == 1 && !cnt4) // 2+3
            get(cnt, use + ksm[num] * 2, unuse - ksm[num] * 2);
        if (!cnt1 && !cnt2 && !cnt3 && cnt4 == 1) // 1+1+4
            get(cnt, use + ksm[num] * 2, unuse - ksm[num] * 2);
        if (!cnt1 && !cnt2 && !cnt3 && !cnt4) // 2
            get(cnt, use + ksm[num] * 2, unuse - ksm[num] * 2);
 
        if (!cnt1 && !cnt2 && !cnt3 && (!pd || (pd && (last < 0 || last == 2 || num != last + 1)))) // 2+? // 2+4+? 且不可能构成顺子(剪枝)
            dfs(cnt, pos + 1, use + ksm[num] * 2, unuse - ksm[num] * 2, cnt1, cnt2 + 1, cnt3, cnt4, num, 0);
        if (!cnt1 && cnt2 == 1 && !cnt3 && !cnt4 && (!pd || (pd && (last < 0 || last == 2 || num != last + 1)))) // 2+2+?(4) 且不可能构成顺子(剪枝)
            dfs(cnt, pos + 1, use + ksm[num] * 2, unuse - ksm[num] * 2, cnt1, cnt2 + 1, cnt3, cnt4, num, 0);
    }
    if (c >= 1)
    {
        if (!cnt1 && !cnt2 && cnt3 == 1 && !cnt4)
            get(cnt, use + ksm[num] * 1, unuse - ksm[num] * 1); // 1+3
        if (cnt1 == 1 && !cnt2 && !cnt3 && cnt4 == 1)
            get(cnt, use + ksm[num] * 1, unuse - ksm[num] * 1); // 1+1+4
        if (!cnt1 && !cnt2 && !cnt3 && !cnt4)
            get(cnt, use + ksm[num] * 1, unuse - ksm[num] * 1); // 1+0
 
        if (!cnt1 && !cnt2 && !cnt3 && cnt4 <= 1 && (!pd || (pd && (last < 0 || last == 2 || num != last + 1)))) // 1+? // 1+4+? 且不可能构成顺子(剪枝)
            dfs(cnt, pos + 1, use + ksm[num] * 1, unuse - ksm[num] * 1, cnt1 + 1, cnt2, cnt3, cnt4, num, 0);
        if (cnt1 == 1 && !cnt2 && !cnt3 && !cnt4 && (!pd || (pd && (last < 0 || last == 2 || num != last + 1)))) // 1+1+?(4) 且不可能构成顺子(剪枝)
            dfs(cnt, pos + 1, use + ksm[num] * 1, unuse - ksm[num] * 1, cnt1 + 1, cnt2, cnt3, cnt4, num, 0);
    }
 
    if (num > 2 && pd) // 可能构成顺子
    {
        if ((last + 1 == num && last != 2) || last < 0) // 可能构成顺子
        {
            if (c >= 3 && !cnt1 && !cnt2 && !cnt4)
            {
                if (cnt3 >= 1) // 3+3+...
                    get(cnt, use + ksm[num] * 3, unuse - ksm[num] * 3);
 
                dfs(cnt, pos + 1, use + ksm[num] * 3, unuse - ksm[num] * 3, cnt1, cnt2, cnt3 + 1, cnt4, num, pd && (last + 1 == num || last < 0));
                // 尝试构成 3 顺
            }
            if (c >= 2 && !cnt1 && !cnt3 && !cnt4)
            {
                if (cnt2 >= 2) // 2+2+2+...
                    get(cnt, use + ksm[num] * 2, unuse - ksm[num] * 2);
 
                dfs(cnt, pos + 1, use + ksm[num] * 2, unuse - ksm[num] * 2, cnt1, cnt2 + 1, cnt3, cnt4, num, pd && (last + 1 == num || last < 0));
                // 尝试构成对顺
            }
            if (c >= 1 && !cnt2 && !cnt3 && !cnt4)
            {
                if (cnt1 >= 4) // 1+1+1+1+1+...
                    get(cnt, use + ksm[num] * 1, unuse - ksm[num] * 1);
 
                dfs(cnt, pos + 1, use + ksm[num] * 1, unuse - ksm[num] * 1, cnt1 + 1, cnt2, cnt3, cnt4, num, pd && (last + 1 == num || last < 0));
                // 尝试构成单顺
            }
        }
    }
 
    dfs(cnt, pos + 1, use, unuse, cnt1, cnt2, cnt3, cnt4, last, pd);
    // 不出这个点数的牌
}
 
void solve()
{
	// 注意清空 !!!!!
	ans = 0;
	ans_cnt = -1;
	while (q.size()) q.pop();
	mp.clear();
 
	for (int i = 1; i <= n; i++)
	{
		a[i] = read();
		if (a[i] == 1) a[i] += 13;
		read();
	}
	sort(a + 1, a + 1 + n);
 
	ll ansssss = 0;
	for (int i = 1; i <= n; i++) ansssss += ksm[a[i]];
 
	q.push({0, 0, ansssss});
	mp[0] = 0;
 
	if (n)
	{
		while (q.size())
		{
			int cnt = q.front().cnt + 1;
			ll use = q.front().use, unuse = q.front().unuse, x = unuse;
			if (ans && cnt != ans_cnt) break; // 搜到过且不可能有最优状态,则退出循环
            if (mp.count(unuse)) // 剪枝
    			ans = (ans == 0 ? cnt - 1 + mp[unuse] : min(ans, cnt - 1 + mp[unuse])), ans_cnt = cnt - 1;
 
			q.pop();
			tail = 0;
			memset(t, 0, sizeof(t));
			
			// 记录每个点数还需要出多少张牌
			for (int i = 0; i < 15; i++)
			{
				t[i] += x % 5;
				x /= 5;
				if (t[i]) st[++tail] = i;
			}
 
			dfs(cnt, 1, use, unuse, 0, 0, 0, 0, -114514, 1);
		}
	}
	cout << ans << "\n";
}
 
signed main()
{
	int T = read();
	n = read();
	while (T--) solve();
 
	return 0;
}
posted @ 2025-08-18 12:12  Wy_x  阅读(16)  评论(1)    收藏  举报