#P3196. 最佳的出牌方法(200分)
-
1000ms
Tried: 115
Accepted: 30
Difficulty: 5
所属公司 :
华为od
-
算法标签>动态规划
最佳的出牌方法(200分)
题解
题面描述
手上有一副扑克牌,每张牌按牌面数字记分(J=11,Q=12,K=13,没有大小王)。出牌时按照以下规则记分:
- 出单张:记牌面分数,例如出一张 2,得分为 2。
- 出对子或三张:记牌面分数总和再乘以 2,例如出 3 张 3,得分为 (3+3+3)×2=18。
- 出顺子(5 张连续的牌):记牌面分数总和再乘以 2,例如出 34567 顺,得分为 (3+4+5+6+7)×2=50。
- 出炸弹(4 张相同的牌):记牌面分数总和再乘以 3,例如出 4 张 4,得分为 4×4×3=48。
求出一副牌的最高得分数。
思路
本题的核心是通过动态规划(DP)或记忆化搜索(DFS + Memoization)的方法,枚举所有可能的出牌组合,并选择得分最高的组合。具体步骤如下:
-
牌面转换:将输入的牌面字符转换为对应的数值,便于后续处理。例如,
J转换为 11,Q转换为 12,K转换为 13,0转换为 10。 -
统计牌面数量:统计每种牌面出现的次数,存储在一个数组中。例如,
counts[1]表示牌面为 1 的牌有几张,依此类推。 -
动态规划:
- 定义一个状态表示为当前各牌面剩余的数量。
- 对于每种状态,尝试所有可能的出牌方式(单张、对子、三张、炸弹、顺子)。
- 对于每种出牌方式,递归计算剩余牌组的最大得分,并更新当前状态的最大得分。
- 使用记忆化缓存(如哈希表)来存储已经计算过的状态,避免重复计算。
-
处理顺子:
- 顺子需要 5 张连续的牌,且不包含
10JQKA(即0JQK1不算顺)。 - 枚举所有可能的起始牌面,检查是否满足顺子的条件。
- 顺子需要 5 张连续的牌,且不包含
cpp
#include <bits/stdc++.h>
using namespace std;
// 将牌面字符转换为对应的数值
int cardValue(char c){
if(c >= '1' && c <= '9') return c - '0';
if(c == '0') return 10;
if(c == 'J') return 11;
if(c == 'Q') return 12;
if(c == 'K') return 13;
return -1; // 不可能的情况
}
// 使用记忆化搜索的方式计算最大得分
unordered_map<string, long long> dp_cache;
// 计算最大得分的函数
long long dfs(vector<int> &counts){
// 将当前状态转换为字符串作为缓存的键
string key = "";
for(auto cnt : counts) key += to_string(cnt) + ",";
if(dp_cache.find(key) != dp_cache.end()) return dp_cache[key];
long long res = 0;
// 尝试所有可能的出牌方式
// 1. 出单张
for(int i=1;i<=13;i++) {
if(counts[i] >=1){
counts[i]--;
res = max(res, (long long)i + dfs(counts));
counts[i]++;
}
}
// 2. 出对子
for(int i=1;i<=13;i++) {
if(counts[i] >=2){
counts[i] -=2;
res = max(res, (long long)(i*2)*2 + dfs(counts));
counts[i] +=2;
}
}
// 3. 出三张
for(int i=1;i<=13;i++) {
if(counts[i] >=3){
counts[i] -=3;
res = max(res, (long long)(i*3)*2 + dfs(counts));
counts[i] +=3;
}
}
// 4. 出炸弹
for(int i=1;i<=13;i++) {
if(counts[i] >=4){
counts[i] -=4;
res = max(res, (long long)(i*4)*3 + dfs(counts));
counts[i] +=4;
}
}
// 5. 出顺子
// 顺子需要5张连续的牌,且不包含10JQKA(即0JQK1)
for(int start=1; start<=9; start++){ // 13-5+1=9
bool valid=true;
for(int j=0; j<5; j++) {
if(counts[start+j] <1){
valid=false;
break;
}
}
// 检查是否包含1和13的情况(0JQK1不算顺)
if(valid){
// 如果包含1和13,则不算
bool invalid = false;
for(int j=0; j<5; j++){
if((start + j) ==1 || (start + j) ==13){
invalid=true;
break;
}
}
if(!invalid){
// 计算顺子的总分
int sum =0;
for(int j=0; j<5; j++) sum += (start + j);
// 出顺子
for(int j=0; j<5; j++) counts[start + j]--;
res = max(res, (long long)(sum)*2 + dfs(counts));
for(int j=0; j<5; j++) counts[start + j]++;
}
}
}
dp_cache[key] = res;
return res;
}
int main(){
string s;
cin >> s;
// 初始化牌面计数
vector<int> counts(14,0); // counts[1..13]
for(char c: s){
int val = cardValue(c);
counts[val]++;
}
cout << dfs(counts) << endl;
}
python
from functools import lru_cache
# 将牌面字符转换为对应的数值
def card_value(c):
if c.isdigit() and c != '0':
return int(c)
if c == '0':
return 10
if c == 'J':
return 11
if c == 'Q':
return 12
if c == 'K':
return 13
return -1 # 不可能的情况
def main():
s = input().strip()
counts = [0]*14 # counts[1..13]
for c in s:
val = card_value(c)
counts[val] +=1
@lru_cache(None)
def dfs(state):
counts = list(state)
res = 0
# 1. 出单张
for i in range(1,14):
if counts[i] >=1:
counts[i] -=1
res = max(res, i + dfs(tuple(counts)))
counts[i] +=1
# 2. 出对子
for i in range(1,14):
if counts[i] >=2:
counts[i] -=2
res = max(res, (i*2)*2 + dfs(tuple(counts)))
counts[i] +=2
# 3. 出三张
for i in range(1,14):
if counts[i] >=3:
counts[i] -=3
res = max(res, (i*3)*2 + dfs(tuple(counts)))
counts[i] +=3
# 4. 出炸弹
for i in range(1,14):
if counts[i] >=4:
counts[i] -=4
res = max(res, (i*4)*3 + dfs(tuple(counts)))
counts[i] +=4
# 5. 出顺子
for start in range(1,10):
valid = True
for j in range(5):
if counts[start+j] <1:
valid = False
break
if valid:
# 检查是否包含1和13
invalid = False
for j in range(5):
if (start+j)==1 or (start+j)==13:
invalid = True
break
if not invalid:
sum_seq = sum(start + j for j in range(5))
for j in range(5):
counts[start+j] -=1
res = max(res, sum_seq*2 + dfs(tuple(counts)))
for j in range(5):
counts[start+j] +=1
return res
print(dfs(tuple(counts)))
if __name__ == "__main__":
main()
java
import java.util.*;
public class Main {
// 将牌面字符转换为对应的数值
public static int cardValue(char c){
if(c >= '1' && c <= '9') return c - '0';
if(c == '0') return 10;
if(c == 'J') return 11;
if(c == 'Q') return 12;
if(c == 'K') return 13;
return -1; // 不可能的情况
}
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
String s = sc.next();
int[] counts = new int[14]; // counts[1..13]
for(char c: s.toCharArray()){
int val = cardValue(c);
counts[val]++;
}
Map<String, Long> memo = new HashMap<>();
System.out.println(dfs(counts, memo));
}
// 递归计算最大得分
public static long dfs(int[] counts, Map<String, Long> memo){
StringBuilder keyBuilder = new StringBuilder();
for(int i=1;i<=13;i++) keyBuilder.append(counts[i]).append(",");
String key = keyBuilder.toString();
if(memo.containsKey(key)) return memo.get(key);
long res = 0;
// 1. 出单张
for(int i=1;i<=13;i++){
if(counts[i] >=1){
counts[i]--;
res = Math.max(res, i + dfs(counts, memo));
counts[i]++;
}
}
// 2. 出对子
for(int i=1;i<=13;i++){
if(counts[i] >=2){
counts[i] -=2;
res = Math.max(res, (long)(i*2)*2 + dfs(counts, memo));
counts[i] +=2;
}
}
// 3. 出三张
for(int i=1;i<=13;i++){
if(counts[i] >=3){
counts[i] -=3;
res = Math.max(res, (long)(i*3)*2 + dfs(counts, memo));
counts[i] +=3;
}
}
// 4. 出炸弹
for(int i=1;i<=13;i++){
if(counts[i] >=4){
counts[i] -=4;
res = Math.max(res, (long)(i*4)*3 + dfs(counts, memo));
counts[i] +=4;
}
}
// 5. 出顺子
for(int start=1; start<=9; start++){
boolean valid = true;
for(int j=0; j<5; j++) {
if(counts[start+j] <1){
valid = false;
break;
}
}
if(valid){
// 检查是否包含1和13
boolean invalid = false;
for(int j=0; j<5; j++){
if((start + j) ==1 || (start + j) ==13){
invalid = true;
break;
}
}
if(!invalid){
// 计算顺子的总分
int sum =0;
for(int j=0; j<5; j++) sum += (start + j);
// 出顺子
for(int j=0; j<5; j++) counts[start + j]--;
res = Math.max(res, (long)(sum)*2 + dfs(counts, memo));
for(int j=0; j<5; j++) counts[start + j]++;
}
}
}
memo.put(key, res);
return res;
}
}
题目内容
手上有一副扑克牌,每张牌按牌面数字记分(J=11,Q=12,K=13,没有大小王),出牌时按照以下规则记分:
出单张,记牌面分数,例如出一张 2 ,得分为 2
出对或 3 张,记牌面分数总和再 ×2,例如出 3 张 3 ,得分为 (3+3+3)×2=18
出 5 张顺,记牌面分数总和再 ×2 ,例如出 34567 顺,得分为(3+4+5+6+7)×2=50
出 4 张炸弹,记牌面分数总和再 ×3 ,例如出 4 张 4 ,得分为 4×4×3=48
求出一副牌最高的得分数
输入描述
按顺序排好的一副牌,最少 1 张,最多 15 张。
1−9 输入为数字 1−9 ,10 输入为数字 0 ,JQK 输入为大写字母 JQK .
无需考虑输入非法的情况,例如输入字符不在 [0−9JQK] 范围或某一张牌超过 4 张
输出描述
最高的得分数
备注
积分规则中没有的出牌方式不支持,例如不支持 3 带 1、4 带 2 ,不支持 5 张以上的顺,且 10JQKA (0JQK1) 不算顺。
样例1
输入
33445677
输出
67
说明
出对 3、对 4、对 7,单张 5、6,得分为 67;
出 34567 顺,再出单张 3、4、7,得分为 64
因此最高得分是按对出,可得到最高分 67 ,输出结果 67