1016 Numbers That Count 模拟

本文介绍了一种通过压缩数字串并对其进行分类的方法。利用C++实现了一个程序,该程序能够统计数字串中每个数字出现的次数,并将这些信息压缩成更短的形式。随后,根据数字串是否能自我压缩成相同形式、压缩几步后可以达到自我压缩状态或进入循环状态等条件进行分类。
#include <iostream>
#include <cstring>
using namespace std;


/*压缩数字串n,存放到t*/
void R(char* n,char* t)
{
    int i,j;
    int time[10]={0};  //记录n中各个数字出现的次数
    for (int i=0;n[i];i++)
        time[ n[i]-'0' ]++;


    for (i=0,j=0; i<10; i++)
    {
        if (time[i])
        {
            if (time[i]<10)  //数字i出现次数<10,即占1位
            {
                t[j++]=time[i]+'0';
                t[j++]=i+'0';
            }
            else    //数字i出现次数>=10,即占2位
            {
                t[j++]=time[i]/10+'0';
                t[j++]=time[i]%10+'0';
                t[j++]=i+'0';
            }
        }
    }
    t[j]='\0';


    return;
}


int main()
{
    char n[16][85];    //n[0]为原串,n[1~15]分别为n连续压缩15次的数字串


    while (cin>>n[0] && n[0][0]!='-')
    {
        bool flag1=false;    //属性1,n is self-inventorying
        int flag2=0;         //属性2,n is self-inventorying after j steps,顺便记录j
        int flag3=0;         //属性3,n is enters an inventory loop of length k,顺便记录k


        for (int i=1; i<=15; i++)
            R(n[i-1],n[i]);


        if (!strcmp(n[0],n[1]))  //属性1,n压缩1次就是其本身
            flag1=true;


        if (!flag1)
        {
            for (int j=1; j<15; j++)
                if (!strcmp(n[j],n[j+1]))  //属性2, n压缩j次后的数字串n[j]具有属性1
                {
                    flag2=j;
                    break;
                }


            if (!flag2)
            {
                for (int j=1; j<=15; j++)  //属性3,两两枚举各次压缩的数字串,注意循环间隔>=2
                {
                    for (int i=0;i<=j-2;i++)
                    {
                        if (!strcmp(n[j],n[i]))
                        {
                            flag3=j-i;
                            break;
                        }
                    }
                    if(flag3)
                        break;
                }
            }
        }


        if (flag1)
            cout<<n[0]<<" is self-inventorying"<<endl;
        else if (flag2)
            cout<<n[0]<<" is self-inventorying after "<<flag2<<" steps"<<endl;
        else if (flag3)
            cout<<n[0]<<" enters an inventory loop of length "<<flag3<<endl;
        else
            cout<<n[0]<<" can not be classified after 15 iterations"<<endl;
    }
    return 0;
}
请用c++回答,代码无注释,思考过程简短 7-3 Memories of Youth 分数 35 作者 陈越 单位 浙江大学 mem.jpg There was a group of 54 youths who were very good friends. Each one of them picked a card from a deck of cards to commemorate their friendship. But 54 years later, they cannot remember who has picked which card… Fortunately they have taken a lot of pictures at the time, and the card numbers have been recorded on the back of some pictures for some of them. Hopefully these clues are sufficient to recover the card numbers for everyone, even thought the correspondence between the person and the card was not recorded. Now your job is to help these friends recover the memories of their youth. Input Specification: Each input file contains one test case. For each case, the first line contains 2 positive integers M (≤54) and N (≤100), which are the number of friends (hence it is assumed that each of them corresponds to a distinct card number in the range [1, M]) and the number of pictures, respectively. Then N pictures are given. The information of each picture occupies 3 lines. The 1st line contains a positive integer k (<M) which is the number of persons in that picture. The 2nd line gives k distinct codes, each consists of 2 English capital letters, representing the names of the persons in that pictures. Their card numbers (k distinct integers in [1, M]) are given in the 3rd line. All the numbers in a line are separated by a space. Output Specification: For each test case, print the name code for each card in ascending order of the card numbers from 1 to M. Each code occupies a line. In case that it is impossible to determine the person who corresponds to a certain card, print ? in the line instead. It is guarenteed that there will not be any conflict conclusions. There might be cases such as persons A and B correspond to cards n 1 ​ and n 2 ​ , so without more information we cannot determine who corresponds to which card; yet there is no case so that a single card corresponds to more than one person. Sample Input: 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 Sample Output: EF GH CD KL IJ AB ? ? 代码长度限制 16 KB Java (javac) 时间限制 800 ms 内存限制 64 MB 其他编译器 时间限制 400 ms 内存限制 64 MB 栈限制 8192 KB
最新发布
08-14
<think>题目要求:根据给定的照片信息,推断出每个卡片(编号从1到M)对应的人(用两个大写字母表示)。如果无法确定,则输出&#39;?&#39;。 思路: 1. 我们可以使用图论的方法。构建一个二分图,左侧是人的名字(如"AB", "CD"等),右侧是卡片编号(1到M)。 2. 每张照片提供的信息:照片中有k个人,以及k个名字和k个卡片编号(但是不知道名字和卡片的对应关系)。 因此,这张照片告诉我们:这k个名字和这k个卡片编号之间存在一个完全匹配,但具体匹配关系未知。 3. 因此,对于一张照片,我们可以得到:这k个名字中的每个名字都只能与这k个卡片编号中的某一个匹配(不能匹配到其他编号), 同样,这k个卡片编号中的每个编号都只能匹配这k个名字中的某一个(不能匹配到其他名字)。 4. 我们可以将问题转化为:建立名字和卡片之间的可能连接(边),然后通过不断排除不可能的情况(即删边)来得到确定的匹配。 具体地,对于每张照片,我们可以在对应的名字集合和卡片集合之间建立一个完全二分图(即名字集合中的每个名字和卡片集合中的每个卡片都连一条边)。 但是,这样会建立很多边。然后,我们通过传递的信息(约束)来删除不可能的边。 5. 然而,题目保证没有冲突结论,但可能无法完全确定。我们需要得到每个卡片对应的名字(如果能够确定的话),否则输出&#39;?&#39;。 6. 另一种思路:我们记录每个名字可能对应的卡片集合,以及每个卡片可能对应的名字集合。 初始时,每个名字可以对应1~M的任意卡片,每个卡片可以对应任意名字(但名字是有限的,即输入中出现的名字,以及注意总共有M个名字,但题目没有说名字就是M个?注意:题目说M是朋友的数量,所以总共有M个人,也就是M个名字。但是输入中给出的名字是2个字母的代码,并且有M个不同的代码?注意:输入的第一行M是朋友数量,所以有M个名字,每个名字是2个字母的代码,且题目保证名字是2个英文大写字母,并且是k个不同的名字。 7. 但是注意:输入中给出的名字不一定包含所有M个名字!因为每张照片只包含一部分人。所以我们需要先收集所有出现过的名字?然而,输出要求从卡片1到M,输出对应的名字(如果无法确定则输出&#39;?&#39;)。所以我们必须知道所有名字?但是题目没有给出所有名字的列表,怎么办? 8. 重新读题:第一行给出M和N,M是朋友数量(即总人数),N是照片数量。然后每张照片给出k个人(名字)和k个卡片编号(但是不给出对应关系)。注意:题目说“每个朋友对应一个卡片编号(1到M)”,但是输入中并没有给出所有M个人的名字。所以我们需要通过照片信息来收集所有出现过的名字?但是可能有些名字从未出现,那么这些卡片对应的人就永远无法确定?所以输出时,对于没有出现过的名字对应的卡片,我们只能输出&#39;?&#39;。 9. 但是题目说“保证没有冲突结论”,并且“可能无法确定”。所以我们需要处理的是:对于每个卡片,如果我们能确定它对应哪个名字,就输出名字;否则输出&#39;?&#39;。 10. 关键:我们不知道所有名字的集合。所以我们需要先收集所有出现过的名字。但是,注意:总共有M个人,但照片中可能没有出现所有人。那么没有出现的人的名字我们不知道,所以他们的卡片编号对应的人就无法确定?但是题目要求输出从卡片1到M的对应名字,所以我们必须知道每个卡片对应的人的名字,但如果没有出现过,那么我们就无法确定,输出&#39;?&#39;。 11. 然而,题目没有给出所有名字的列表,所以我们只能通过照片来收集名字。但是,有可能某个人没有在任何照片中出现,那么我们就无法知道他的名字(因为输入没有给出)。这怎么办?注意:题目说“each of them corresponds to a distinct card number”,所以有M个人,但是输入中并没有给出这M个人的名字列表。我们只能通过照片信息来获取名字。 12. 因此,我们需要:记录所有出现过的名字。但是,如果一个人没有在任何照片中出现,那么我们就不知道他的名字。那么输出时,对于卡片编号,如果这个卡片编号在照片中出现过(即某张照片的卡片编号列表中有这个编号),那么我们就需要推断出它对应哪个名字;如果这个卡片编号从未在照片中出现,那么显然我们无法确定(因为连约束条件都没有),所以输出&#39;?&#39;。但是,这里有一个问题:即使一个卡片编号在照片中出现过,我们也不一定能唯一确定名字;同样,一个名字在照片中出现过,我们也不一定能确定它对应的卡片编号。 13. 所以,我们可能需要建立两个映射: name_to_possible_cards: 每个名字可能对应的卡片集合(初始为全集,然后根据照片缩小范围) card_to_possible_names: 每个卡片可能对应的名字集合(初始为全集,然后根据照片缩小范围) 14. 但是,初始全集有多大?名字的全集我们并不知道(因为输入没有给出所有名字),卡片是1~M。 15. 因此,我们需要先收集所有出现过的名字。我们可以遍历所有照片,记录所有出现过的名字。但是,注意:总共有M个人,所以名字应该有M个。但是,如果照片中只出现了部分名字,那么剩下的名字我们不知道,也无法建立映射?所以,我们只能对出现过的名字建立映射,没有出现的名字我们暂时不管。 16. 但是,输出要求:按卡片编号1到M输出对应的名字。如果某个卡片编号对应的人(名字)没有在照片中出现过,那么我们无法知道他的名字(因为输入没有给出),所以只能输出&#39;?&#39;。但是,我们如何知道某个卡片编号对应的人的名字呢?如果没有出现过,我们连名字是什么都不知道,所以输出&#39;?&#39;是合理的。 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() 否则,输出&#39;?&#39; 32. 但是,注意:有些卡片可能从未在照片中出现过,那么card_to_names[c]是空集(因为我们没有初始化它)。所以,对于没有在照片中出现的卡片,我们直接输出&#39;?&#39;。 33. 问题:在读取照片时,我们只更新了照片中出现的卡片。对于没有出现的卡片,card_to_names[c]是空集(或者我们初始化时没有设置?我们初始化card_to_names为大小为M+1的vector,每个元素初始为空集)。所以,对于没有在照片中出现的卡片,我们输出&#39;?&#39;。 34. 但是,还有一种情况:卡片在照片中出现了,但是经过传播后,它的可能名字集合大小大于1(无法确定),那么输出&#39;?&#39;。 35. 然而,我们还需要考虑名字:有些名字可能没有出现在任何照片中,那么这些名字对应的卡片编号我们无法确定?但是,题目要求输出卡片编号1到M对应的名字。所以,我们只关心卡片编号对应的名字,而不需要知道所有名字。 36. 但是,在传播过程中,我们只处理了在照片中出现过的名字和卡片。对于没有出现过的卡片,我们不会加入队列,也不会更新,所以最后card_to_names[c]为空,输出&#39;?&#39;。 37. 但是,有一个问题:一个名字在照片中出现过,但是对应的卡片编号可能被传播为唯一,那么即使这个卡片编号在之前的照片中没有直接出现,我们也能确定?不,我们每张照片只更新了照片中出现的卡片和名字。所以,如果一张照片中出现了名字s,那么s的可能卡片集合初始化为该照片的卡片集合,然后后续照片会取交集。但是,如果s没有在后面的照片中出现,那么它的集合就不会再更新。然后,通过传播,我们可能能确定s对应的卡片。 38. 所以,我们只需要按照上述步骤处理即可。 39. 但是,在初始化时,我们只处理了照片中出现的名字和卡片。对于没有出现过的卡片,我们不会初始化card_to_names[c](即它保持为空集)。所以,最后输出时,如果card_to_names[c]为空,那么输出&#39;?&#39;。但是,如果卡片c在照片中出现过,那么我们在读取照片时已经初始化了card_to_names[c](初始化为该照片的名字集合,然后后续照片取交集),然后传播后可能变为1个名字,也可能还是多个(则输出&#39;?&#39;)。 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]为空(因为我们没有在照片中更新它),所以输出&#39;?&#39;。 54. 但是,注意:在传播过程中,一个卡片即使没有在照片中出现,但是通过传播(从其他名字和卡片传播)会更新吗?不会,因为我们只更新了在照片中出现过的卡片(在读取照片时初始化了),没有出现的卡片我们不会初始化,也不会在传播过程中被更新(因为传播的起点是照片中出现过的名字和卡片)。所以,没有出现过的卡片,card_to_names[c]一直为空,输出&#39;?&#39;。 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。所以输出&#39;?&#39;。但是,我们看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的名字集合为空,我们就输出&#39;?&#39;。但是题目保证无冲突,所以不会出现空集。 70. 所以,我们按照上述代码实现。 71. 但是,由于代码较长,且M和N较小,我们可以通过。 72. 注意:在更新名字s的卡片集合时,我们覆盖了原来的集合,所以保存old_set时,我们在更新之前保存。 73. 在传播过程中,我们更新了名字s的卡片集合(覆盖)和卡片c的名字集合(覆盖),然后删除了其他相关集合中的记录。 74. 最后,我们输出时,对于卡片c,如果它的名字集合大小为1,则输出名字;否则输出&#39;?&#39;。 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>为了解决这个问题,我们需要根据给定的照片信息推断出每个卡片编号对应的朋友名字。照片信息提供了每张照片中出现的朋友名字和对应的卡片编号,但没有给出具体的对应关系。我们需要利用这些信息逐步缩小每个名字可能对应的卡片范围,并通过传播更新来确定唯一的对应关系,无法确定的卡片则输出&#39;?&#39;。 ### 方法思路 1. **初始化数据结构**:使用两个映射结构: - `name_to_cards`:记录每个名字可能对应的卡片集合。 - `card_to_names`:记录每个卡片可能对应的名字集合。 2. **处理照片信息**:对于每张照片,更新名字和卡片的可能集合: - 对于照片中的每个名字,将其可能的卡片集合与当前照片的卡片集合取交集。 - 对于照片中的每个卡片,将其可能的名字集合与当前照片的名字集合取交集。 3. **传播更新**:使用队列处理确定的对应关系: - 如果某个名字的卡片集合大小为1,将其加入队列,并更新对应的卡片名字集合为这个名字,同时删除其他名字中该卡片的记录。 - 如果某个卡片的名字集合大小为1,将其加入队列,并更新对应的名字卡片集合为这个卡片,同时删除其他卡片中该名字的记录。 - 重复上述过程直到队列为空,确保所有可能的更新都被处理。 4. **输出结果**:对于每个卡片编号,如果其名字集合大小为1,输出对应的名字;否则输出&#39;?&#39;。 ### 解决代码 ```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,输出对应的名字;否则输出&#39;?&#39;。 ### 相关问题 1. 如何进一步优化传播更新的效率? 2. 如果存在冲突的结论,如何检测并处理? 3. 如何扩展此方法处理更大的数据集(如M和N更大)? 4. 是否有其他数据结构或算法可以更高效地解决此问题?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值