字典计数法和集合公式法

题目考点

这道题主要考的是这几个点:

  1. 输入处理
    第一行读入每组人数 K,第二行读入一串房间号。
  2. 列表遍历
    需要把所有房间号一个个看过去。
  3. 统计次数
    题目的核心其实是:找出那个只出现 1 次的房间号
  4. 字典或集合思想
    最朴素的方法是用字典统计每个房间号出现了几次。

审题

我们先把题目真正说的事情翻译成人话。

酒店里来了很多游客:

  • 有一个队长,单独住一个房间,所以这个房间号只出现 1 次
  • 其他人都是家庭出行,每个家庭有 K 个人,所以同一个家庭的房间号会出现 K 次

现在给你:

  • 一个整数 K
  • 一串乱序的房间号列表

要求你输出:

  • 队长的房间号

输入是什么

第一行:

K

表示每个家庭有多少人。

第二行:

房间号1 房间号2 房间号3 ...

是一整串房间号。

输出是什么

输出那个只出现一次的房间号。

题目真正要我们判断什么

本质就是一句话:

谁的出现次数是 1?

容易忽略的点

这里有一个很重要的审题点:

题目没有给你“有几个家庭”,也没有给你“有多少游客”,但这并不影响做题。
因为我们只需要把第二行所有房间号读进来,然后统计出现次数即可。

也就是说,这题的重点不是总人数,而是:

房间号出现的规律:别人出现 K 次,队长出现 1 次。


思路提示

先不要急着写代码,先想清楚方向。

你可以这样想:

第一步:把所有房间号读进来

比如样例里会得到一个列表,里面很多数字重复出现。

第二步:统计每个房间号出现了多少次

例如:

  • 1 出现了 5 次
  • 2 出现了 5 次
  • 3 出现了 5 次
  • 8 出现了 1 次

第三步:找出出现次数为 1 的那个房间号

这就是答案。

所以,这题最直接的思路就是:

先统计,再找唯一。


完整设计思路

这题对初学者最稳的做法,就是用字典计数。

第 1 步:读入 K

k = int(input())

这里只是把每个家庭的人数读进来。

第 2 步:读入第二行房间号

rooms = list(map(int, input().split()))

这句你可以这样理解:

  • input():读入整行字符串
  • split():按空格切开
  • map(int, ...):把每一项从字符串变成整数
  • list(...):最终变成一个整数列表

第 3 步:用字典统计次数

准备一个空字典:

count = {}

然后遍历每个房间号:

  • 如果这个房间号之前没出现过,就先记成 1
  • 如果已经出现过,就在原来的次数上加 1

第 4 步:再遍历字典,找次数等于 1 的房间号

谁的次数是 1,谁就是队长房间。


代码实现

下面给你一个最朴素、最适合初学者理解的版本。

if __name__ == '__main__':
    k = int(input())
    rooms = list(map(int, input().split()))

    count = {}

    for room in rooms:
        if room in count:
            count[room] += 1
        else:
            count[room] = 1

    for room in count:
        if count[room] == 1:
            print(room)
            break

运行演示

我们用题目里的样例来模拟一下:

K = 5
rooms = [1, 2, 3, 6, 5, 4, 4, 2, 5, 3, 6, 1, 6, 5, 3, 2, 4, 1, 2, 5, 1, 4, 3, 6, 8, 4, 3, 1, 5, 6, 2]

统计过程大意

程序会不断更新字典:

  • 看到 1,记为 1: 1
  • 看到 2,记为 2: 1
  • 看到 3,记为 3: 1
  • 后面重复遇到,就不断加 1

最终统计结果会类似于:

{
    1: 5,
    2: 5,
    3: 5,
    4: 5,
    5: 5,
    6: 5,
    8: 1
}

这时一看就知道:

  • 8 出现了 1 次
  • 所以答案就是 8

输出:

8

另一种更“题目风格”的做法

这道题其实常常被放在 set 这一章,所以还有一种更巧的写法。

为什么可以用集合

集合 set(rooms) 会自动去重。

比如原列表里:

[1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 8]

去重后变成:

{1, 2, 8}

如果把去重后的和乘以 K,再减去原列表总和,就能把那些重复的家庭房间抵消掉,最后只剩队长房间。

公式是:

( sum(set(rooms)) * k - sum(rooms) ) // (k - 1)

代码写法

if __name__ == '__main__':
    k = int(input())
    rooms = list(map(int, input().split()))

    answer = (sum(set(rooms)) * k - sum(rooms)) // (k - 1)
    print(answer)

