240125 杂题选谈

PKUSC2022 Mahjong

http://222.180.160.110:1024/contest/4813/problem/1


https://www.bilibili.com/video/BV1JB4y1R7AP/

这里是 PKUSC 当时的讲解视频。听说可以证明本题一定有 \(\le 5\) 的解。好神奇。


就比如说我们爆搜,\(9^4\times 13^4\) 这个显然干不动对吧,所以我们考虑反过来 DP。

我们先把字符串转化成计数数组,就是每种牌有多少片。不妨将 1m ~ 9s 编号为 \(1\sim 27\),记 \(g_i\) 为编号为 \(i\) 的牌的数量。

为什么我们觉得 DP 不好打呢?因为换牌操作可以将两个毫不相干的牌的数量更改,不好记录状态。我们不妨直接将换牌拆成 丢弃一张牌借取令一张牌 两个操作。很显然这两个操作数量是一样的,因为我们的牌数量自始至终不变。

对于比较简单的对子作为终局的情况,我们只关心对数是否为 \(7\),所以设计状态:\(dp_{i,j}\) 表示前 \(i\) 张牌中凑出 \(j\) 个对子的最小代价。

那么就分 把当前牌丢一些 / 借一些拿去组对子直接丢弃当前牌 两种方案。因为丢 / 借的数量是不确定的,直接 abs 一下就好。刷表,有:

\[dp_{i+1,j}=dp_{i,j}+g_{i+1}\\ dp_{i+1,j+1}=dp_{i,j}+|g_{i+1}-2| \]

最后我们的答案就来自 \(dp_{27,7}\)。然后注意这里我们的终局是 14 张牌 你能秒我,但起手只有 13 张牌,所以其实会有一个额外的借牌操作,假设答案为 \(x\),那么其实 \(dp_{27,7}=2\times x+1\)


有了对子的铺垫,面子手其实也还好。我们需要考虑的是对子和面子的个数。

但是有个问题,对子只用借 / 丢当前花色,但面子可能是会借 / 丢下一个 / 下下一个花色的。

所以干脆全部记录到状态里,令 \(f_{i,j,k,a,b}\) 表示当前在第 \(i\) 个花色,凑成了 \(j\) 个面子,\(k\) 个对子,需要 \(a\)\(i+1\)\(b\)\(i+2\)。注意因为表示丢借有负数不太容易,不如就直接设成需要的数量了。

因为这个需要数量只是前面的花色对当前花色的需要 \(a\),我们还要满足当前花色 自身 的需要 \(now\)(也就是说当前花色一共需要 \(a+now\) 张)。注意这里 \(a\) 张全部都是拿去借给前面的花色用的,自己不能用。

  • 对于 \(k=0\)\(now\ge 2\),此时可以从 \(now\) 里拿两张出来凑对子,剩下的 \(now - 2\) 因为肯定 \(\le 2\),所以只能全部拿去凑顺子。所以有:

    \[f_{i+1,j+now-2,1,b+now-2, now-2}=f_{i,j,0,a,b}+|g_{i+1}-(a+now)| \]

  • 对于 \(now\ge 3\),拿三张凑一面。有:

    \[f_{i+1,j+now-2,k,b+now-3,now-3}=f_{i,j,0,a,b}+|g_{i+1}-(a+now)| \]

  • 对于 \(now\ne 0\),可以凑顺子,有:

    \[f_{i+1,j+now,k,b+now,now}=f_{i,j,0,a,b}+|g_{i+1}-(a+now)| \]

    注意不能跨花色借牌,也就是不能让 \(i=8/9/17/18/26/27\)

答案就是 \(f_{27,4,1,0,0}\)


然后这两个情况取一个 \(\min\) 就是答案。

namespace XSC062 {
using namespace fastIO;
using std::cin;
using std::getline;
using str = std::string;
int g[30];
int dp[30][15];
str sm, sp, ss;
int f[30][7][2][7][7];
int abs(int x) { return x >= 0 ? x : -x; }
int min(int x, int y) { return x < y ? x : y; }
void upd(int &x, int y) { x = min(x, y); return; }
int main() {
	getline(cin, sm, 'm');
	getline(cin, sp, 'p');
	getline(cin, ss, 's');
	for (auto i : sm) ++g[i - '0'];
	for (auto i : sp) ++g[i - '0' + 9];
	for (auto i : ss) ++g[i - '0' + 18];
	// 打对子
	memset(dp, 0x3f, sizeof (dp));
	dp[0][0] = 0;
	for (int i = 0; i < 27; ++i) {
		for (int j = 0; j <= 7; ++j) {
			if (dp[i][j] == 0x3f3f3f3f) continue;
			upd(dp[i + 1][j], dp[i][j] + g[i + 1]);
			upd(dp[i + 1][j + 1], dp[i][j] + abs(g[i + 1] - 2));
		}
	}
	// 打飞机
	memset(f, 0x3f, sizeof (f));
	f[0][0][0][0][0] = 0;
	for (int i = 0; i < 27; ++i)
	for (int j = 0; j <= 4; ++j)
	for (int k = 0; k <= 1; ++k)
	for (int a = 0; a <= 4; ++a)
	for (int b = 0; b <= 4; ++b) {
		if (i % 9 == 8 && b) continue;
		if (i % 9 == 0 && a + b) continue;
		for (int now = 0; now <= 4; ++now) { // 对当前的额外需求 
			if (a + now > 4) continue;
			int v = f[i][j][k][a][b] + abs(g[i + 1] - (a + now));
			if (j + now <= 4 && b + now <= 4) // 直接硬配顺子 
				upd(f[i + 1][j + now][k][b + now][now], v);
			if (now >= 2 && !k && j + now - 2 <= 4 && b + now - 2 <= 4) // 借两个去凑对子 
				upd(f[i + 1][j + now - 2][1][b + now - 2][now - 2], v);
			if (now >= 3 && j + now - 2 <= 4 && b + now - 3 <= 4) // 借两个去凑三不带 
				upd(f[i + 1][j + now - 2][k][b + now - 3][now - 3], v);
		}
	}
	// 拿来借走会算两次 
	print(min(dp[27][7], f[27][4][1][0][0]) / 2, '\n');
	return 0;
}
} // namespace XSC062

PKUSC2023 D1T1

http://222.180.160.110:1024/contest/4820/problem/2

我们发现呢如果我们直接按题目说的 \(i=1\sim n\) 跑一遍肯定是要起飞的。所以我们换一个方向,对于每一个答案它适配于哪一个 \(i\)。其实这种技巧在输出这种一串答案的时候挺常用的好像。

posted @ 2024-01-25 20:03  XSC062  阅读(27)  评论(0编辑  收藏  举报