专栏文章
题解:P2540 [NOIP2015 提高组] 斗地主 加强版
P2540题解参与者 6已保存评论 11
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 11 条
- 当前快照
- 1 份
- 快照标识符
- @miqeapwy
- 此快照首次捕获于
- 2025/12/04 03:23 3 个月前
- 此快照最后确认于
- 2025/12/04 03:23 3 个月前
题目分析
这道题按照我们老师的话来说就是很神奇,写完才知道这道题有多么神奇。看到这道题依然只能想到搜索每一步出牌。
我们看题可以知道,牌的花色是不影响出牌的,所以输入时,我们不存每张牌长啥样,只存某种点数牌的数量,记作 。
当然,输入需要特别把 记在 的位置,这样才能更好的枚举顺子。以下分析均不带火箭,因为统计答案时直接使用更方便。
发现,有一部分出牌方法中,牌的点数和能否出是有关的,而另一部分中,牌的点数是无关的。我们依此大致可以把出牌分为两类。
- 顺子类:单顺子/双顺子/三顺子
- 其它类:炸弹/单张牌/对子牌/三张牌/三带一/三带二/四带二(需要拆解成四带二张和四带二对处理)
当然,不分类这道题依然可做,暴力卡时也能过。不过我更倾向于分类后使用这个“神奇”的剪枝来做。
牌的点数有关,我们没有什么好办法处理。但牌的点数无关就可以使用预处理的动态规划(四维:
dp[单张数量][对子数量][三张数量][炸弹数量])来减去搜索的时间复杂度。我们先来处理顺子类。顺子类 - 搜索处理
我们使用暴力做法,暴力枚举顺子中每个点数的个数 ,起点位置 与可能的终点位置 。对于每一个起点位置,我们开一个计数器 记成功选中的牌的点数个数。
对于每一个可能的终点位置 ,如果能够选择 张牌,则选择 张牌,并
cnt++;,如果不能,则直接跳出。这里用数学直觉找出一个临界值用于判定,当顺子
cnt 足够时, 和 满足 ,对于本题 的取值都是满足的。由此可写出顺子部分的代码。注意删除和回溯。这份片段中, 代表走到这一步需要的步数。
CPPfor (int a = 1; a <= 3; a++){
for (int i = 3; i <= 14; i++){
int cnt = 0;
for (int j = i; j <= 14; j++){
if (s[j] >= a) cnt++;
else break;
if (a * cnt >= 5){
for (int k = i; k <= j; k++){
s[k] -= a;
}
dfs(x+1);
for (int k = i; k <= j; k++){
s[k] += a;
}
}
}
}
}
其它类 - 动态规划
我们记 为单张数, 为对子数, 为三张数, 为炸弹数。记
dp[a][b][c][d] 表示这些牌需要用几次出完。具体如何转移见下表。| 出牌种类 | 附加值 | ||||
|---|---|---|---|---|---|
| 单张牌 | |||||
| 对子牌 | |||||
| 三张牌 | |||||
| 炸弹 | |||||
| 三带一 | |||||
| 三带二 | |||||
| 四带两张 | |||||
| 四带两对 | |||||
| 拆一对* | |||||
| 拆三张* | |||||
| 拆炸弹成两对* | |||||
| 拆炸弹成单+三张* |
注释:四带两张和四带两对都是四带二。所有带星号的都是拆牌操作,它们可能使得答案更优,且不需要消耗步数。
引入拆牌操作后,我们就需要仔细思考动态规划的顺序了。显然, 的顺序是更合理的,保证了每个值被推算前,它需要用到的值都已经推算完成。
预处理时进行动态规划即可。为了减少代码量,可以使用临时变量。注意边界问题,初始值的设置与 值不能够被覆盖。
CPPfor (int d = 0; d <= 5; d++){
for (int c = 0; c <= 8; c++){
for (int b = 0; b <= 12; b++){
for (int a = 0; a <= 23; a++){
if (!a && !b && !c && !d) continue;
int s = 0x3f3f3f3f;
if (a) s = min(s, dp[a-1][b][c][d] + 1);
if (b) s = min(s, dp[a][b-1][c][d] + 1);
if (c) s = min(s, dp[a][b][c-1][d] + 1);
if (d) s = min(s, dp[a][b][c][d-1] + 1);
if (a && c) s = min(s, dp[a-1][b][c-1][d] + 1);
if (b && c) s = min(s, dp[a][b-1][c-1][d] + 1);
if (a >= 2 && d) s = min(s, dp[a-2][b][c][d-1] + 1);
if (b >= 2 && d) s = min(s, dp[a][b-2][c][d-1] + 1);
if (b) s = min(s, dp[a+2][b-1][c][d]);
if (c) s = min(s, dp[a+1][b+1][c-1][d]);
if (d) s = min(s, dp[a][b+2][c][d-1]);
if (d) s = min(s, dp[a+1][b][c+1][d-1]);
dp[a][b][c][d] = s;
}
}
}
}
剪枝的具体实现
由于牌一共就 种(不加大小王),我们直接暴力统计即可。调用时注意,双王可作单张,也可作火箭(即多加一步)。
CPPmemset(c, 0, sizeof c);
for (int i = 2; i <= 14; i++) c[s[i]]++;
ans = min(ans, x + dp[c[1]+s[0]][c[2]][c[3]][c[4]]);
if (s[0] == 2) ans = min(ans, x + dp[c[1]][c[2]][c[3]][c[4]] + 1);
当然,不要忘记我们的最简单的剪枝“最优性剪枝”。
CPPif (x >= ans) return ;
最终将上面的代码块串在一起,加上输入输出就是本题的代码。
题目总结与参考代码
本题的“神奇之处”就在于使用动态规划进行剪枝。最后提醒一句,本题多测,不要忘记清空和重置答案。
CPP#include <bits/stdc++.h>
using namespace std;
int T, n, s[25], c[5], dp[26][15][10][6], ans;
void dfs(int x){
if (x >= ans) return ;
memset(c, 0, sizeof c);
for (int i = 2; i <= 14; i++) c[s[i]]++;
ans = min(ans, x + dp[c[1]+s[0]][c[2]][c[3]][c[4]]);
if (s[0] == 2) ans = min(ans, x + dp[c[1]][c[2]][c[3]][c[4]] + 1);
for (int a = 1; a <= 3; a++){
for (int i = 3; i <= 14; i++){
int cnt = 0;
for (int j = i; j <= 14; j++){
if (s[j] >= a) cnt++;
else break;
if (a * cnt >= 5){
for (int k = i; k <= j; k++){
s[k] -= a;
}
dfs(x+1);
for (int k = i; k <= j; k++){
s[k] += a;
}
}
}
}
}
return ;
}
int main(){
for (int d = 0; d <= 5; d++){
for (int c = 0; c <= 8; c++){
for (int b = 0; b <= 12; b++){
for (int a = 0; a <= 23; a++){
if (!a && !b && !c && !d) continue;
int s = 0x3f3f3f3f;
if (a) s = min(s, dp[a-1][b][c][d] + 1);
if (b) s = min(s, dp[a][b-1][c][d] + 1);
if (c) s = min(s, dp[a][b][c-1][d] + 1);
if (d) s = min(s, dp[a][b][c][d-1] + 1);
if (a && c) s = min(s, dp[a-1][b][c-1][d] + 1);
if (b && c) s = min(s, dp[a][b-1][c-1][d] + 1);
if (a >= 2 && d) s = min(s, dp[a-2][b][c][d-1] + 1);
if (b >= 2 && d) s = min(s, dp[a][b-2][c][d-1] + 1);
if (b) s = min(s, dp[a+2][b-1][c][d]);
if (c) s = min(s, dp[a+1][b+1][c-1][d]);
if (d) s = min(s, dp[a][b+2][c][d-1]);
if (d) s = min(s, dp[a+1][b][c+1][d-1]);
dp[a][b][c][d] = s;
}
}
}
}
cin >> T >> n;
while (T--){
memset(s, 0, sizeof s);
for (int i = 1; i <= n; i++){
int x, y; cin >> x >> y;
if (x == 1) x = 14;
s[x]++;
}
ans = 0x7f7f7f7f;
dfs(0);
cout << ans << endl;
}
return 0;
}
相关推荐
评论
共 11 条评论,欢迎与作者交流。
正在加载评论...