这两种写法怎么选

对于初学者,我建议你先掌握第一种:

  • 字典计数法更直观
  • 更符合“审题 -> 统计 -> 找唯一”的思路
  • 以后遇到类似题也更容易迁移

第二种集合公式法可以当作进阶补充。


方法总结

这类题你以后可以这样识别:

题目特征

如果题目出现这种描述:

  • 大多数元素都重复固定次数
  • 只有一个元素出现次数不同
  • 让你找那个特殊值

你脑子里可以立刻想到两条路线:

路线 1:统计次数

最稳,最好理解。

步骤是:

  1. 读入数据
  2. 统计每个值出现次数
  3. 找特殊次数的那个值

路线 2:利用数学规律或集合规律

适合题目重复规律非常整齐时使用,比如这题里“其余都出现 K 次,只有一个出现 1 次”。


易错点回顾

1. 误以为还需要输入总人数

不需要。
因为第二行已经直接把所有房间号给你了。

2. 只想着排序,不想着“统计”

排序也能做,但这题最自然的想法其实是统计次数。

3. map(int, input().split()) 没转成列表就直接反复用

虽然这题里很多时候不转列表也能遍历一次,但初学阶段建议直接写:

rooms = list(map(int, input().split()))

更稳,也更容易理解。

4. 用集合后误以为能直接找到答案

集合只能去重,不能直接告诉你谁是只出现一次的。
必须结合总和规律,或者结合计数。


本节小结

这道题最核心的不是酒店故事,而是把它翻译成程序问题:

在一堆数字中,找出那个只出现一次的数字。

对初学者来说,最好先建立这样的做题习惯:

  • 先看输入输出
  • 再看数字之间的规律
  • 把故事题翻译成“统计题”

这才是这道题真正训练你的地方。


练习

下面给你一道同类型的小练习,先不要急着看答案,自己试着按今天的思路做。

题目

一个整数 K = 3,表示每个家庭有 3 个人。
现在给出房间号列表:

10 20 10 30 20 10 20

已知除了队长房间外,其余房间号都出现 3 次。
请找出队长房间号。

提示

先不要想公式,先用最稳的方法:

  1. 统计每个房间号出现了几次
  2. 找出出现次数为 1 的那个房间号

集合公式法到底为什么成立?

你前面没看懂,这很正常。
因为这个方法虽然代码短,但它背后的逻辑其实比“字典计数法”更绕一点。

所以这一节我们不急着看代码,先专门把这个公式拆开讲透:

(sum(set(rooms)) * k - sum(rooms)) // (k - 1)

你只要把这句真正看懂,以后再遇到这种“多数重复 K 次,只有一个特殊值只出现 1 次”的题,就会很顺。


先说结论:这个公式是在“消掉重复项”

这道题里有一个很特别的规律:

  • 普通家庭房间号,都会出现 K
  • 只有队长房间号,只出现 1

集合公式法的核心思路就是:

想办法把那些“重复 K 次”的房间号抵消掉,最后只剩队长房间号。

它不是在“数次数”,而是在“利用总和做抵消”。


第一步:先理解 set(rooms) 到底做了什么

假设有这样一组房间号:

rooms = [1, 1, 1, 2, 2, 2, 5]

这里假设:

  • K = 3
  • 房间 1 出现了 3 次
  • 房间 2 出现了 3 次
  • 房间 5 只出现了 1 次,它就是队长房间

原列表的和

sum(rooms) = 1+1+1+2+2+2+5 = 14

去重后的集合

set(rooms) = {1, 2, 5}

集合会自动去掉重复项,只保留每个房间号的一份。

所以:

sum(set(rooms)) = 1+2+5 = 8

第二步:为什么要乘以 K

这一步最关键。

我们把去重后的总和再乘以 K

sum(set(rooms)) * k = 8 * 3 = 24

也就是:

(1 + 2 + 5) * 3 = 1*3 + 2*3 + 5*3

变成:

3 + 6 + 15 = 24

你可以把它理解成:

  • 原本集合里每个房间号只有 1 份
  • 乘上 K 之后,相当于“人为把每个房间号都补成 K 份”

对于普通家庭房间号来说,这正好和原列表一样:

  • 房间 1 本来就是 3 次
  • 房间 2 本来就是 3 次

但是对于队长房间 5 来说:

  • 原列表里它只有 1 次
  • 乘上 K 后,相当于把它也当成了 3 次

这就多算了。


第三步:为什么再减去 sum(rooms)

现在我们来看:

