题解 P2540 【斗地主增强版】

· · 题解

暴力出奇迹,我是搜索小名士

博客中观看体验更佳:My Blog

分析

其实我一开始没有想着去写100分的代码,因为爆搜的复杂度太大,然后这题好像我只会爆搜。

第一步

首先,很容易想到一种最纯粹的爆搜。每层都将当前剩余牌可行的情况全部枚举判断,然后打出一种可行的牌进入下一层继续搜索。

搜索是盲目的,我们考虑剪枝。    ——某金牌教练

一个剪枝是搜索必备,如果当前答案大于或等于已知的最小答案,我们接下来的搜索一定不会更新答案,所以回到上一层。

然后再继续想,可以加一个启发式剪枝,计算打出所有剩余牌的最大出牌次数。

用肉眼就能推出,一副牌的最大出牌次数就是牌的数码种类数,两个王在此可以算一种。然后我们计算出当前最大出牌次数加上当前已出牌次数去更新答案,这样就会剪去接下来的一些较差的出牌方法。

第二步

上面的方法必不能通过本题,不需要尝试也能猜测。所以还要继续剪枝。

再稍微思考一下,我们每层搜索将全部出牌方法都枚举一遍过于暴力。我们发现,有些出牌方法自始至终永远不可行,并且可行的出牌方法一定越来越少。然后就想到了预处理,我们预处理出最开始的手牌的可行出牌方法。

这里我的代码极其鬼畜,可能对代码能力有些要求。我用了一些自认为省空间,用起来舒服的方法。将可行方法的信息写成结构体,就不演示,代码中在讲讲。

最关键的是,我们要计算每种方法的出牌数量,可用来后续我们继续优化。

这些代码全部可在输入后搜索前执行,我是将这些可行方法放到了vector中。在搜索时,枚举vector的所有元素。再进行一次针对性判断,也就是判断需要打的那几张牌就够了。

第三步

我们说记录了每种方法的出牌数量。那我们从大到小排序,这样大概能优化时间,我也不知道。

在这我们提个醒,如果你将王炸、对子、三张牌放入可行方法中,那你一定会TLE,因为我一开始就是这样。(实践出真知,但其实非加强版不需要,到这里已经通过了)

我们每层搜索,只枚举上一层搜索枚举没有枚举到的就行了,这是个小剪枝。如果当前方法的出牌数大于当前剩余数量,就直接枚举下一个出牌方法,这也可算上个小剪枝。然后还要再次判断当前剩余的牌下这种方法是否可行。

还有几个小的要点,我们不管是预处理,还是每层搜索都要用到一个小动态规划,用来求某个牌当左端点,可以连成(单、双、三)顺子的最大长度。鉴于这个题的难度,我相信大家都会这个小动归。

然后还有几个提示,四带二可以带两个相同数码的对子(俗称炸弹),同理,可以带两个相同数码的一张牌(俗称对子)。然后1当14,大小王当15、16比较好写代码。

经过多次提交更改调试,终于通过了本题。

代码

不开O2两秒多通过。


