洛谷 U321190 麻将 加强加强版 题解

Description

给定一副 k 张牌的麻将牌,求能「听」哪些牌。

对于所有数据,1k2×105

link:https://www.luogu.com.cn/problem/U321190

Solution

算法零#

枚举「听」的牌,用状压 DP 或者贪心判断。

时间复杂度 O(2npoly(n))O(n3),期望得分 1020

算法一#

将牌放入桶内,计 aii 的牌的个数。枚举「听」的牌,用 DP 判断是否能「听」。

在 DP 前,有一个性质:由于三组同样的「顺子」可以被重新划分为三组「刻子」,所以相同的「顺子」不会超过三组。

fi,j,k,0/1 为用完 1i 的牌,有 j 组以 i2 开头的「顺子」,ki1 开头的「顺子」,有或无「雀头」(1 为有,0 为无)的情况是否可能。转移如下:

  • 不分出「雀头」,则 fi+1,k,(ai+1jk)mod3,0/1fi,j,k,0/1
  • 分出「雀头」,则 fi+1,k,(ai+1jk2)mod3,1fi,j,k,0

时间复杂度 O(n2),有着 18 的大常数。期望得分 50

代码实现:

#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
namespace Milkcat {
    typedef long long LL;
    typedef pair<LL, LL> pii;
    const int N = 5e3 + 5;
    int n, k, x, cnt[N], a[N], f[N][3][3][2];
	vector<int> res;
	bool check(int qwq) {
		for (int i = 1; i <= n; i ++)
			a[i] = cnt[i] + (i == qwq);
		memset(f, 0, sizeof f), f[0][0][0][0] = 1;
		for (int i = 0; i < n; i ++)
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++) 
					for (int p = 0; p < 2; p ++) {
						if (!f[i][j][k][p]) continue;
						if (a[i + 1] >= j + k) f[i + 1][k][(a[i + 1] - j - k) % 3][p] = 1;
						if (!p && a[i + 1] >= j + k + 2) f[i + 1][k][(a[i + 1] - j - k - 2) % 3][1] = 1;
					}
		return f[n][0][0][1];
	}
	int main() {
		cin >> n >> k;
		for (int i = 1; i <= k; i ++)
			cin >> x, cnt[x] ++;
		for (int i = 1; i <= n; i ++)
			if (check(i)) res.push_back(i);
		if (res.empty()) puts("NO");
		for (int v : res) cout << v << ' ';
        return 0;
    }
}
int main() {
    int T = 1;
    while (T --) Milkcat::main();
    return 0;
}

算法二

这种 DP 似乎无法继续优化,我们发现复杂度瓶颈在枚举「听」的牌,考虑将它放入 DP 状态中。

于是,设 fi,j,k,0/1,t 为用完 1i 的牌,有 j 组以 i2 开头的「顺子」,ki1 开头的「顺子」,有或无「雀头」(1 为有,0 为无)的情况下「听」t 是否可能。我们发现 DP 状态可以使用 bitset 优化。

为了辅助转移,我们设 gi,j,k,0/1 为用完 1i 的牌,有 j 组以 i2 开头的「顺子」,ki1 开头的「顺子」,有或无「雀头」(1 为有,0 为无)的情况是否可能(算法一的 DP)。

g 的转移见算法一。

转移枚举 i,然后分三步:

  • 转移 g
  • f 之间转移:条件同 g,设 fx 转移到 fy,需要将 fy 设为两者的并集。
  • gf 转移:加上一张点数为 i 的牌,转移条件仍然同第 g,但是是由 g 转移到 f,设 gi,j,k,l 转移到 f,则令 fi,j,k,l,i1

时间复杂度 O(n2w),由于还是有 18 的常数,所以常数没有特别小。期望得分 80

可能有点抽象,可以看代码理解。

代码实现:

#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
namespace Milkcat {
    typedef long long LL;
    typedef pair<LL, LL> pii;
    const int N = 1e4 + 5;
    int n, k, x, a[N], g[N][3][3][2];
	bitset<N> f[N][3][3][2];
	vector<int> res;
	int main() {
		cin >> n >> k;
		for (int i = 1; i <= k; i ++)
			cin >> x, a[x] ++;
		g[0][0][0][0] = 1;
		for (int i = 0; i < n; i ++) {
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++)
					for (int p = 0; p < 2; p ++) {
						if (!g[i][j][k][p]) continue;
						if (a[i + 1] >= j + k) g[i + 1][k][(a[i + 1] - j - k) % 3][p] = 1;
						if (!p && a[i + 1] >= j + k + 2) g[i + 1][k][(a[i + 1] - j - k - 2) % 3][1] = 1;
					}
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++)
					for (int p = 0; p < 2; p ++) {
						if (!f[i][j][k][p].count()) continue;
						if (a[i + 1] >= j + k) f[i + 1][k][(a[i + 1] - j - k) % 3][p] |= f[i][j][k][p];
						if (!p && a[i + 1] >= j + k + 2) f[i + 1][k][(a[i + 1] - j - k - 2) % 3][1] |= f[i][j][k][p];
					}
			a[i + 1] ++;
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++)
					for (int p = 0; p < 2; p ++) {
						if (!g[i][j][k][p]) continue;
						if (a[i + 1] >= j + k) f[i + 1][k][(a[i + 1] - j - k) % 3][p].set(i + 1);
						if (!p && a[i + 1] >= j + k + 2) f[i + 1][k][(a[i + 1] - j - k - 2) % 3][1].set(i + 1);
					}
		}
		if (!f[n][0][0][1].count()) puts("QAQ");
		for (int i = 1; i <= n; i ++)
			if (f[n][0][0][1][i]) cout << i << ' ';
        return 0;
    }
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0); 
    int T = 1;
    while (T --) Milkcat::main();
    return 0;
}

算法三

发现算法二中的 bitset 形式简单,考虑优化。

发现 bitset 仅维护了三种操作:

  1. 单点赋值为 1
  2. 两个 bitset 按位或。
  3. 查询最终的一个 bitset

将一个 DP 状态(即一个 bitset)看作一个点,将操作一挂在对应的点上,将操作二转移点与目标点间连一条边。由于 DP 的无后效性,所以建出来的图是 DAG。最后从最终状态出发,遍历能到达的点,处理操作一即可。

时间复杂度 O(n),还是有 18 的大常数。期望得分 100

代码实现:

#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
namespace Milkcat {
    typedef long long LL;
    typedef pair<LL, LL> pii;
    const int N = 4e6 + 5;
    int n, k, x, a[N], vis[N], g[N][3][3][2];
	vector<int> res, G[N], C[N];
	inline int id(int i, int j, int k, int p) { return i * 18 + j * 6 + k * 2 + p; }
	void dfs(int u) {
		for (int v : C[u]) vis[v] = 1;
		for (int v : G[u]) dfs(v);
	}
	int main() {
		cin >> n >> k;
		for (int i = 1; i <= k; i ++)
			cin >> x, a[x] ++;
		g[0][0][0][0] = 1;
		for (int i = 0; i < n; i ++) {
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++)
					for (int p = 0; p < 2; p ++) {
						if (!g[i][j][k][p]) continue;
						if (a[i + 1] >= j + k) g[i + 1][k][(a[i + 1] - j - k) % 3][p] = 1;
						if (!p && a[i + 1] >= j + k + 2) g[i + 1][k][(a[i + 1] - j - k - 2) % 3][1] = 1;
					}
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++)
					for (int p = 0; p < 2; p ++) {
						int t = id(i, j, k, p);
						if (a[i + 1] >= j + k) G[id(i + 1, k, (a[i + 1] - j - k) % 3, p)].push_back(t);
						if (!p && a[i + 1] >= j + k + 2) G[id(i + 1, k, (a[i + 1] - j - k - 2) % 3, 1)].push_back(t);
					}
			a[i + 1] ++;
			for (int j = 0; j < 3; j ++)
				for (int k = 0; k < 3; k ++)
					for (int p = 0; p < 2; p ++) {
						if (!g[i][j][k][p]) continue;
						if (a[i + 1] >= j + k) C[id(i + 1, k, (a[i + 1] - j - k) % 3, p)].push_back(i + 1);
						if (!p && a[i + 1] >= j + k + 2) C[id(i + 1, k, (a[i + 1] - j - k - 2) % 3, 1)].push_back(i + 1);
					}
		}
		dfs(id(n, 0, 0, 1));
		for (int i = 1; i <= n; i ++)
			if (vis[i]) res.push_back(i);
		if (res.empty()) cout << "QAQ\n";
		for (int v : res) cout << v << ' ';
        return 0;
    }
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0); 
    int T = 1;
    while (T --) Milkcat::main();
    return 0;
}
posted @   喵仔牛奶  阅读(86)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
主题色彩