sum(set(rooms)) * k - sum(rooms)

代入上面的数:

24 - 14 = 10

为什么会得到 10?

我们展开来看。

sum(set(rooms)) * k 里包含的内容

1*3 + 2*3 + 5*3

sum(rooms) 里包含的内容

1*3 + 2*3 + 5*1

两者相减:

(1*3 + 2*3 + 5*3) - (1*3 + 2*3 + 5*1)

普通家庭房间会被完全抵消:

1*3 - 1*3 = 0
2*3 - 2*3 = 0

只剩队长房间:

5*3 - 5*1 = 5*(3-1) = 10

所以最后得到的不是队长房间号本身,而是:

队长房间号 * (K - 1)

这里就是:

5 * (3 - 1) = 10

第四步:为什么最后还要除以 (K - 1)

因为上一步剩下的是:

队长房间号 * (K - 1)

所以只要再除以 (K - 1),就能把真正的队长房间号还原出来:

10 // 2 = 5

所以最终公式就是:

(sum(set(rooms)) * k - sum(rooms)) // (k - 1)

用代数方式再看一遍

如果你想把它彻底吃透,可以看这个“字母版”。

假设:

  • 普通家庭房间号有 a, b, c
  • 队长房间号是 x
  • 每个家庭人数是 K

那么原列表总和就是:

sum(rooms) = a*K + b*K + c*K + x

因为 x 只出现 1 次。

而去重后的集合总和是:

sum(set(rooms)) = a + b + c + x

再乘以 K

sum(set(rooms)) * K = a*K + b*K + c*K + x*K

现在两者相减:

(sum(set(rooms)) * K) - sum(rooms)
= (a*K + b*K + c*K + x*K) - (a*K + b*K + c*K + x)
= x*K - x
= x*(K - 1)

最后除以 (K - 1)

x*(K - 1) // (K - 1) = x

这就得到队长房间号了。


为什么这个方法成立的前提很严格

这个公式不是随便什么题都能用,它成立必须满足两个条件:

条件 1:除了一个特殊值,其他值都必须出现完全相同的 K

比如都出现 5 次,或者都出现 3 次。

条件 2:特殊值只出现 1 次

如果题目改成:

  • 有两个特殊值
  • 或者特殊值出现 2 次
  • 或者其他家庭出现次数不统一

那这个公式就不一定能用了。

所以你要记住:

集合公式法是“利用固定重复次数做抵消”的技巧题方法,不是通用统计法。


为什么字典计数法更好理解,而集合公式法更像技巧

这两种方法的本质区别是:

字典计数法

思路非常直接:

  • 谁出现几次,我就老老实实记下来
  • 最后找出现 1 次的

它更适合初学者,因为它是“看得见过程”的。

集合公式法

思路更像数学技巧:

  • 先去重
  • 再补成 K
  • 再减去原总和
  • 用抵消得到队长房间

它代码短,但不如字典法直观。

所以你的感觉完全正常:
这不是你不会,是这个方法本来就比字典法更绕。


代入题目样例看一次

题目样例中:

K = 5

房间号中:

  • 1, 2, 3, 4, 5, 6 都出现了 5 次
  • 8 只出现了 1 次

去重后的集合

{1, 2, 3, 4, 5, 6, 8}

它们的和是:

1 + 2 + 3 + 4 + 5 + 6 + 8 = 29

乘以 K = 5

29 * 5 = 145

原列表总和呢?

因为 1 到 6 各出现 5 次,8 出现 1 次:

(1+2+3+4+5+6)*5 + 8
= 21*5 + 8
= 105 + 8
= 113

相减:

145 - 113 = 32

这个 32 其实就是:

8 * (5 - 1) = 8 * 4 = 32

最后除以 4

32 // 4 = 8

答案就是 8


一句话记忆这个公式

你可以这样记:

去重后的和,相当于每个房间只留一份;乘 K,是把每个房间都补成 K 份;减去原列表后,普通家庭正好抵消,只剩队长多补出来的 K-1 份。

这句话有点长,但它就是这个公式的本质。


代码再看一遍,就不神秘了

if __name__ == '__main__':
    k = int(input())
    rooms = list(map(int, input().split()))
    answer = (sum(set(rooms)) * k - sum(rooms)) // (k - 1)
    print(answer)

现在你应该能把每一部分对上了:

  • set(rooms):去重
  • sum(set(rooms)):每种房间号只算一次
  • * k:假装每个房间都出现 K
  • - sum(rooms):把原来的真实总和减掉
  • // (k - 1):还原出队长房间号