#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;
int T, n;
struct PAI {
    int num, col;
} ps[30];
// 记录牌的数码,颜色(没啥用)
#define SHUNZI 3
#define SANDAI 4
#define SIDAI 5
//顺子、三带、四带
struct CP {
    // 记录出牌方法的信息
    int typ, l, r, k1, k2, siz;
    // typ 为宏定义的值
    // siz 为出牌数量
    // l r 多种使用。l:顺子左端点,其他为最多的那张牌。r:顺子右端点,其他为少的那张牌需要的个数
    // k1 k2 三带、四带使用,表示附带哪一(两)张牌。
    CP(int typ, int siz, int l= 0, int r= 0, int k1= 0, int k2= 0) {
        this->typ= typ, this->siz= siz, this->l= l, this->r= r, this->k1= k1, this->k2= k2;
    }
    inline int operator<(const CP &c2) const {
        return siz > c2.siz;
    }
};
int pai[20], bestans, us3[20], us2[20], us1[20];
vector< CP > v;
void dfs(int nown, int nowa, int ls) {
    if(nowa >= bestans) return;
    // 小剪枝
    bestans= min(bestans, nowa + nown);
    // 更新答案,全部单打(没什么用)
    if(!nown) return;
    int res= 0, us[4][20];
    memset(us, 0, sizeof(us));
    for(int i= 2; i <= 16; i++) res+= (pai[i] != 0);
    if(pai[15] && pai[16]) --res;
    // 15、16为大小王,主程序中有,大小王可以王炸
    bestans= min(bestans, nowa + res);
    // 再次更新答案(剪枝)
    for(int i= 14, cnt= 0; i >= 2; i--) {
        if(pai[i] < 1) continue;
        us[1][i]= us[1][i + 1] + 1;
        if(pai[i] < 2) continue;
        us[2][i]= us[2][i + 1] + 1;
        if(pai[i] < 3) continue;
        us[3][i]= us[3][i + 1] + 1;
    }
    // DP出可行的顺子长度
    for(int i= ls; i < v.size(); i++) {
        if(v[i].siz > nown) continue;
        switch(v[i].typ) {
            case SHUNZI: {
                if(v[i].r - v[i].l + 1 > us[v[i].k1][v[i].l]) break;
                for(int j= v[i].l; j <= v[i].r; j++) pai[j]-= v[i].k1;
                dfs(nown - v[i].siz, nowa + 1, i + 1);
                for(int j= v[i].l; j <= v[i].r; j++) pai[j]+= v[i].k1;
                // 判断顺子
                break;
            }
            case SANDAI: {
                if(pai[v[i].l] < 3 || pai[v[i].k1] < v[i].r) break;
                pai[v[i].l]-= 3, pai[v[i].k1]-= v[i].r;
                dfs(nown - v[i].siz, nowa + 1, i + 1);
                pai[v[i].l]+= 3, pai[v[i].k1]+= v[i].r;
                // 判断三带
                break;
            }
            case SIDAI: {
                if(pai[v[i].l] < 4) break;
                if(pai[v[i].k1] < 1 || pai[v[i].k2] < 1) break;
                if(v[i].r == 2 && (pai[v[i].k1] < 2 || pai[v[i].k2] < 2)) break;
                // 都需要两张
                if(v[i].r == 2 && v[i].k1 == v[i].k2 && pai[v[i].k1] < 4) break;
                // 两对相同数码
                if(v[i].r == 1 && v[i].k1 == v[i].k2 && pai[v[i].k1] < 2) break;
                // 两张相同数码
                pai[v[i].l]-= 4;
                pai[v[i].k1]-= v[i].r;
                pai[v[i].k2]-= v[i].r;
                dfs(nown - v[i].siz, nowa + 1, i + 1);
                pai[v[i].l]+= 4;
                pai[v[i].k1]+= v[i].r;
                pai[v[i].k2]+= v[i].r;
                // 判断四带
            }
            default: break;
        }
    }
    return;
}
int main() {
    scanf("%d%d", &T, &n);
    while(T--) {
        memset(pai, 0, sizeof(pai)), bestans= 0x3f3f3f3f;
        for(int i= 1; i <= n; i++) {
            scanf("%d%d", &ps[i].num, &ps[i].col);
            if(ps[i].num == 1) ps[i].num= 14;
            // 1 当 14
            if(ps[i].num == 0) ps[i].num= ps[i].col + 14;
            // 大小王当15、16
            ++pai[ps[i].num];
        }
        memset(us3, 0, sizeof(us3)), memset(us2, 0, sizeof(us2)), memset(us1, 0, sizeof(us1));
        v.clear();
        for(int i= 14; i >= 3; i--) {
            if(pai[i] < 1) continue;
            us1[i]= us1[i + 1] + 1;
            if(pai[i] < 2) continue;
            us2[i]= us2[i + 1] + 1;
            if(pai[i] < 3) continue;
            us3[i]= us3[i + 1] + 1;
        }
        // DP出顺子长度
        for(int i= 2; i <= 16; i++) {
            for(int j= 5; j <= us1[i]; j++) v.push_back(CP(SHUNZI, j, i, i + j - 1, 1));
            for(int j= 3; j <= us2[i]; j++) v.push_back(CP(SHUNZI, j * 2, i, i + j - 1, 2));
            for(int j= 2; j <= us3[i]; j++) v.push_back(CP(SHUNZI, j * 3, i, i + j - 1, 3));
            // i 当左端点的顺子
            if(pai[i] < 3) continue;
            for(int j= 2; j <= 16; j++) {
                if(i == j || !pai[j]) continue;
                v.push_back(CP(SANDAI, 4, i, 1, j));
                if(pai[j] < 2) continue;
                v.push_back(CP(SANDAI, 5, i, 2, j));
            }
            // 三带
            if(pai[i] < 4) continue;
            for(int j= 2; j <= 16; j++) {
                if(i == j || !pai[j]) continue;
                for(int k= j + 1; k <= 16; k++) {
                    if(i == k || !pai[k]) continue;
                    v.push_back(CP(SIDAI, 6, i, 1, j, k));
                    if(pai[j] < 2 || pai[k] < 2) continue;
                    v.push_back(CP(SIDAI, 8, i, 2, j, k));
                }
                if(pai[j] == 4) v.push_back(CP(SIDAI, 8, i, 2, j, j));
                if(pai[j] > 2) v.push_back(CP(SIDAI, 6, i, 1, j, j));
            }
            // 四带
        }
        sort(v.begin(), v.end());
        // 排序
        dfs(n, 0, 0);
        cout << bestans << endl;
    }
    return 0;
}
···