🚪 "开关门"问题
彭彭老师
这道题是经典的"因子与完全平方数"问题,关键是从动手模拟到发现规律
第一步:动手模拟,建立直觉
用小数据亲手模拟一遍,比如 n=10。
画一张表格,用 ✅ 表示开,❌ 表示关:
| 房间号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 初始状态 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| 服务员1(全开) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 服务员2(2的倍数翻转) | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
| 服务员3(3的倍数翻转) | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| 服务员4(4的倍数翻转) | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 服务员5(5的倍数翻转) | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
| 服务员6 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
| 服务员7 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
| 服务员8 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 服务员9 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 服务员10 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
💡 自己数一数:最终打开的是哪些房间?(答案:1、4、9)
第二步:"为什么"
问一个关键问题:
"房间6被哪些服务员碰过?"
列出来:
- 服务员1(1×6=6)
- 服务员2(2×3=6)
- 服务员3(3×2=6)
- 服务员6(6×1=6)
一共被碰了 4次 (偶数次),所以最终是关着的。
再问:
"房间4被哪些服务员碰过?"
- 服务员1(1×4=4)
- 服务员2(2×2=4)
- 服务员4(4×1=4)
一共被碰了 3次 (奇数次),所以最终是开着的。
第三步:总结规律------找"朋友配对"
这是最关键的一步:
每个房间号就像要找"乘法好朋友"。比如房间12的好朋友有:
- 1 和 12(1×12=12)
- 2 和 6(2×6=12)
- 3 和 4(3×4=12)
好朋友总是成对出现 的,所以大多数房间被碰偶数次,最后关上了。
但是!有一种特殊情况------自己和自己配对:
- 房间4:2×2=4,2只能和自己配对,没有别人了
- 房间9:3×3=9,3只能和自己配对
- 房间16:4×4=16
这些房间多出来一次,总共被碰奇数次 ,所以最后开着!
用一句话总结:
🎯 只有"自己×自己"的房间号(也就是1、4、9、16、25......),门才是开着的。
第四步:联系代码,理解实现
cpp
// 外层循环:第i个服务员
for (int i = 1; i <= n; i++) {
// 内层循环:处理i的倍数(即i能整除的房间)
for (int j = i; j <= n; j += i) {
door[j] = 1 - door[j]; // 翻转状态
}
}
其实我们发现了规律,可以直接写更简单的代码!
cpp
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
bool first = true;
for (int i = 1; i * i <= n; i++) {
if (!first) cout << " ";
cout << i * i;
first = false;
}
cout << endl;
return 0;
}
开关灯问题和开关门问题本质上是同一道题,只是换了个"故事外壳"而已。
两者的对比
| 开关门问题 | 开关灯问题 | |
|---|---|---|
| 对象 | 房间的门 | 走廊的灯 |
| 初始状态 | 门关着 | 灯灭着 |
| 操作 | 第i个人翻转编号为i的倍数的门 | 第i个人翻转编号为i的倍数的灯 |
| 最终问题 | 哪些门开着 | 哪些灯亮着 |
核心逻辑完全一样:
- 每个门/灯被翻转的次数 = 它的因子个数
- 因子个数为奇数 → 最终是"开/亮"
- 只有完全平方数的因子个数是奇数(因为有一个因子"自己×自己",无法配对)
所以结论也一样:最终亮着的灯(或开着的门)就是 1, 4, 9, 16, 25, 36......
"你们发现了吗?不管是开关门还是开关灯,其实都是同一个问题!就像数学里的'鸡兔同笼',题目可以编出很多故事,但解题方法是一样的。"
可以用简单的数学方法
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin>>n;
for(int i=1;i*i<=n;i++){
cout<<i*i<<" ";
}
return 0;
}
也可以用模拟
cpp
#include <bits/stdc++.h>
using namespace std;
bool lights[1005]={false};
int main() {
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j+=i){
lights[j]=!lights[j];//切换灯的状态
}
}
//输出灯
bool f = true;
for(int i=1;i<=n;i++){
if(lights[i]){
if(!f)cout<<" ";
cout<<i;
f=false;
}
}
return 0;
}

cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
int n,m;
cin>>n>>m;
bool arr[5005]={false};//灯初始全开
//模拟m个人操作
for(int i=1;i<=m;i++){
for(int j=i;j<=n;j+=i){
arr[j] = !arr[j];//切换状态
}
}
//输出关闭的灯
bool f=true;
for(int i=1;i<=n;i++){
if(arr[i]){
if(!f)cout<<",";//逗号输出法
cout<<i;
f=false;
}
}
return 0;
}
常见的"开关灯"和"开关门"问题通常有以下几种区别:
常见的差异点
| 对比项 | 开关灯问题 | 开关门问题 |
|---|---|---|
| 初始状态 | 通常初始全亮(开) | 通常初始全关(关) |
| 最终输出 | 输出关着的编号 | 输出开着的编号 |
| 操作人数 | 有n盏灯、m个人(m可能≠n) | 通常n扇门、n个人(一一对应) |
核心区别
-
初始状态相反 → 最终结果相反。灯初始全开,被切换奇数次的灯最终是关 的;门初始全关,被切换奇数次的门最终是开的。
-
操作人数是否等于总数 → 经典开关门问题中,人和门数量相同(都是n),所以最终开着的门就是完全平方数 。但你这道题中
m和n可以不同,当m < n时,大编号的灯可能因子不全被操作,结果就不是简单的完全平方数了。
当 m 可能 ≠ n 时,建议直接用模拟法。 核心区别如下:
- 开关门(m = n) :所有因子都在操作范围内,被切换奇数次的门一定是完全平方数,可直接输出。
- 开关灯(m ≠ n) :当
m < n时,大编号灯的因子可能超出m的范围,导致操作次数奇偶性改变,数学规律失效。此时必须用模拟法,按题意逐人逐灯切换状态。
数学做法的适用边界 :只有当 m ≥ n(操作人数 ≥ 灯数)时,所有因子都能被操作到,数学结论才成立。只要 m < n,就必须老老实实模拟。