你以后怎么判断能不能用这个公式

看到题目时,你先问自己三个问题:

1. 其他数是不是都重复固定的 K 次?

如果是,继续。

2. 是不是只有一个特殊数出现 1 次?

如果是,继续。

3. 我是不是只需要找这个唯一的特殊值?

如果是,那就可以考虑这个公式。

否则,老老实实用字典计数。


本节小结

集合公式法的本质不是“集合很神奇”,而是:

用去重后的总和和原总和做差,把重复项全部抵消掉。

最后只会剩下队长多补出来的那几份,所以再除以 (K - 1) 就可以了。

如果你现在已经能理解下面这句话,就说明你真的看懂了:

普通家庭在“补成 K 份”和“原本就是 K 份”之间会互相抵消,只有队长原本只有 1 份,所以会留下 (K - 1) 份。


练习

你可以自己手算这道小题,不要急着写代码。

题目

K = 3
rooms = [7, 3, 7, 7, 5, 3, 3]

请你用集合公式法算出队长房间号。

提示

按这个顺序手算:

  1. 先算 set(rooms)
  2. 再算 sum(set(rooms))
  3. 再乘以 K
  4. 再减去 sum(rooms)
  5. 最后除以 K - 1

补充说明:为什么这里能用 set,而不能直接用 list

这个问题非常关键。
因为如果这一点不弄明白,集合公式法就会一直像“硬背公式”。

这节我们专门讲清楚:

为什么公式里必须写 sum(set(rooms)),而不能直接写 sum(rooms),也不能把 list 放到那个位置替代掉。


先说结论

在这道题里:

  • list 保存的是原始完整数据
  • set 保存的是去重后的数据

而集合公式法之所以成立,核心前提就是:

我们必须先拿到“每个房间号只保留一份”的结果。

这个工作,set 能自动完成,list 不能。

所以不是说 list 完全不能用,而是说:

  • 原始房间号当然要先用 list 读进来
  • 但在公式的“去重”这一步,必须用 set

先区分:listset 到底有什么本质区别

list 的特点

列表会保留所有元素,包括重复项

例如:

rooms = [1, 1, 1, 2, 2, 2, 5]

这个列表里:

  • 1 会保留 3 次
  • 2 会保留 3 次
  • 5 保留 1 次

列表不会帮你去重。

set 的特点

集合只保留不重复的元素

set(rooms) = {1, 2, 5}

它自动把重复项去掉了。

这正是这道题最需要的性质。


这道题为什么一定需要“去重后的结果”

我们再回到题目的本质:

  • 普通家庭房间号都出现 K
  • 队长房间号只出现 1 次

集合公式法其实是在做这样一件事:

第一步:先让“每个房间号都只保留一份”

比如:

rooms = [1, 1, 1, 2, 2, 2, 5]

去重后得到:

{1, 2, 5}

这时候每个房间号都只有一份了。

第二步:再把这些“只保留一份”的房间号统一乘上 K

(1 + 2 + 5) * 3

这一步的意思其实是:

假装每个房间号都应该出现 3 次。

于是:

  • 房间 1 被补成 3 次
  • 房间 2 被补成 3 次
  • 房间 5 也被补成 3 次

然后再减去原始列表总和,普通家庭就抵消掉,最后只剩队长多补出来的部分。

注意这里最关键的一点:

如果你一开始没有“去重”,那你根本就不是在“每个房间只保留一份”的基础上乘 K,整个公式就失效了。


为什么不能直接用 list

这是最核心的问题。

假设我们错误地把公式里的 set(rooms) 换成 rooms 本身,也就是直接写成:

(sum(rooms) * k - sum(rooms)) // (k - 1)

我们看看会发生什么。


用具体例子看错误在哪里

还是用这个例子:

k = 3
rooms = [1, 1, 1, 2, 2, 2, 5]

正确做法

先去重:

set(rooms) = {1, 2, 5}
sum(set(rooms)) = 8

然后:

(8 * 3 - 14) // 2
= (24 - 14) // 2
= 10 // 2
= 5

得到正确答案 5


错误做法:把 set(rooms) 换成 rooms

因为:

sum(rooms) = 14

那么错误公式会变成:

(14 * 3 - 14) // 2
= (42 - 14) // 2
= 28 // 2
= 14

结果变成了 14,这根本不是队长房间号。


为什么会错得这么离谱

因为 list 里有重复项。

你写:

sum(rooms)

得到的是:

1+1+1+2+2+2+5

这是“真实总和”。

