1 条题解
-
0
思路与算法
每组文字和数列定义一个部分的一一映射:字母到数字,以及数字到字母。题目保证单组内部合法。两组相容,当且仅当共同字母没有指向不同数字、共同数字没有来自不同字母。以每组为一个顶点,两组相容就连边,要求最多可同时满足多少组,转成这个图的最大团大小。
最多40个顶点,折半为左右各至多20个。右半为每个子集S预处理dp[S]:其内部最大团大小。取S里任意顶点v,令T=S去掉v,则dp[S]=max(dp[T],1+dp[T与v的右半邻居交集]),分别表示不选v或选v。
左半枚举所有子集,递推判断它本身是否为团,同时维护其所有顶点在右半的公共邻居allowed[S]。若左子集是团,就可与右半allowed[S]中的任意团组合,候选答案为左侧人数加dp[allowed[S]]。空左子集也要覆盖,初始答案取完整右半的dp。
正确性说明
如果若干组能被同一个完整对应关系同时满足,那么任意两组对同一字母、同一数字的约束都不冲突,所以它们两两相容。反过来,如果若干组两两相容,所有部分映射的并集是一个一致且单射的部分对应关系:任何冲突都会在产生这两个约束的某两组间出现。尚未分配的字母和数字数量相等,可任意配对补全。因此两两相容充要,最大满足组数等于相容图最大团。
对于右半子集S,一个最优团要么不包含v,大小至多dp[T];要么包含v,其余顶点必须在T中且都邻接v,最优大小正是1+dp[T与邻居交集]。两类方案均可构造,所以递推准确。引用的子集都比S小,按掩码递增计算有效。
左半子集S从T再加v,它为团当且仅当T是团且T每个顶点都邻接v;右侧公共邻居也恰为allowed[T]与v右侧邻居的交集。因而两项递推均正确。每个完整团都能唯一分为左、右两部分,右部必位于左部公共邻居中;枚举对应左子集时,dp提供不差于其右部的最优选择。所有候选组合又确实为团,故枚举得到全局最优值。
复杂度
设左右顶点数为L、R,L+R=M且均不超过20。读取并建立组内映射O(M字符串长度+MN),两两相容检查O(M²N)。两侧子集分别作常数次位运算与转移,O(2^L+2^R)。总时间O(M最大长度+M²N+2^L+2^R),额外空间O(MN+2^L+2^R),包含映射和子集数组初始化。右最优值及左合法标记用unsigned char,公共邻居掩码用unsigned,两个20位子集的主体数组约6MiB,符合原64MiB限制。
易错点
- 目标按整组是否满足计数,不能把每个字符匹配次数当权值。
- 除字母到数字外,还必须检查数字到字母,防止多字母共用一个数字。
- 不同组可相同,输入仍表示不同组;相同约束的多组可以一起计入答案,不能去重删除顶点。
- 右dp存子集内最优团,子集本身不必是团;左侧只有本身为团的子集才能参与组合。
- 空左子集、单组、N=1、全部40组相容、完整映射互相冲突和随机部分约束均覆盖。
- 使用最多20位的半边掩码,避免用32位变量直接表示40个组。
参考代码(C++11)
#include <algorithm> #include <iostream> #include <string> #include <vector> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<vector<int>> f(m, vector<int>(n, -1)), inv = f; for (int i = 0; i < m; ++i) { string s; cin >> s; for (char c : s) { int d; cin >> d; --d; f[i][c - 'A'] = d; inv[i][d] = c - 'A'; } } int L = m / 2, R = m - L; vector<unsigned> al(L), ar(R), cross(L); for (int i = 0; i < m; ++i) for (int j = i + 1; j < m; ++j) { bool ok = true; for (int x = 0; x < n; ++x) { if (f[i][x] >= 0 && f[j][x] >= 0 && f[i][x] != f[j][x]) ok = false; if (inv[i][x] >= 0 && inv[j][x] >= 0 && inv[i][x] != inv[j][x]) ok = false; } if (ok) { if (j < L) { al[i] |= 1u << j; al[j] |= 1u << i; } else if (i >= L) { ar[i - L] |= 1u << (j - L); ar[j - L] |= 1u << (i - L); } else cross[i] |= 1u << (j - L); } } vector<unsigned char> dp(1u << R); for (unsigned s = 1; s < dp.size(); ++s) { int v = __builtin_ctz(s); unsigned t = s & (s - 1); dp[s] = max((int)dp[t], 1 + (int)dp[t & ar[v]]); } vector<unsigned char> valid(1u << L); vector<unsigned> allowed(1u << L); valid[0] = 1; allowed[0] = (1u << R) - 1; int ans = dp.back(); for (unsigned s = 1; s < valid.size(); ++s) { int v = __builtin_ctz(s); unsigned t = s & (s - 1); allowed[s] = allowed[t] & cross[v]; valid[s] = valid[t] && !(t & ~al[v]); if (valid[s]) ans = max(ans, __builtin_popcount(s) + (int)dp[allowed[s]]); } cout << ans << '\n'; }
- 1