<think>题目要求:根据给定的照片信息,推断出每个卡片(编号从1到M)对应的人(用两个大写字母表示)。如果无法确定,则输出'?'。
思路:
1. 我们可以使用图论的方法。构建一个二分图,左侧是人的名字(如"AB", "CD"等),右侧是卡片编号(1到M)。
2. 每张照片提供的信息:照片中有k个人,以及k个名字和k个卡片编号(但是不知道名字和卡片的对应关系)。
因此,这张照片告诉我们:这k个名字和这k个卡片编号之间存在一个完全匹配,但具体匹配关系未知。
3. 因此,对于一张照片,我们可以得到:这k个名字中的每个名字都只能与这k个卡片编号中的某一个匹配(不能匹配到其他编号),
同样,这k个卡片编号中的每个编号都只能匹配这k个名字中的某一个(不能匹配到其他名字)。
4. 我们可以将问题转化为:建立名字和卡片之间的可能连接(边),然后通过不断排除不可能的情况(即删边)来得到确定的匹配。
具体地,对于每张照片,我们可以在对应的名字集合和卡片集合之间建立一个完全二分图(即名字集合中的每个名字和卡片集合中的每个卡片都连一条边)。
但是,这样会建立很多边。然后,我们通过传递的信息(约束)来删除不可能的边。
5. 然而,题目保证没有冲突结论,但可能无法完全确定。我们需要得到每个卡片对应的名字(如果能够确定的话),否则输出'?'。
6. 另一种思路:我们记录每个名字可能对应的卡片集合,以及每个卡片可能对应的名字集合。
初始时,每个名字可以对应1~M的任意卡片,每个卡片可以对应任意名字(但名字是有限的,即输入中出现的名字,以及注意总共有M个名字,但题目没有说名字就是M个?注意:题目说M是朋友的数量,所以总共有M个人,也就是M个名字。但是输入中给出的名字是2个字母的代码,并且有M个不同的代码?注意:输入的第一行M是朋友数量,所以有M个名字,每个名字是2个字母的代码,且题目保证名字是2个英文大写字母,并且是k个不同的名字。
7. 但是注意:输入中给出的名字不一定包含所有M个名字!因为每张照片只包含一部分人。所以我们需要先收集所有出现过的名字?然而,输出要求从卡片1到M,输出对应的名字(如果无法确定则输出'?')。所以我们必须知道所有名字?但是题目没有给出所有名字的列表,怎么办?
8. 重新读题:第一行给出M和N,M是朋友数量(即总人数),N是照片数量。然后每张照片给出k个人(名字)和k个卡片编号(但是不给出对应关系)。注意:题目说“每个朋友对应一个卡片编号(1到M)”,但是输入中并没有给出所有M个人的名字。所以我们需要通过照片信息来收集所有出现过的名字?但是可能有些名字从未出现,那么这些卡片对应的人就永远无法确定?所以输出时,对于没有出现过的名字对应的卡片,我们只能输出'?'。
9. 但是题目说“保证没有冲突结论”,并且“可能无法确定”。所以我们需要处理的是:对于每个卡片,如果我们能确定它对应哪个名字,就输出名字;否则输出'?'。
10. 关键:我们不知道所有名字的集合。所以我们需要先收集所有出现过的名字。但是,注意:总共有M个人,但照片中可能没有出现所有人。那么没有出现的人的名字我们不知道,所以他们的卡片编号对应的人就无法确定?但是题目要求输出从卡片1到M的对应名字,所以我们必须知道每个卡片对应的人的名字,但如果没有出现过,那么我们就无法确定,输出'?'。
11. 然而,题目没有给出所有名字的列表,所以我们只能通过照片来收集名字。但是,有可能某个人没有在任何照片中出现,那么我们就无法知道他的名字(因为输入没有给出)。这怎么办?注意:题目说“each of them corresponds to a distinct card number”,所以有M个人,但是输入中并没有给出这M个人的名字列表。我们只能通过照片信息来获取名字。
12. 因此,我们需要:记录所有出现过的名字。但是,如果一个人没有在任何照片中出现,那么我们就不知道他的名字。那么输出时,对于卡片编号,如果这个卡片编号在照片中出现过(即某张照片的卡片编号列表中有这个编号),那么我们就需要推断出它对应哪个名字;如果这个卡片编号从未在照片中出现,那么显然我们无法确定(因为连约束条件都没有),所以输出'?'。但是,这里有一个问题:即使一个卡片编号在照片中出现过,我们也不一定能唯一确定名字;同样,一个名字在照片中出现过,我们也不一定能确定它对应的卡片编号。
13. 所以,我们可能需要建立两个映射:
name_to_possible_cards: 每个名字可能对应的卡片集合(初始为全集,然后根据照片缩小范围)
card_to_possible_names: 每个卡片可能对应的名字集合(初始为全集,然后根据照片缩小范围)
14. 但是,初始全集有多大?名字的全集我们并不知道(因为输入没有给出所有名字),卡片是1~M。
15. 因此,我们需要先收集所有出现过的名字。我们可以遍历所有照片,记录所有出现过的名字。但是,注意:总共有M个人,所以名字应该有M个。但是,如果照片中只出现了部分名字,那么剩下的名字我们不知道,也无法建立映射?所以,我们只能对出现过的名字建立映射,没有出现的名字我们暂时不管。
16. 但是,输出要求:按卡片编号1到M输出对应的名字。如果某个卡片编号对应的人(名字)没有在照片中出现过,那么我们无法知道他的名字(因为输入没有给出),所以只能输出'?'。但是,我们如何知道某个卡片编号对应的人的名字呢?如果没有出现过,我们连名字是什么都不知道,所以输出'?'是合理的。
17. 然而,题目没有说名字是未知的,而是说“他们不记得谁拿了哪张卡”,但是名字是已知的(因为照片背面有名字),但是照片中可能没有包含所有人。所以,我们只能通过照片来获取名字。因此,我们只能处理照片中出现过的名字。
18. 所以,步骤:
Step 1: 读取M, N。
Step 2: 初始化:
card_to_names: 一个大小为M+1的数组(从1到M),每个元素是一个集合,初始包含所有出现过的名字?但是初始时我们不知道任何名字,所以我们可以先初始化为空?不对,后面我们会通过照片加入名字。
Step 3: 遍历N张照片:
读取k,然后读取k个名字(存入一个临时数组names),读取k个卡片编号(存入一个临时数组cards)。
注意:这k个名字和k个卡片编号是出现在同一张照片中,所以它们之间有关系:这k个名字和这k个卡片编号构成一个匹配(但不知道具体对应关系)。
对于这张照片,我们得到的信息是:
名字集合S = {names[0], names[1], ...} 只能匹配卡片集合T = {cards[0], cards[1], ...},而不能匹配其他卡片。
反过来,卡片集合T中的每个卡片也只能匹配S中的某个名字。
因此,我们可以用这个信息来更新每个名字的可能卡片集合,以及每个卡片的可能名字集合。
具体更新:
对于每个名字s in S:
它之前可能有一个可能卡片集合,现在我们知道它只能出现在T中,所以我们可以将其可能卡片集合与T取交集(如果之前没有,那么初始化为T)。
同样,对于每个卡片c in T:
它的可能名字集合与S取交集。
但是,注意:初始时,我们还没有为名字s建立可能卡片集合,所以我们可以这样处理:
如果s是第一次出现,则它的可能卡片集合初始化为T。
否则,更新为:possible_cards[s] = possible_cards[s] ∩ T
同样,对于卡片c(c在T中):
如果c是第一次被更新,则它的可能名字集合初始化为S(即names的集合)。
否则,更新为:possible_names[c] = possible_names[c] ∩ S
Step 4: 但是,这样更新后,我们可能得到一些集合大小大于1的情况(即无法确定),我们需要进一步利用约束条件:每个名字只能对应一个卡片,每个卡片只能对应一个名字。
19. 然而,仅仅通过交集更新可能还不够,因为信息是逐步给出的。但是,题目保证没有冲突,所以我们可以通过不断更新直到稳定(类似约束传播)。但是,题目没有说数据量很大(M<=54,N<=100),所以我们可以迭代更新直到没有变化。
20. 但是,我们如何利用“唯一性”来进一步缩小范围?
如果一个名字s的可能卡片集合缩小到只有一个卡片c,那么:
我们可以将卡片c的可能名字集合缩小为只包含s(因为卡片c只能对应一个名字,而s已经确定对应c,那么其他名字就不能再对应c了)。
同样,如果一个卡片c的可能名字集合缩小到只有一个名字s,那么:
名字s的可能卡片集合就可以缩小为只包含c。
然后,这个更新可能会引起连锁反应,所以我们需要迭代更新直到没有变化。
21. 算法步骤:
初始化:
possible_cards: 一个map,键为名字,值为卡片集合(初始为空,或者我们第一次遇到这个名字时初始化为该照片的卡片集合,然后后续照片取交集)
possible_names: 一个大小为M+1的数组(索引1到M),每个是一个集合(名字的集合),初始为空,第一次遇到该卡片时初始化为该照片的名字集合,然后后续取交集。
然后,我们还需要一个队列,用于传播更新:当某个名字的卡片集合大小变为1,或者某个卡片的集合大小变为1时,我们将其加入队列,然后传播更新。
具体传播:
当名字s的卡片集合变为{c},那么:
对于卡片c:将其可能名字集合更新为{s}(这样就会删除其他名字,然后如果其他名字之前包含c,那么也要从其他名字的卡片集合中删除c,并检查这些名字的集合是否变为1,如果是则加入队列)。
同时,对于其他名字,如果它们之前包含c,那么需要从它们的卡片集合中删除c(因为c已经被s占用了),然后检查这些名字的集合是否变为1。
同样,当卡片c的名字集合变为{s},那么:
对于名字s:将其卡片集合更新为{c}(如果之前不是,那么更新并传播)。
同时,对于其他卡片,如果它们之前包含s,那么需要从它们的名字集合中删除s(因为s已经被c占用了),然后检查这些卡片的集合是否变为1。
但是,注意:我们更新一个名字(或卡片)时,可能会影响其他名字和卡片,所以需要传播。
22. 然而,题目中M和N都不大,我们可以迭代更新直到没有变化。
23. 实现步骤:
a. 初始化两个映射:
map<string, set<int>> possible_cards; // 每个名字可能的卡片集合
vector<set<string>> possible_names(M+1); // 每个卡片可能的名字集合,索引1到M
b. 注意:初始时,这两个映射都是空的。
c. 遍历每张照片:
读取k,然后k个名字(存入vector<string> names),k个卡片编号(存入vector<int> cards)。
将names存入一个集合S,cards存入一个集合T。
对于每个名字s in names:
如果possible_cards中还没有s,则添加s,其卡片集合为T。
否则,将possible_cards[s]更新为原集合与T的交集。
并且,如果更新后,集合大小变为1,那么将这个名字加入队列(用于传播)。
对于每个卡片c in cards:
如果possible_names[c]为空(即还没有初始化),则将其初始化为S。
否则,将possible_names[c]更新为原集合与S的交集。
并且,如果更新后,集合大小变为1,那么将这个卡片加入队列。
d. 然后,我们使用一个队列(可以同时处理名字和卡片)来传播更新。由于我们有两种类型(名字和卡片),我们可以用两个队列,或者一个队列,但是需要标记类型。这里我们可以用一个队列,队列中存储两种事件:名字事件(表示某个名字的可能卡片集合发生变化)和卡片事件(表示某个卡片的可能名字集合发生变化)。
但是,为了简单,我们可以分别处理:我们每次遍历所有名字和所有卡片,检查是否有更新,直到没有更新为止?但是这样效率不高,M很小,所以可以。
或者,我们使用一个队列,队列元素是一个pair,第一个元素表示类型(0表示名字,1表示卡片),第二个元素表示名字字符串或卡片编号。
然后,我们进行广度优先传播。
e. 传播规则:
当从队列中取出一个名字s:
检查possible_cards[s]的大小:
如果为0,说明冲突(但题目保证无冲突,所以不考虑)。
如果为1,则设c0为这个唯一的卡片。
然后,遍历卡片c0的possible_names集合(注意:此时这个集合可能包含多个名字,但我们要把除了s以外的名字都删除?不,我们已经在更新卡片c0的集合时通过交集得到了一个集合,现在因为名字s已经确定对应c0,那么我们需要在卡片c0的集合中删除其他名字吗?但是,我们之前已经用交集更新了,所以卡片c0的集合中应该包含s,但可能还有别的名字。然而,我们这里需要传播:因为名字s占用了c0,所以其他名字不能再用c0。
因此,我们需要:对于卡片c0,将其可能名字集合更新为{s}(因为只有s能对应c0)。但是,这样更新后,如果卡片c0的集合大小发生变化(比如从多个变成了1个),那么我们需要将c0加入队列(作为卡片事件)。
同时,对于其他名字t(t属于原来卡片c0的可能名字集合中除s以外的名字),我们需要从possible_cards[t]中删除c0。如果删除后,t的集合大小变为1,那么将t加入队列。
当从队列中取出一个卡片c:
检查possible_names[c]的大小:
如果为1,则设s0为这个唯一的名字。
然后,遍历名字s0的possible_cards集合(同样,此时这个集合可能包含多个卡片),我们要将除了c以外的卡片都删除?不,我们通过传播:名字s0只能对应c,所以其他卡片不能再用s0。
因此,我们需要:对于名字s0,将其卡片集合更新为{c}(这样更新后,如果名字s0的集合大小发生变化,比如从多个变成1个,那么s0可能已经被处理过?但这里我们更新后,如果s0的集合大小变为1,那么将s0加入队列?但是注意:我们正在处理卡片c,然后更新名字s0,所以名字s0的集合会变成{c},如果之前s0的集合大小大于1,那么现在变为1,所以需要加入队列。
同时,对于其他卡片d(d属于原来名字s0的卡片集合中除c以外的卡片),我们需要从possible_names[d]中删除s0。如果删除后,d的集合大小变为1,那么将d加入队列。
f. 注意:在传播过程中,我们可能会多次将同一个名字或卡片加入队列,所以我们需要确保在更新时检查是否真的发生了变化。
24. 但是,这个算法比较复杂,而且M和N都很小,我们可以采用更简单的方法:迭代更新直到稳定(即没有集合发生变化)。迭代次数最多为M(因为每个集合最多减少M次),所以我们可以迭代多次。
25. 另一种更简单的思路:我们不断进行以下两个步骤直到没有变化:
a. 对于每个名字,如果它的可能卡片集合大小为1,设为c,那么对于这个卡片c,将它的可能名字集合设为这个唯一的名字(同时,这个卡片c在其他名字的可能卡片集合中删除)。
b. 对于每个卡片,如果它的可能名字集合大小为1,设为s,那么对于这个名字s,将它的可能卡片集合设为这个唯一的卡片(同时,这个名字s在其他卡片的可能名字集合中删除)。
但是,注意:在a步骤中,当我们删除一个名字的某个卡片时,可能会导致另一个名字的卡片集合发生变化(比如变为1),所以我们需要迭代。
具体迭代:
changed = true
while changed:
changed = false
for 每个名字s:
if possible_cards[s].size() == 1:
c = 这个唯一的卡片
# 然后检查卡片c的可能名字集合,如果它的大小大于1,那么我们需要将卡片c的可能名字集合更新为{s}(因为名字s已经确定对应c,所以卡片c只能对应s)
# 但是,注意:我们之前已经通过交集更新了,所以卡片c的集合中可能包含s和其他名字。现在,我们通过这个规则来更新:将卡片c的可能名字集合更新为{s},这会导致卡片c的集合发生变化(从多个名字变为一个名字),所以我们需要标记changed=true,并且同时,对于卡片c的集合中除s以外的所有名字t,从possible_cards[t]中删除c(这又可能导致t的集合发生变化)。
# 但是,我们在这里并不立即传播,而是先记录,然后在下一次迭代中处理?或者我们立即传播?为了正确性,我们最好立即更新受影响的名字和卡片。
所以,我们改为:
while (存在某个名字s,其卡片集合大小为1) 或者 (存在某个卡片c,其名字集合大小为1):
先处理所有名字集合大小为1的名字:
对于每个名字s(如果其卡片集合大小为1):
取出c = *possible_cards[s].begin()
然后,对于卡片c,我们将其名字集合设置为{s}(注意:这个操作会删除c原来集合中除s以外的所有名字)。但是,这个操作会影响其他名字:对于被删除的每个名字t(t≠s),我们需要在t的卡片集合中删除c。这可能会导致t的卡片集合变为空(冲突,但题目保证不会)或变为1(则加入待处理)。
然后处理所有卡片集合大小为1的卡片:
对于每个卡片c(如果其名字集合大小为1):
取出s = *possible_names[c].begin()
然后,对于名字s,我们将其卡片集合设置为{c}(同样,这个操作会删除s原来集合中除c以外的所有卡片),然后对于被删除的每个卡片d(d≠c),我们需要在d的名字集合中删除s。
但是,这样交替处理,可能会死循环?或者我们需要一个队列。
26. 由于M很小,我们可以采用队列来传播更新。但是,为了简单,我们可以多次迭代整个名字列表和卡片列表(比如迭代M次),因为每次迭代至少减少一个可能性。
27. 但是,我们也可以使用队列。这里我们使用两个队列:一个用于名字,一个用于卡片。
初始化:将所有名字(其卡片集合大小已经为1的)加入名字队列;将所有卡片(其名字集合大小已经为1的)加入卡片队列。
然后,循环直到两个队列都为空:
处理名字队列:
取出一个名字s,如果其卡片集合大小为1(否则跳过,因为可能被其他更新改变了):
c = 唯一的那个卡片
然后,我们检查卡片c的名字集合:如果大小大于1,那么我们需要将卡片c的名字集合更新为{s}(因为名字s已经确定对应c,所以卡片c只能对应s)。然后,对于卡片c的原名字集合中除s以外的每个名字t:
在t的卡片集合中删除c(这可能导致t的卡片集合大小变为1,那么将t加入名字队列)
然后,将卡片c加入卡片队列(因为卡片c的名字集合现在变为1了,所以需要检查传播)
处理卡片队列:
取出一个卡片c,如果其名字集合大小为1(否则跳过):
s = 唯一的那个名字
然后,检查名字s的卡片集合:如果大小大于1,那么将名字s的卡片集合更新为{c}。
然后,对于名字s的原卡片集合中除c以外的每个卡片d:
在d的名字集合中删除s(这可能导致d的名字集合大小变为1,将d加入卡片队列)
然后,将名字s加入名字队列(因为名字s的卡片集合现在变为1了,所以需要检查传播)
28. 但是,注意:在更新过程中,我们可能会重复加入同一个名字或卡片,所以我们需要在更新集合后检查是否变为1,然后才加入队列。另外,当我们从队列中取出一个元素时,它可能已经被更新过了(不再是1),所以需要检查。
29. 实现细节:
- 我们使用:
map<string, set<int>> name_to_cards;
vector<set<string>> card_to_names; // 大小为M+1,索引1..M
- 初始化:在读取每张照片后,更新name_to_cards和card_to_names(取交集)。
- 然后,我们初始化两个队列:
queue<string> name_queue;
queue<int> card_queue;
遍历所有名字:如果name_to_cards[s].size()==1,则将s加入name_queue。
遍历所有卡片(1..M):如果card_to_names[c].size()==1,则将c加入card_queue。
- 然后,循环直到两个队列都为空:
while (!name_queue.empty() || !card_queue.empty()) {
// 先处理名字队列
while (!name_queue.empty()) {
string s = name_queue.front(); name_queue.pop();
if (name_to_cards[s].size() != 1) continue; // 可能已经被其他更新改变了
int c = *name_to_cards[s].begin();
// 更新卡片c:将卡片c的名字集合设置为{s}
// 但是,如果卡片c的名字集合已经是{s}(即大小已经是1且就是s),那么跳过?
// 否则,我们需要记录卡片c原来的名字集合(除s以外的名字)
set<string> old_set = card_to_names[c]; // 复制
if (old_set.size() == 1 && *old_set.begin() == s) {
// 已经更新过了,跳过
continue;
}
// 将卡片c的名字集合更新为{s}
card_to_names[c] = set<string>{s};
// 对于old_set中除s以外的每个名字t,在t的卡片集合中删除c
for (string t : old_set) {
if (t == s) continue;
// 在t的卡片集合中删除c
if (name_to_cards.find(t) != name_to_cards.end()) { // 确保t存在
if (name_to_cards[t].count(c)) {
name_to_cards[t].erase(c);
// 如果删除后,t的卡片集合大小变为1,则将t加入名字队列
if (name_to_cards[t].size() == 1) {
name_queue.push(t);
}
}
}
}
// 然后,因为卡片c的名字集合变成了1,所以将c加入卡片队列
card_queue.push(c);
}
// 然后处理卡片队列
while (!card_queue.empty()) {
int c = card_queue.front(); card_queue.pop();
if (card_to_names[c].size() != 1) continue;
string s = *card_to_names[c].begin();
// 更新名字s:将名字s的卡片集合设置为{c}
set<int> old_set = name_to_cards[s]; // 复制
if (old_set.size() == 1 && *old_set.begin() == c) {
continue;
}
name_to_cards[s] = set<int>{c};
// 对于old_set中除c以外的每个卡片d,在d的名字集合中删除s
for (int d : old_set) {
if (d == c) continue;
if (d >= 1 && d <= M) { // 确保d在合法范围
if (card_to_names[d].count(s)) {
card_to_names[d].erase(s);
if (card_to_names[d].size() == 1) {
card_queue.push(d);
}
}
}
}
// 然后,因为名字s的卡片集合变成了1,所以将s加入名字队列
name_queue.push(s);
}
}
30. 但是,这个传播过程可能会相互调用,直到没有变化。
31. 最后,我们输出:对于卡片编号从1到M:
如果card_to_names[c].size()==1,那么输出*card_to_names[c].begin()
否则,输出'?'
32. 但是,注意:有些卡片可能从未在照片中出现过,那么card_to_names[c]是空集(因为我们没有初始化它)。所以,对于没有在照片中出现的卡片,我们直接输出'?'。
33. 问题:在读取照片时,我们只更新了照片中出现的卡片。对于没有出现的卡片,card_to_names[c]是空集(或者我们初始化时没有设置?我们初始化card_to_names为大小为M+1的vector,每个元素初始为空集)。所以,对于没有在照片中出现的卡片,我们输出'?'。
34. 但是,还有一种情况:卡片在照片中出现了,但是经过传播后,它的可能名字集合大小大于1(无法确定),那么输出'?'。
35. 然而,我们还需要考虑名字:有些名字可能没有出现在任何照片中,那么这些名字对应的卡片编号我们无法确定?但是,题目要求输出卡片编号1到M对应的名字。所以,我们只关心卡片编号对应的名字,而不需要知道所有名字。
36. 但是,在传播过程中,我们只处理了在照片中出现过的名字和卡片。对于没有出现过的卡片,我们不会加入队列,也不会更新,所以最后card_to_names[c]为空,输出'?'。
37. 但是,有一个问题:一个名字在照片中出现过,但是对应的卡片编号可能被传播为唯一,那么即使这个卡片编号在之前的照片中没有直接出现,我们也能确定?不,我们每张照片只更新了照片中出现的卡片和名字。所以,如果一张照片中出现了名字s,那么s的可能卡片集合初始化为该照片的卡片集合,然后后续照片会取交集。但是,如果s没有在后面的照片中出现,那么它的集合就不会再更新。然后,通过传播,我们可能能确定s对应的卡片。
38. 所以,我们只需要按照上述步骤处理即可。
39. 但是,在初始化时,我们只处理了照片中出现的名字和卡片。对于没有出现过的卡片,我们不会初始化card_to_names[c](即它保持为空集)。所以,最后输出时,如果card_to_names[c]为空,那么输出'?'。但是,如果卡片c在照片中出现过,那么我们在读取照片时已经初始化了card_to_names[c](初始化为该照片的名字集合,然后后续照片取交集),然后传播后可能变为1个名字,也可能还是多个(则输出'?')。
40. 但是,注意:在传播过程中,我们可能会删除名字,所以即使一个卡片在照片中出现过,最后它的可能名字集合也可能为空(冲突),但题目保证无冲突,所以不会出现空集(除非初始交集为空,但题目保证无冲突结论,所以不会出现初始交集为空的情况)。
41. 因此,我们按照上述步骤实现。
42. 但是,还有一点:在读取照片时,我们更新名字s的可能卡片集合:如果s是第一次出现,则初始化为T(该照片的卡片集合);否则,取交集。同样,更新卡片c的名字集合:如果c是第一次出现,则初始化为S(该照片的名字集合);否则,取交集。
43. 注意:取交集时,如果交集为空,那么说明冲突,但题目保证不会。
44. 代码步骤:
int M, N;
cin >> M >> N;
vector<set<string>> card_to_names(M+1); // 1-indexed, card_to_names[0] unused
map<string, set<int>> name_to_cards;
for (int i=0; i<N; i++) {
int k;
cin >> k;
vector<string> names(k);
for (int j=0; j<k; j++) {
cin >> names[j];
}
vector<int> cards(k);
for (int j=0; j<k; j++) {
cin >> cards[j];
}
set<string> nameSet(names.begin(), names.end());
set<int> cardSet(cards.begin(), cards.end());
// 更新每个名字的可能卡片集合
for (string name : names) {
if (name_to_cards.find(name) == name_to_cards.end()) {
// 第一次出现,初始化
name_to_cards[name] = cardSet;
} else {
// 取交集
set<int> newSet;
set_intersection(name_to_cards[name].begin(), name_to_cards[name].end(),
cardSet.begin(), cardSet.end(),
inserter(newSet, newSet.begin()));
name_to_cards[name] = newSet;
}
}
// 更新每个卡片的可能名字集合
for (int card : cards) {
if (card_to_names[card].empty()) {
// 第一次出现,初始化为nameSet
card_to_names[card] = nameSet;
} else {
set<string> newSet;
set_intersection(card_to_names[card].begin(), card_to_names[card].end(),
nameSet.begin(), nameSet.end(),
inserter(newSet, newSet.begin()));
card_to_names[card] = newSet;
}
}
}
// 然后,我们初始化队列
queue<string> name_queue;
queue<int> card_queue;
// 遍历所有名字,将集合大小为1的名字加入name_queue
for (auto& p : name_to_cards) {
if (p.second.size() == 1) {
name_queue.push(p.first);
}
}
// 遍历所有卡片(1..M),将集合大小为1的卡片加入card_queue
for (int c=1; c<=M; c++) {
if (card_to_names[c].size() == 1) {
card_queue.push(c);
}
}
// 然后传播
while (!name_queue.empty() || !card_queue.empty()) {
// 先处理名字队列
while (!name_queue.empty()) {
string s = name_queue.front(); name_queue.pop();
if (name_to_cards[s].size() != 1) continue; // 可能已经被更新了
int c = *name_to_cards[s].begin();
// 更新卡片c:将其名字集合设为{s}
// 如果卡片c的名字集合已经是{s}(且大小为1),则跳过,否则更新
if (card_to_names[c].size() == 1 && *card_to_names[c].begin() == s) {
continue;
}
set<string> old_set = card_to_names[c]; // 保存旧的集合
card_to_names[c] = set<string>{s}; // 更新为只包含s
// 遍历old_set中除s以外的每个名字t,从t的卡片集合中删除c
for (string t : old_set) {
if (t == s) continue;
// 如果t在name_to_cards中(肯定在,因为old_set来自之前的更新,而t是之前卡片c的可能名字)
if (name_to_cards.find(t) != name_to_cards.end()) {
// 如果t的卡片集合包含c,则删除
if (name_to_cards[t].count(c)) {
name_to_cards[t].erase(c);
// 如果删除后,t的卡片集合大小为1,则加入名字队列
if (name_to_cards[t].size() == 1) {
name_queue.push(t);
}
}
}
}
// 现在卡片c的集合已经更新,所以将c加入卡片队列(因为现在大小变为1)
card_queue.push(c);
}
// 处理卡片队列
while (!card_queue.empty()) {
int c = card_queue.front(); card_queue.pop();
if (card_to_names[c].size() != 1) continue;
string s = *card_to_names[c].begin();
// 更新名字s:将其卡片集合设为{c}
if (name_to_cards[s].size() == 1 && *name_to_cards[s].begin() == c) {
continue;
}
set<int> old_set = name_to_cards[s]; // 保存旧的集合
name_to_cards[s] = set<int>{c}; // 更新为只包含c
// 遍历old_set中除c以外的每个卡片d,从d的名字集合中删除s
for (int d : old_set) {
if (d == c) continue;
if (d>=1 && d<=M) {
if (card_to_names[d].count(s)) {
card_to_names[d].erase(s);
if (card_to_names[d].size() == 1) {
card_queue.push(d);
}
}
}
}
// 现在名字s的集合已经更新,所以将s加入名字队列(因为现在大小变为1)
name_queue.push(s);
}
}
// 输出:对于卡片1到M
for (int c=1; c<=M; c++) {
if (card_to_names[c].size() == 1) {
cout << *card_to_names[c].begin() << endl;
} else {
cout << "?" << endl;
}
}
45. 但是,这个代码可能有一个问题:在更新名字s的卡片集合为{c}后,我们遍历old_set(即原来的卡片集合)中除c以外的每个卡片d,然后从card_to_names[d]中删除s。但是,这个删除操作可能会影响其他名字吗?不会,因为s只对应c了,所以其他卡片d不能再有s,所以需要删除。
46. 但是,注意:在更新名字s的卡片集合为{c}之前,名字s的卡片集合可能有多个,然后我们更新为{c},然后删除其他卡片d中关于s的记录。这样,这些卡片d的集合大小可能变为1(如果原来大小为2,删掉一个变成1),那么就需要加入队列。
47. 另外,注意:在更新过程中,我们可能会删除一些名字或卡片,但不会出现空集(因为题目保证无冲突)。
48. 但是,还有一种情况:一个名字可能出现在多张照片中,但通过交集后,它的卡片集合变为空?题目保证无冲突,所以不会。
49. 最后,我们还需要注意:在读取照片时,名字和卡片的交集更新。我们使用了set_intersection,需要包含头文件<algorithm>,并且注意集合必须有序(set是有序的)。
50. 但是,在C++中,set_intersection要求两个集合是有序的,而set本身有序,所以可以直接用。
51. 但是,set_intersection的用法:
set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(), inserter(newSet, newSet.begin()));
52. 另外,我们只处理了出现过的名字。对于没有出现过的名字,我们不会加入name_to_cards,所以不会影响传播。
53. 但是,在输出时,我们只关心卡片对应的名字。对于没有在照片中出现的卡片,card_to_names[c]为空(因为我们没有在照片中更新它),所以输出'?'。
54. 但是,注意:在传播过程中,一个卡片即使没有在照片中出现,但是通过传播(从其他名字和卡片传播)会更新吗?不会,因为我们只更新了在照片中出现过的卡片(在读取照片时初始化了),没有出现的卡片我们不会初始化,也不会在传播过程中被更新(因为传播的起点是照片中出现过的名字和卡片)。所以,没有出现过的卡片,card_to_names[c]一直为空,输出'?'。
55. 因此,代码如上。
56. 但是,我们还需要考虑:一个名字可能出现在多张照片中,我们通过取交集更新了它的可能卡片集合。同样,卡片也是。
57. 测试样例:
输入:8 5
2
AB CD
3 6
3
EF GH IJ
1 2 5
3
AB GH IJ
5 2 6
3
KL GH CD
4 2 3
2
UV XY
7 8
输出:
EF
GH
CD
KL
IJ
AB
?
?
我们手动模拟:
第一张照片:AB, CD -> {3,6}
AB: {3,6}; CD: {3,6}
卡片3: {AB,CD}; 卡片6: {AB,CD}
第二张照片:EF, GH, IJ -> {1,2,5}
EF: {1,2,5}; GH: {1,2,5}; IJ: {1,2,5}
卡片1: {EF,GH,IJ}; 卡片2: {EF,GH,IJ}; 卡片5: {EF,GH,IJ}
第三张照片:AB, GH, IJ -> {5,2,6}
AB: {3,6} ∩ {2,5,6} -> 取交集:{6} (因为3不在{2,5,6}中,所以AB只能对应6?但是注意,第三张照片的卡片是5,2,6,所以AB的可能卡片集合变为{6}(因为之前是{3,6},交集为{6}) -> 所以AB确定为卡片6,加入队列。
GH: {1,2,5} ∩ {2,5,6} -> {2,5}
IJ: {1,2,5} ∩ {2,5,6} -> {2,5}
卡片2: 原集合{EF,GH,IJ} ∩ {AB,GH,IJ} -> 因为AB不在原集合中(原集合是EF,GH,IJ),所以交集为{GH,IJ}
卡片5: 原集合{EF,GH,IJ} ∩ {AB,GH,IJ} -> {GH,IJ}
卡片6: 原集合{AB,CD} ∩ {AB,GH,IJ} -> {AB} (因为CD不在{AB,GH,IJ}中,所以交集为{AB}) -> 所以卡片6确定为AB,加入队列。
然后,开始传播:
名字队列:AB(因为AB的卡片集合大小为1,卡片6)
处理AB:卡片6的名字集合已经更新为{AB}(在第三张照片后更新了?注意:在第三张照片中,我们更新了卡片6的集合为{AB}(因为第三张照片中卡片6的可能名字集合是{AB,GH,IJ}与之前的{AB,CD}取交集为{AB})?不对,第三张照片后,我们更新了卡片6:初始(第一张照片后)卡片6的可能名字集合为{AB,CD},第三张照片中卡片6的名字集合是{AB,GH,IJ},取交集:{AB}。所以卡片6的集合在第三张照片后已经变为{AB},所以在第三张照片后,我们就将卡片6加入队列(因为大小为1)。
所以,在第三张照片后,我们初始化队列时,名字队列:AB(因为AB的卡片集合大小为1)和卡片队列:卡片6(因为大小为1)都会被加入。
我们先处理名字队列AB:它对应卡片6,然后更新卡片6:但卡片6已经为{AB},所以跳过?不,我们检查到卡片6的名字集合大小为1且就是AB,所以跳过。
然后处理卡片队列6:卡片6对应AB,然后更新名字AB:名字AB的卡片集合已经为{6},所以跳过。
然后,第三张照片后,我们还有GH和IJ的卡片集合为{2,5},卡片2和5的集合为{GH,IJ}。
第四张照片:KL, GH, CD -> {4,2,3}
KL: 第一次出现 -> {2,3,4}
GH: 原来{2,5} ∩ {2,3,4} -> {2}
CD: 原来{3,6} ∩ {2,3,4} -> {3}
所以GH确定为2,CD确定为3。
卡片2:原来{GH,IJ} ∩ {GH,KL,CD} -> {GH} (因为IJ不在{GH,KL,CD}中,所以交集为{GH}) -> 所以卡片2确定为GH,加入队列。
卡片3:原来为空(因为卡片3之前没有出现过?注意第一张照片有卡片3?第一张照片的卡片是3,6,所以卡片3在第一张照片后初始化为{AB,CD},然后第四张照片的卡片3的名字集合为{KL,GH,CD},取交集:{CD}(因为AB不在第四张照片中,所以交集为{CD}) -> 所以卡片3确定为CD,加入队列。
卡片4:第一次出现,初始化为{KL,GH,CD}。
然后,第四张照片后,我们更新队列:
名字队列:GH(大小为1,卡片2)、CD(大小为1,卡片3)、KL(大小为3,不加入)
卡片队列:卡片2(大小为1)、卡片3(大小为1)、卡片4(大小为3,不加入)
处理名字队列:
GH:对应2,更新卡片2:将卡片2的名字集合设为{GH}(原来已经是{GH}?因为第四张照片后已经更新为{GH},所以跳过?)
CD:对应3,更新卡片3:设为{CD}(原来已经是{CD},跳过)
处理卡片队列:
卡片2:对应GH,更新名字GH:将GH的卡片集合设为{2}(原来已经是{2},跳过)
卡片3:对应CD,更新名字CD:设为{3}(原来已经是{3},跳过)
然后,我们看IJ:在第三张照片后,IJ的卡片集合为{2,5},但是第四张照片后,卡片2已经被GH占用,所以我们需要从IJ的卡片集合中删除2?但是,在第四张照片中,我们并没有更新IJ?因为第四张照片没有出现IJ。所以,IJ的卡片集合还是{2,5}?但是,在传播过程中,当卡片2被确定为GH时,我们会在更新卡片2的名字集合为{GH}时,同时删除其他名字(即IJ)在卡片2中的记录,并且从IJ的卡片集合中删除2?这个删除操作是在传播过程中由卡片2的更新触发的。
具体:在第四张照片后,卡片2的名字集合更新为{GH}(在第四张照片中,我们通过交集更新,卡片2的名字集合变为{GH},然后我们将其加入队列)。然后,在处理卡片2时,我们更新名字GH(将GH的卡片集合设为{2}),同时,我们还要遍历卡片2的旧名字集合(即{GH,IJ})中除GH以外的名字(即IJ),然后从IJ的卡片集合中删除2。这样,IJ的卡片集合就从{2,5}变成了{5}(大小为1),所以将IJ加入名字队列。
然后,名字队列中有IJ:处理IJ,它对应5,然后更新卡片5:将卡片5的名字集合设为{IJ}(原来卡片5的名字集合是{GH,IJ},在第二张照片后,然后第三张照片更新为{GH,IJ}(因为第三张照片有IJ,卡片5,然后与第二张照片的交集还是{GH,IJ}?不对,第三张照片中卡片5的名字集合:第二张照片后为{EF,GH,IJ},第三张照片的名字集合为{AB,GH,IJ},取交集为{GH,IJ}。然后第四张照片没有出现卡片5,所以不变。现在,在传播过程中,我们更新卡片5:将卡片5的名字集合设为{IJ}(因为IJ确定为5),然后遍历卡片5的旧集合{GH,IJ},删除GH(在GH的卡片集合中删除5?但是GH的卡片集合已经确定为{2},所以删除5没有影响(因为GH的卡片集合中已经没有5了,因为第四张照片后GH的卡片集合已经是{2}了,没有5了)?所以这一步可能不需要?但是我们在更新名字GH的卡片集合为{2}时,已经将5从GH的卡片集合中删除了?不,我们并没有主动删除,而是通过传播:当GH的卡片集合被更新为{2}时,我们遍历GH原来的卡片集合({2,5})中除2以外的卡片(即5),然后从卡片5的名字集合中删除GH。所以,在第四张照片后,处理GH(名字队列)时,我们更新了GH的卡片集合为{2},然后遍历了GH原来的卡片集合{2,5}(在更新前保存的old_set)中除2以外的卡片5,然后从卡片5的名字集合中删除GH。这样,卡片5的名字集合就变成了{IJ}(因为原来卡片5的集合是{GH,IJ},删除GH后为{IJ}),所以卡片5的名字集合变为1,加入卡片队列。
然后,处理卡片队列中的卡片5:它对应IJ,然后更新名字IJ:将IJ的卡片集合设为{5}(原来已经是{5},因为之前从{2,5}中删除了2,变为{5})。然后,遍历IJ原来的卡片集合({5})中除5以外的卡片(没有),所以不做。然后加入名字队列(IJ),但IJ的集合已经是{5},所以处理时跳过。
最后,第五张照片:UV, XY -> {7,8}
这两个名字第一次出现,卡片7,8也是第一次出现。
所以,UV: {7,8}; XY: {7,8}; 卡片7: {UV,XY}; 卡片8: {UV,XY},无法确定。
所以,输出:
卡片1: 没有在照片中出现过?但是第二张照片有卡片1?不对,第二张照片的卡片是1,2,5。所以卡片1在第二张照片中出现过,初始名字集合为{EF,GH,IJ}。但是,在后续照片中,卡片1没有出现过,所以没有更新。然后,在传播过程中,也没有更新到卡片1?因为卡片1的名字集合一直是{EF,GH,IJ},大小为3,没有变为1。所以输出'?'。但是,我们看EF:第二张照片后,EF的卡片集合为{1,2,5},然后第三张照片没有EF,所以没有更新。第四张照片也没有EF,第五张也没有。而且,在传播过程中,卡片1没有更新(因为它的名字集合大小没有变为1),所以EF的卡片集合也没有被更新(因为其他卡片没有占用1)。所以卡片1无法确定。
但是,题目输出中,卡片1对应的是EF?为什么?因为题目输出:
EF
GH
CD
KL
IJ
AB
?
?
所以卡片1对应EF,卡片2对应GH,卡片3对应CD,卡片4对应KL,卡片5对应IJ,卡片6对应AB,卡片7和8无法确定。
为什么卡片1能确定?因为传播过程中,我们通过其他约束可以推出。
我们重新看:第二张照片后,卡片1: {EF,GH,IJ},卡片2: {EF,GH,IJ},卡片5: {EF,GH,IJ}。
第三张照片:卡片2: {GH,IJ},卡片5: {GH,IJ},卡片6: {AB}(确定)。
第四张照片:卡片2: {GH}(确定),卡片3: {CD}(确定),卡片4: {KL,GH,CD} -> 但是GH和CD已经确定,所以KL只能对应4?不对,第四张照片中卡片4的名字集合为{KL,GH,CD},但是GH和CD已经被确定,所以卡片4只能对应KL?但是我们的算法中,在第四张照片后,我们通过交集更新卡片4为{KL,GH,CD},然后通过传播:当GH和CD被确定后,我们会从卡片4的名字集合中删除GH和CD(因为GH已经确定对应2,CD对应3,所以它们不能出现在其他卡片的名字集合中)。所以,在传播过程中,我们会将GH和CD从卡片4的名字集合中删除,这样卡片4的名字集合就只剩下{KL},所以卡片4确定为KL,然后KL的卡片集合也会被更新为{4}。
然后,卡片2被确定为GH后,我们从卡片2的旧名字集合{EF,GH,IJ}中删除GH(但此时卡片2的名字集合已经更新为{GH},然后我们遍历旧集合{EF,GH,IJ},删除GH以外的名字(EF和IJ)在卡片2中的记录,即从EF和IJ的卡片集合中删除2。这样,EF的卡片集合就从{1,2,5}中删除2,变为{1,5};IJ的卡片集合从{2,5}中删除2,变为{5}(变为5,所以IJ确定为5)。然后,IJ确定为5后,更新卡片5:将卡片5的名字集合设为{IJ},然后遍历卡片5的旧集合{EF,GH,IJ}(在第二张照片后)中除IJ以外的名字(EF和GH),从EF和GH的卡片集合中删除5。GH的卡片集合已经是{2},所以删除5没有影响。EF的卡片集合为{1,5},删除5后变为{1},所以EF确定为1。
所以,最终卡片1确定为EF。
但是,我们的算法中,这个传播过程是如何实现的?
在第四张照片后,我们处理了GH和CD(名字队列)以及卡片2和卡片3(卡片队列),然后更新了GH和CD的对应关系,同时删除了其他名字在卡片2和3中的记录(从而影响了EF和IJ的卡片集合,导致IJ变为1,然后IJ加入名字队列,然后处理IJ,更新卡片5,然后删除EF和GH在卡片5中的记录,导致EF的卡片集合变为{1},然后EF加入名字队列,然后处理EF,更新卡片1,然后删除其他名字在卡片1中的记录(GH和IJ)?然后GH和IJ的卡片集合中删除1?但是GH的卡片集合已经是{2},所以删除1没有影响;IJ的卡片集合已经是{5},删除1也没有影响。
所以,我们需要在传播过程中,当更新一个名字(或卡片)时,要更新它原来关联的所有卡片(或名字)的集合,删除占用的那个卡片(或名字)在其他地方的记录。
我们上面的代码已经实现了这个传播。
因此,我们按照上面的代码实现。
58. 但是,上面的代码中,在更新一个名字s的卡片集合为{c}时,我们遍历了s原来的卡片集合(old_set)中除c以外的每个卡片d,然后从卡片d的名字集合中删除s。同时,在更新一个卡片c的名字集合为{s}时,我们遍历了c原来的名字集合(old_set)中除s以外的每个名字t,然后从名字t的卡片集合中删除c。
59. 这样,当一个名字被确定对应一个卡片时,它会从其他卡片的名字集合中删除,从而可能使其他卡片的集合大小变为1(然后加入队列)。同样,当一个卡片被确定对应一个名字时,它会从其他名字的卡片集合中删除,从而可能使其他名字的集合大小变为1。
60. 所以,我们的代码应该能处理样例。
61. 但是,注意:在更新名字s的卡片集合为{c}时,我们只更新了s的集合,然后删除了其他卡片d中关于s的记录。但是,我们没有更新卡片c的名字集合?因为卡片c的名字集合可能还包含其他名字(比如t),但是我们在更新名字s的卡片集合为{c}时,并不直接更新卡片c的名字集合,而是通过传播:当名字s的卡片集合变为{c}后,我们会将卡片c加入卡片队列(因为名字s的更新事件会触发卡片c的更新事件?不,我们并没有在名字s的更新中直接更新卡片c的名字集合,而是通过传播:在名字s的更新中,我们更新了卡片c的名字集合为{s}(在名字队列的处理中,我们更新了卡片c的名字集合为{s},然后才传播的)?是的,在名字队列的处理中,我们更新了卡片c的名字集合为{s}(然后才传播的)。
具体在名字队列的处理中:
string s = name_queue.front();
int c = *name_to_cards[s].begin();
// 更新卡片c的名字集合为{s}(覆盖)
set<string> old_set = card_to_names[c]; // 保存旧的集合
card_to_names[c] = set<string>{s}; // 更新为{s}
// 然后遍历old_set中除s以外的名字t,从t的卡片集合中删除c
所以,我们更新了卡片c的名字集合。
62. 因此,代码可以处理。
63. 但是,注意:在更新名字s的卡片集合为{c}时,我们并没有立即更新其他卡片(比如原来s对应的卡片d)的名字集合(删除s),而是等到处理名字队列时,通过保存的old_set来删除。所以,传播是及时的。
64. 最后,我们输出卡片1到M的对应名字。
65. 注意:在传播过程中,可能有些卡片的名字集合变为空?但是题目保证无冲突,所以不会。
66. 代码实现中,注意头文件:
#include <iostream>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <algorithm>
#include <cctype>
#include <iterator>
67. 另外,set_intersection的使用需要包含<algorithm>和<iterator>(因为用了inserter)。
68. 但是,在取交集时,如果原集合为空,那么set_intersection不会插入任何元素?所以,在更新名字s时,如果s还没有初始化,我们就直接赋值为cardSet,否则取交集。卡片同理。
69. 但是,在名字s的更新中,如果交集为空,那么name_to_cards[s]就变为空集,然后后续传播中,如果遇到这个空集,我们不会加入队列(因为要求大小为1),所以不会传播,但是最后输出时,如果卡片c的名字集合为空,我们就输出'?'。但是题目保证无冲突,所以不会出现空集。
70. 所以,我们按照上述代码实现。
71. 但是,由于代码较长,且M和N较小,我们可以通过。
72. 注意:在更新名字s的卡片集合时,我们覆盖了原来的集合,所以保存old_set时,我们在更新之前保存。
73. 在传播过程中,我们更新了名字s的卡片集合(覆盖)和卡片c的名字集合(覆盖),然后删除了其他相关集合中的记录。
74. 最后,我们输出时,对于卡片c,如果它的名字集合大小为1,则输出名字;否则输出'?'。
75. 注意:在传播过程中,一个名字可能被删除(即它的卡片集合变为空),那么它就不会出现在任何卡片的名字集合中,所以不会影响输出。但是,题目保证无冲突,所以不会出现空集。
76. 编写代码,注意细节。
77. 由于题目要求代码无注释,且思考过程简短,所以这里我们给出代码,但思考过程已经详细。
78. 但是,由于代码较长,我们写完后测试样例。
79. 如果时间或内存超限?M<=54,N<=100,集合大小最大为54,迭代次数最多为54,所以不会超时。
80. 开始写代码。
注意:在读取照片时,卡片编号是整数,名字是字符串。
另外,在更新名字的卡片集合时,我们使用set_intersection,注意两个集合都是set,所以有序。
但是,在传播过程中,我们更新名字s的卡片集合为{c}时,我们直接构造了一个只包含c的set。同样,更新卡片c的名字集合为{s}时,构造了只包含s的set。
最后,输出时,按卡片编号1到M输出。
但是,注意:在传播过程中,我们可能没有处理所有卡片(比如卡片1在传播过程中被更新了,但是我们在输出时,卡片1的名字集合大小为1,所以输出EF)。
因此,代码实现如下:</think>为了解决这个问题,我们需要根据给定的照片信息推断出每个卡片编号对应的朋友名字。照片信息提供了每张照片中出现的朋友名字和对应的卡片编号,但没有给出具体的对应关系。我们需要利用这些信息逐步缩小每个名字可能对应的卡片范围,并通过传播更新来确定唯一的对应关系,无法确定的卡片则输出'?'。
### 方法思路
1. **初始化数据结构**:使用两个映射结构:
- `name_to_cards`:记录每个名字可能对应的卡片集合。
- `card_to_names`:记录每个卡片可能对应的名字集合。
2. **处理照片信息**:对于每张照片,更新名字和卡片的可能集合:
- 对于照片中的每个名字,将其可能的卡片集合与当前照片的卡片集合取交集。
- 对于照片中的每个卡片,将其可能的名字集合与当前照片的名字集合取交集。
3. **传播更新**:使用队列处理确定的对应关系:
- 如果某个名字的卡片集合大小为1,将其加入队列,并更新对应的卡片名字集合为这个名字,同时删除其他名字中该卡片的记录。
- 如果某个卡片的名字集合大小为1,将其加入队列,并更新对应的名字卡片集合为这个卡片,同时删除其他卡片中该名字的记录。
- 重复上述过程直到队列为空,确保所有可能的更新都被处理。
4. **输出结果**:对于每个卡片编号,如果其名字集合大小为1,输出对应的名字;否则输出'?'。
### 解决代码
```cpp
#include <iostream>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <algorithm>
#include <iterator>
using namespace std;
int main() {
int M, N;
cin >> M >> N;
vector<set<string>> card_to_names(M + 1);
map<string, set<int>> name_to_cards;
for (int i = 0; i < N; i++) {
int k;
cin >> k;
vector<string> names(k);
for (int j = 0; j < k; j++) {
cin >> names[j];
}
vector<int> cards(k);
for (int j = 0; j < k; j++) {
cin >> cards[j];
}
set<string> nameSet(names.begin(), names.end());
set<int> cardSet(cards.begin(), cards.end());
for (string name : names) {
if (name_to_cards.find(name) == name_to_cards.end()) {
name_to_cards[name] = cardSet;
} else {
set<int> newSet;
set_intersection(name_to_cards[name].begin(), name_to_cards[name].end(),
cardSet.begin(), cardSet.end(),
inserter(newSet, newSet.begin()));
name_to_cards[name] = newSet;
}
}
for (int card : cards) {
if (card_to_names[card].empty()) {
card_to_names[card] = nameSet;
} else {
set<string> newSet;
set_intersection(card_to_names[card].begin(), card_to_names[card].end(),
nameSet.begin(), nameSet.end(),
inserter(newSet, newSet.begin()));
card_to_names[card] = newSet;
}
}
}
queue<string> name_queue;
queue<int> card_queue;
for (auto& p : name_to_cards) {
if (p.second.size() == 1) {
name_queue.push(p.first);
}
}
for (int c = 1; c <= M; c++) {
if (card_to_names[c].size() == 1) {
card_queue.push(c);
}
}
while (!name_queue.empty() || !card_queue.empty()) {
while (!name_queue.empty()) {
string s = name_queue.front();
name_queue.pop();
if (name_to_cards[s].size() != 1) {
continue;
}
int c = *name_to_cards[s].begin();
if (card_to_names[c].size() == 1 && *card_to_names[c].begin() == s) {
continue;
}
set<string> old_set = card_to_names[c];
card_to_names[c] = set<string>{s};
for (string t : old_set) {
if (t == s) continue;
if (name_to_cards.find(t) != name_to_cards.end()) {
if (name_to_cards[t].find(c) != name_to_cards[t].end()) {
name_to_cards[t].erase(c);
if (name_to_cards[t].size() == 1) {
name_queue.push(t);
}
}
}
}
card_queue.push(c);
}
while (!card_queue.empty()) {
int c = card_queue.front();
card_queue.pop();
if (card_to_names[c].size() != 1) {
continue;
}
string s = *card_to_names[c].begin();
if (name_to_cards[s].size() == 1 && *name_to_cards[s].begin() == c) {
continue;
}
set<int> old_set = name_to_cards[s];
name_to_cards[s] = set<int>{c};
for (int d : old_set) {
if (d == c) continue;
if (d >= 1 && d <= M) {
if (card_to_names[d].find(s) != card_to_names[d].end()) {
card_to_names[d].erase(s);
if (card_to_names[d].size() == 1) {
card_queue.push(d);
}
}
}
}
name_queue.push(s);
}
}
for (int c = 1; c <= M; c++) {
if (card_to_names[c].size() == 1) {
cout << *card_to_names[c].begin() << endl;
} else {
cout << "?" << endl;
}
}
return 0;
}
```
### 代码解释
1. **初始化数据结构**:`card_to_names`是一个向量,索引从1到M,每个元素是一个字符串集合,表示该卡片可能对应的名字。`name_to_cards`是一个映射,键为名字,值为整数集合,表示该名字可能对应的卡片编号。
2. **处理照片信息**:读取每张照片中的名字和卡片编号,更新`name_to_cards`和`card_to_names`。对于每个名字,将其可能的卡片集合与当前照片的卡片集合取交集;对于每个卡片,将其可能的名字集合与当前照片的名字集合取交集。
3. **传播更新**:使用两个队列分别处理名字和卡片。如果某个名字的卡片集合大小为1,将其加入队列,并更新对应的卡片名字集合,同时删除其他名字中该卡片的记录。同样,如果某个卡片的名字集合大小为1,将其加入队列,并更新对应的名字卡片集合,同时删除其他卡片中该名字的记录。重复此过程直到队列为空。
4. **输出结果**:遍历每个卡片编号,如果其名字集合大小为1,输出对应的名字;否则输出'?'。
### 相关问题
1. 如何进一步优化传播更新的效率?
2. 如果存在冲突的结论,如何检测并处理?
3. 如何扩展此方法处理更大的数据集(如M和N更大)?
4. 是否有其他数据结构或算法可以更高效地解决此问题?