但集合公式法前半部分需要的是:

每个房间号只算一次之后的总和

也就是:

1+2+5

如果你不用 set 去重,而还是拿原列表去乘 K,本质上你是在把“已经重复过的数据”又重复放大了一次。

所以公式当然就坏了。


你可以把它理解成两种完全不同的数据

同样都是房间号,它们在这题里其实扮演了两种角色。

角色 1:原始完整名单

[1, 1, 1, 2, 2, 2, 5]

这个列表表示真实入住情况。
它告诉你:

  • 房间 1 有 3 个人
  • 房间 2 有 3 个人
  • 房间 5 有 1 个人

这就是原始数据。

所以:

sum(rooms)

表示的是“真实总和”。


角色 2:房间号种类名单

{1, 2, 5}

这个集合不是在表示“每个人住哪间房”,而是在表示:

一共有哪些不同的房间号。

所以:

sum(set(rooms))

表示的是“每种房间号只取一次后的总和”。

而集合公式法正是需要这种“每种只取一次”的数据。


为什么 list 不能自动完成“去重”这件事

列表本身的设计目标不是去重,而是按顺序保存所有元素。

例如:

rooms = [1, 1, 1, 2, 2, 2, 5]

列表保留的是:

  • 元素顺序
  • 重复次数
  • 完整原始记录

而集合 set 的设计目标正好相反:

  • 不关心顺序
  • 自动去重
  • 只关心“有哪些不同元素”

所以这题里:

  • 你需要完整数据时,用 list
  • 你需要“每个房间号只留一份”时,用 set

可以这么记:list 适合“记录全部”,set 适合“保留种类”

这句话很适合初学阶段记忆。

list

适合保存所有数据,不会丢掉重复项。

set

适合提取“不重复元素”。

这道题的公式法恰好同时需要两种信息:

  1. 原始完整数据的总和
    sum(rooms)
  2. 去重后每种房间号的总和
    sum(set(rooms))

所以这题不是“set 替代 list”,而是:

先有 list,再从 list 变出 set


一个更直观的比喻

你可以把这道题想成“学生名单”。

原始名单:

['张三', '张三', '张三', '李四', '李四', '李四', '王五']

这是完整签到表,谁来几次都记着。

而集合去重后:

{'张三', '李四', '王五'}

这变成了“有哪些不同的人”。

如果你要统计“出现总次数”,用原始名单。
如果你要统计“有几种不同的人”,用集合。

这题里的房间号也是一样。


这题里 list 到底还能不能用

当然能用,而且必须用。

因为输入读进来以后,最自然的形式就是列表:

rooms = list(map(int, input().split()))

这个 rooms 本身就是列表。

然后你可以根据需要做两件事:

用列表本身

sum(rooms)

得到真实总和。

在列表基础上转成集合

sum(set(rooms))

得到去重后的总和。

所以准确地说,不是“不能用 list”,而是:

不能用 list 去代替公式里本来应该承担“去重”任务的 set


一眼判断:什么时候必须用 set

当你看到题目中出现这些需求时,就要想到 set

  • 去重
  • 只保留不同元素
  • 判断有哪些不同值
  • 利用“不重复元素”做运算

而这道题的公式法,本质上就是:

先拿到不重复的房间号,再进行数学抵消。

所以必须用 set


和字典计数法再对比一下

你会发现:

字典计数法

关心的是:

  • 每个房间号出现几次

所以它保留的是“次数信息”。

集合公式法

关心的是:

  • 哪些房间号是不同的
  • 去重后的总和是多少

所以它保留的是“去重结果”。

这也是为什么两种方法都能做题,但用到的数据结构不同。


本节小结

这题中:

  • list 用来保存完整房间号列表
  • set 用来得到“每个房间号只保留一份”的结果

集合公式法之所以成立,关键就在于“去重”这一步。
list 不会自动去重,set 会。

所以不是说 list 没用,而是说:

在公式里那个位置,我们需要的是“去重后的房间号集合”,只有 set 能直接完成这个任务。

你可以把这句话记住:

sum(rooms) 是真实总和,sum(set(rooms)) 是去重后的种类总和。集合公式法同时需要这两种总和。


练习

你可以自己判断下面这两个表达式分别代表什么含义。

题目

rooms = [4, 4, 4, 9]

请分别说明:

sum(rooms)
sum(set(rooms))

它们各自表示什么?

提示

你不要只算结果,还要说出它们的“意义”:

  • 一个表示完整列表总和
  • 一个表示去重后总和

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