玩个斗地主也能玩出算法?

后台回复进群一起刷力扣

点击卡片可搜索关键词????

读完本文,可以去力扣解决如下题目:

659. 分割数组为连续子序列(Medium

斗地主中,大小连续的牌可以作为顺子,有时候我们把对子拆掉,结合单牌,可以组合出更多的顺子,可能更容易赢。

那么如何合理拆分手上的牌,合理地拆出顺子呢?我们今天看一道非常有意思的算法题,连续子序列的划分问题。

这是力扣第 659 题「分割数组为连续子序列」,题目很简单:

给你输入一个升序排列的数组nums(可能包含重复数字),请你判断nums是否能够被分割成若干个长度至少为 3 的子序列,每个子序列都由连续的整数组成。

函数签名如下:

bool isPossible(vector<int>& nums);

比如题目举的例子,输入nums = [1,2,3,3,4,4,5,5],算法返回 true。

因为nums可以被分割成[1,2,3,4,5][3,4,5]两个包含连续整数子序列。

但如果输入nums = [1,2,3,4,4,5],算法返回 false,因为无法分割成两个长度至少为 3 的连续子序列。

对于这种涉及连续整数的问题,应该条件反射地想到排序,不过题目说了,输入的nums本就是排好序的。

那么,我们如何判断nums是否能够被划分成若干符合条件的子序列呢?

类似前文 回溯算法进行集合划分,我们想把nums的元素划分到若干个子序列中,其实就是下面这个代码逻辑:

for (int v : nums) {
    if (...) {
        // 将 v 分配到某个子序列中
    } else {
        // 实在无法分配 v
        return false;
    }
    return true;
}

关键在于,我们怎么知道当前元素v如何进行分配呢?

肯定得分情况讨论,把情况讨论清楚了,题目也就做出来了。

总共有两种情况:

1、当前元素v自成一派,「以自己开头」构成一个长度至少为 3 的序列

比如输入nums = [1,2,3,6,7,8],遍历到元素6时,它只能自己开头形成一个符合条件的子序列[6,7,8]

2、当前元素v接到已经存在的子序列后面

比如输入nums = [1,2,3,4,5],遍历到元素4时,它只能接到已经存在的子序列[1,2,3]后面。它没办法自成开头形成新的子序列,因为少了个6

但是,如果这两种情况都可以,应该如何选择?

比如说,输入nums = [1,2,3,4,5,5,6,7],对于元素4,你说它应该形成一个新的子序列[4,5,6]还是接到子序列[1,2,3]后面呢?

显然,nums数组的正确划分方法是分成[1,2,3,4,5][5,6,7],所以元素4应该优先判断自己是否能够接到其他序列后面,如果不行,再判断是否可以作为新的子序列开头。

这就是整体的思路,想让算法代码实现这两个选择,需要两个哈希表来做辅助:

freq哈希表帮助一个元素判断自己是否能够作为开头,need哈希表帮助一个元素判断自己是否可以被接到其他序列后面。

freq记录每个元素出现的次数,比如freq[3] == 2说明元素3nums中出现了 2 次。

那么如果我发现freq[3], freq[4], freq[5]都是大于 0 的,那就说明元素3可以作为开头组成一个长度为 3 的子序列。

need记录哪些元素可以被接到其他子序列后面

比如说现在已经组成了两个子序列[1,2,3,4][2,3,4],那么need[5]的值就应该是 2,说明对元素5的需求为 2。

明白了这两个哈希表的作用,我们就可以看懂解法了:

bool isPossible(vector<int>& nums) {

    unordered_map<int, int> freq, need;

    // 统计 nums 中元素的频率
    for (int v : nums) freq[v]++;

    for (int v : nums) {
        if (freq[v] == 0) {
            // 已经被用到其他子序列中
            continue;
        }
        // 先判断 v 是否能接到其他子序列后面
        if (need.count(v) && need[v] > 0) {
            // v 可以接到之前的某个序列后面
            freq[v]--;
            // 对 v 的需求减一
            need[v]--;
            // 对 v + 1 的需求加一
            need[v + 1]++; 
        } else if (freq[v] > 0 && freq[v + 1] > 0 && freq[v + 2] > 0) {
            // 将 v 作为开头,新建一个长度为 3 的子序列 [v,v+1,v+2]
            freq[v]--;
            freq[v + 1]--;
            freq[v + 2]--;
            // 对 v + 3 的需求加一
            need[v + 3]++;
        } else {
            // 两种情况都不符合,则无法分配
            return false;
        }
    }

    return true;
}

至此,这道题就解决了。

那你可能会说,斗地主里面顺子至少要 5 张连续的牌,我们这道题只计算长度最小为 3 的子序列,怎么办?

很简单,把我们的 else if 分支修改一下,连续判断v之后的连续 5 个元素就行了。

那么,我们再难为难为自己,如果我想要的不只是一个布尔值,我想要你给我把子序列都打印出来,怎么办?

其实这也很好实现,只要修改need,不仅记录对某个元素的需求个数,而且记录具体是哪些子序列产生的需求:

// need[6] = 2 说明有两个子序列需要 6
unordered_map<int, int> need;

// need[6] = {
//     {3,4,5},
//     {2,3,4,5},
// }
// 记录哪两个子序列需要 6
unordered_map<int, vector<vector<int>>> need;

这样,我们稍微修改一下之前的代码就行了:

bool isPossible(vector<int>& nums) {
    unordered_map<int, int> freq;
    unordered_map<int, vector<vector<int>>> need;

    for (int v : nums) freq[v]++;

    for (int v : nums) {
        if (freq[v] == 0) {
            continue;
        }

        if (need.count(v) && need[v].size() > 0) {
            // v 可以接到之前的某个序列后面
            freq[v]--;
            // 随便取一个需要 v 的子序列
            vector<int> seq = need[v].back();
            need[v].pop_back();
            // 把 v 接到这个子序列后面
            seq.push_back(v);
            // 这个子序列的需求变成了 v + 1
            need[v + 1].push_back(seq);

        } else if (freq[v] > 0 && freq[v + 1] > 0 && freq[v + 2] > 0) {
            // 可以将 v 作为开头
            freq[v]--;
            freq[v + 1]--;
            freq[v + 2]--;
            // 新建一个长度为 3 的子序列 [v,v + 1,v + 2]
            vector<int> seq{v, v + 1, v + 2};
            // 对 v + 3 的需求加一
            need[v + 3].push_back(seq);

        } else {
            return false;
        }
    }

    // 打印切分出的所有子序列
    for (auto it : need) {
        for (vector<int>& seq : it.second) {
            for (int v : seq) {
                cout << v << " ";
            }
            cout << endl;
        }
    }

    return true;
}

这样,我们记录具体子序列的需求也实现了。

如果本文对你有帮助,点个赞,微信会给你推荐更多相似文章。

精华文章目录点这里 ????

_____________

学好算法靠套路,认准 labuladong,知乎、B站账号同名。公众号后台回复「进群」可加我好友,拉你进算法刷题群:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
斗地主算法策略可以分为两部分,一部分是出牌策略,另一部分是叫牌策略。 出牌策略: 1. 单牌:优先出手中最小的单牌。 2. 对子:优先出手中最小的对子。 3. 三带一:如果手牌中有三张相同的牌,优先出三带一,且出的牌要尽量小。 4. 顺子:优先出最小顺子,如果有多个顺子,则出最小的那个。 5. 连对:优先出最小连对,如果有多个连对,则出最小的那个。 6. 三带二:如果手牌中有三张相同的牌,并且有一对,优先出三带二,且出的牌要尽量小。 7. 炸弹:如果手牌中有炸弹,优先出炸弹。 8. 飞机:优先出最小的飞机,如果有多个飞机,则出最小的那个。 叫牌策略: 1. 地主牌:如果手牌中有地主牌,优先叫地主。 2. 炸弹:如果手牌中有炸弹,优先叫炸弹。 3. 双王:如果手牌中有双王(大王和小王),优先叫双王。 4. 对子:如果手牌中有对子,优先叫对子。 5. 单牌:如果手牌中有单牌,优先叫单牌。 下面是一个简单的 Python 代码示例实现: ```python import random # 手牌 hand_cards = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] # 地主牌 landlord_cards = [33, 34, 35] # 出牌策略 def play(hand_cards, played_cards): if not played_cards: # 第一手出牌,随机出一张牌 return random.choice(hand_cards) else: # 根据出牌策略出牌 if len(played_cards) == 1: # 出单牌 for card in sorted(hand_cards): if card > played_cards[0]: return card elif len(played_cards) == 2 and played_cards[0] == played_cards[1]: # 出对子 for card in sorted(hand_cards): if card > played_cards[0] and hand_cards.count(card) >= 2: return [card, card] elif len(played_cards) == 3 and played_cards[0] == played_cards[1] == played_cards[2]: # 出三带一 for card in sorted(hand_cards): if card > played_cards[0] and hand_cards.count(card) >= 1: return [card, card, card, hand_cards[0]] elif len(played_cards) >= 5 and len(played_cards) % 2 == 0: # 出连对 for i in range(2, 15): combo = [x for x in range(i, i+len(played_cards)//2)] if set(combo).issubset(hand_cards) and combo[-1] > played_cards[-1]: return combo + [hand_cards[0] for i in range(len(combo)//2)] elif len(played_cards) >= 6 and len(played_cards) % 3 == 0: # 出飞机 for i in range(2, 15): combo = [x for x in range(i, i+len(played_cards)//3)] if set(combo).issubset(hand_cards) and combo[-1] > played_cards[-1]: cards = [] for card in combo: cards += [card, card, card] cards += [hand_cards[0] for i in range(len(combo))] return cards elif len(played_cards) == 4: # 出三带二或炸弹 for card in sorted(hand_cards): if card > played_cards[0] and hand_cards.count(card) >= 3: return [card, card, card, hand_cards[0], hand_cards[0]] for card in sorted(hand_cards): if card > played_cards[0] and hand_cards.count(card) == 4: return [card, card, card, card] elif len(played_cards) == 2 and played_cards[0] != played_cards[1]: # 出单牌 for card in sorted(hand_cards): if card > played_cards[0]: return card elif len(played_cards) == 4 and played_cards[0] == played_cards[1] == played_cards[2] == played_cards[3]: # 出炸弹 for card in sorted(hand_cards): if card > played_cards[0] and hand_cards.count(card) == 4: return [card, card, card, card] else: # 只有炸弹可以大过 for card in sorted(hand_cards): if hand_cards.count(card) == 4 and (not played_cards or card > played_cards[0]): return [card, card, card, card] # 无法出牌 return [] # 叫牌策略 def call_landlord(hand_cards): # 根据叫牌策略叫牌 if 33 in hand_cards and 34 in hand_cards: # 叫双王 return 1 elif len([card for card in hand_cards if card > 10]) >= 5: # 叫炸弹 return 1 elif len([card for card in hand_cards if hand_cards.count(card) >= 2]) >= 1: # 叫对子 return 1 elif len([card for card in hand_cards if hand_cards.count(card) == 1]) >= 3: # 叫单牌 return 1 else: return 0 # 测试代码 print("手牌:", hand_cards) print("地主牌:", landlord_cards) print("叫牌:", call_landlord(hand_cards)) print("出牌:", play(hand_cards, [])) print("出牌:", play(hand_cards, [3, 3, 3])) print("出牌:", play(hand_cards, [3, 3, 3, 4, 4, 4])) print("出牌:", play(hand_cards, [3, 3, 3, 4, 4, 4, 5, 5])) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值