<think>
我们面对的问题与引用中的问题不同,但可以借鉴其中的思想。问题描述如下:
有N盏灯,初始状态全亮。有M个人,按照以下规则操作:
1号人:将所有灯都关闭(因为初始全亮,所以全部变为关闭状态)
2号人:将编号为2的倍数的灯做相反处理(即打开这些灯)
3号人:将编号为3的倍数的灯做相反处理(即关闭的打开,打开的关闭)
...
k号人:将编号为k的倍数的灯做相反处理(k从3到M)
经过M个人操作后,求关闭的灯的编号(按从小到大输出)。
注意:题目中N<=5000,M<=N。
我们分析一下:每个灯最终的状态取决于它被操作了多少次。如果一个灯被操作了奇数次,那么它最终状态与初始状态相反;如果操作了偶数次,则与初始状态相同。
初始状态全亮(我们假设亮为1,灭为0),那么:
操作奇数次:最终状态为0(灭)
操作偶数次:最终状态为1(亮)
因此,问题转化为:对于编号为i的灯(1<=i<=N),它被操作的次数等于在区间[1,M]中,能够整除i的整数的个数(即i的约数个数,且这些约数必须在[1,M]区间内)。
注意:1号人的操作会改变所有灯(因为1是所有数的约数),2号人操作2的倍数,3号人操作3的倍数,等等。
因此,对于第i盏灯,它在第k个人操作时会被操作当且仅当k整除i(且k<=M)。我们用d(i)表示i在[1,M]区间内的约数个数。
则灯i的最终状态为:初始状态为亮(1),如果d(i)是奇数,则状态变为0(灭);如果d(i)是偶数,则状态为1(亮)。
所以,关闭的灯就是那些d(i)为奇数的灯。
然而,我们还需要注意:1号人的操作是关闭所有灯,也就是说,在1号人操作后,所有灯都灭了。然后2号人操作2的倍数(打开),3号人操作3的倍数(此时,对于3的倍数,之前被1号人操作过(灭),然后2号人操作了其中2的倍数(打开),3号人操作时,将3的倍数中2的倍数部分又关掉了,而其他3的倍数(非2的倍数)则被打开)... 所以确实符合约数个数的奇偶性决定最终状态。
但是,我们需要注意的是,操作次数是从1到M的,所以每个灯i的操作次数确实是[1,M]中i的约数个数(包括1和i自身,如果i<=M的话)。
问题:如何快速计算每个灯的约数个数?注意N最大5000,我们可以直接枚举每个灯的约数,然后统计在[1, M]范围内的约数个数。但是这样时间复杂度是O(N*sqrt(N)),而N=5000,sqrt(N)约70,所以5000*70=35万,是可以接受的。
但是,我们还可以换个思路:我们并不需要知道每个灯的所有约数,只需要知道约数个数的奇偶性。而约数个数的奇偶性有什么特点?
注意:一个数的约数个数是奇数当且仅当这个数是完全平方数(因为约数通常成对出现,只有当两个约数相等时,才只有一个,也就是平方数)。但是,这里我们并不是统计所有的约数个数,而是统计在[1,M]区间内的约数个数。所以,即使一个数是平方数,如果它的平方根大于M,那么它在[1,M]区间内约数个数并不一定是奇数。
例如:灯编号i=4,它是一个平方数(2*2),那么在[1,M]区间内,4的约数有1,2,4(如果M>=4)那么有3个,奇数;但如果M=3,那么约数有1,2,共2个,则是偶数。
因此,我们不能直接用完全平方数来判定,而需要实际计算[1,M]区间内的约数个数。
但是,我们可以利用一个事实:在[1, M]区间内,i的约数个数的奇偶性实际上取决于i在[1, M]区间内是否有偶数个约数。然而,直接计算每个数的约数个数奇偶性并不比计算约数个数本身简单多少(因为我们只需要奇偶性,所以可以对2取模,但计算约数个数的过程还是要遍历约数)。
所以,我们直接对每个灯i(从1到N)计算:
cnt = 0
遍历j从1到min(M, sqrt(i)),如果i % j == 0:
那么j是一个约数,同时i/j也是一个约数(除非j*j=i)。
但是要注意:j必须在[1, M]范围内,并且i/j也必须在[1, M]范围内(且不等于j)?不对,我们只需要统计不同的约数,并且同一个约数只能计数一次。
然而,我们并不需要遍历到i,实际上遍历到sqrt(i)就够了。但是要注意重复计数的问题。我们可以这样:
对于每个j(1<=j<=min(M, sqrt(i))),如果j整除i,那么:
如果j <= M,则计数一次;
如果另一个因子k = i/j,并且k<=M且k!=j,那么计数一次。
但是,这样会漏掉吗?不会,因为约数是成对出现的。但是注意:当j==k时(即j*j=i),我们只能计数一次。
所以,伪代码如下:
对于每个i(1到N):
cnt = 0
j从1到sqrt(i)(取整数):
if i % j == 0:
if j <= M:
cnt += 1
k = i // j
if k != j and k <= M:
cnt += 1
然后判断cnt的奇偶性:如果cnt为奇数,则灯i是关闭的;否则是亮的。
但是,这里有一个问题:同一个约数会不会被重复计算?不会,因为j和k是成对出现的,而且j<=sqrt(i),k>=sqrt(i),所以只要保证j和k不同时,分别计数即可。
然而,这种方法需要遍历sqrt(i)次,对于5000个灯,每个灯最多遍历约70次(sqrt(5000)约70.7),总次数5000*70=35万,是可以接受的。
但是,我们有没有更高效的方法?注意到我们只需要知道奇偶性。实际上,一个数在[1,M]区间内的约数个数的奇偶性并不容易直接快速判断。所以采用上述方法是合适的。
但是,我们注意到:每个灯的最终状态只与操作次数的奇偶性有关,而操作次数等于在[1, M]区间内i的约数个数。因此,我们可以换一个角度:枚举每个操作人(因子)对灯的影响。然而,题目要求输出关闭的灯的编号,所以我们需要知道每个灯的状态。
另一种思路:我们可以用一个数组light[1..N]来表示灯的状态(初始为全亮,但我们知道操作奇数次会改变为灭,偶数次亮)。实际上,我们并不需要模拟过程,只需要计数操作次数。
因此,我们采用上述方法计算每个灯的操作次数(只需要奇偶性),然后判断。
但是,我们还可以利用约数个数的性质:在[1, M]区间内,i的约数个数等于满足1<=j<=min(M, i)且j整除i的j的个数。
然而,我们也可以反过来枚举因子:对于每个因子j(1<=j<=M),它会改变所有j的倍数的灯。但是如果我们用一个数组记录每个灯被操作的次数,然后遍历j从1到M,将j的倍数位置的计数加1,最后再判断奇偶性。这样的复杂度是O(M*(N/j)),即O(N*log(M)),因为调和级数。而M<=5000,N<=5000,那么总操作次数大约是N*(1+1/2+1/3+...+1/M)≈N*log(M)≈5000*log(5000),log(5000)≈8.5,所以总操作次数大约5000*8.5=42500次,比上面的35万次要少。
所以,我们可以用第二种方法(更优):
初始化一个数组count[1..N](下标从1到N)为0(表示操作次数)
for j in range(1, M+1): # j表示第j个人
for k in range(j, N+1, j): # k是j的倍数
count[k] += 1
然后,遍历i从1到N:
如果count[i] % 2 == 1,则灯i是关闭的(因为初始全亮,奇数次操作后状态翻转,变为灭)
但是,注意:初始状态是全亮,而1号人操作后所有灯都灭了(因为1号人操作了所有灯,所以每个灯操作次数先变成1),然后2号人操作2的倍数,3号人操作3的倍数...所以符合我们的操作次数计数。
因此,这种方法更直接,且复杂度可以接受(O(N*log(M)))。
但是,我们还需要注意:题目中M<=N,所以内层循环的次数为:对于每个j,内层循环次数为N//j,所以总循环次数为:sum_{j=1}^{M} (N//j) <= N * (1+1/2+1/3+...+1/M) ≈ N * log(M) (调和级数部分和)。当N=M=5000时,这个和小于5000*10(因为调和级数前5000项和约为9),所以总操作次数最多50000次左右,完全可行。
因此,我们采用第二种方法。
步骤:
1. 创建一个长度为N+1的数组count(索引0不用,从1到N),初始化为0。
2. for j in range(1, M+1):
for k in range(j, N+1, j):
count[k] += 1
3. 遍历i从1到N:
if count[i] % 2 == 1:
将i加入结果列表
4. 输出结果列表(已经是从小到大,因为i从1到N遍历)
但是,注意:初始状态全亮,而1号人操作后所有灯都灭(每个灯操作1次,为奇数,所以灯灭),然后2号人操作2的倍数,那么编号为2的倍数的灯操作次数变为2(偶数),所以这些灯会变亮(因为操作偶数次,状态与初始相同,初始亮?不对,初始亮,但1号人操作后所有灯灭,2号人操作后这些灯又亮,所以变成亮)。而我们上面的过程是:我们只记录操作次数,然后根据奇偶性判断最后状态:奇数次则灭,偶数次则亮。
所以,我们的逻辑正确。
但是,我们也可以验证一下小数据:
假设N=3, M=3。
初始:灯1,2,3全亮。
1号人:操作所有灯(1,2,3),操作次数变为1,状态全灭。
2号人:操作2的倍数(2),操作后:灯1:灭(操作1次),灯2:操作次数变成2(偶数,所以亮),灯3:灭(操作1次)。
3号人:操作3的倍数(3),操作后:灯1:灭(1次),灯2:亮(2次),灯3:操作2次(偶数,所以亮)。最终:灯1灭,灯2亮,灯3亮。
按照我们的方法:
count[1]=1(只有1号人操作)-> 奇数 -> 灭
count[2]=2(1号和2号)-> 偶数 -> 亮
count[3]=2(1号和3号)-> 偶数 -> 亮
符合。
另一种情况:M=1,则只有1号人操作,那么所有灯都操作1次,所以所有灯灭(即关闭的灯为1,2,...,N)。
因此,我们按照这个思路写代码。
但是,注意:题目要求按从小到大输出关闭的灯的编号。
代码实现:
由于N最大5000,我们可以直接使用双重循环。
注意:内层循环从j开始,步长为j。
具体代码(Python)如下:
但是,我们还需要考虑性能:双重循环在N=5000时,内层循环总次数大约为5000*(1+1/2+1/3+...+1/5000)≈5000*9.09≈45450次,不会超时。
因此,我们写代码:
注意:灯编号从1到N。
输入:N, M
输出:一行,关闭的灯的编号(按从小到大,空格隔开,如果没有关闭的灯,则输出空行)
例如:输入N=3, M=3,输出"1"
输入N=1, M=1:输出"1"
输入N=3, M=2:输出"1 3"(因为M=2时,1号人操作灯1,2,3(灭);2号人操作灯2(变为亮)。所以最后灯1灭,灯2亮,灯3灭)
但是,我们验证一下N=3,M=2:
灯1:操作次数为1(奇数)->灭
灯2:操作次数为2(1和2)->偶数->亮
灯3:操作次数为1(1号人操作)->灭
所以关闭的灯是1和3。
因此,输出"1 3"
代码:
我们可以这样:
初始化count = [0]*(N+1) # 索引0到N,不使用0
for j in range(1, M+1):
k = j
while k <= N:
count[k] += 1
k += j
然后,遍历i从1到N:
if count[i] % 2 == 1:
将i加入列表
输出列表。
但是,我们可以优化:内层循环用步长j来遍历,这样比while循环更直观。
另一种写法:
count = [0] * (N+1)
for j in range(1, M+1):
# 遍历所有j的倍数
for k in range(j, N+1, j):
count[k] += 1
然后收集关闭的灯。
完整代码:
注意:题目要求输出关闭的灯的编号,按从小到大。
我们写函数:
由于题目没有说明输入输出格式,我们可以按照以下方式:
输入:两个整数N和M(空格隔开)
输出:一行,关闭灯的编号,空格隔开(如果没有,输出空行)
例如:
输入:3 3
输出:1
输入:3 2
输出:1 3
但是,我们也可以测试一下N=1, M=1:输出"1"
另外,注意:当M=0时,所有人都不操作,那么所有灯都是亮的(初始亮),所以没有关闭的灯。
但是题目中M>=1?因为题目说M个人,至少有一个1号人。所以M>=1。
因此,我们不需要考虑M=0。
但是,题目说初始全亮,然后1号人全关,所以M>=1时,1号人一定操作。
所以,我们直接写。
但是,注意双重循环在N=5000时的性能:总操作次数约为N*log(M)(约5000*9=45000次),在Python中是可以接受的。
但是,我们可以进一步优化:我们并不需要精确的操作次数,只需要奇偶性。因此,我们可以用异或操作(或者用布尔数组记录奇偶性)。
改进:我们用一个布尔数组state(或者用0和1表示当前操作次数的奇偶性)来记录每个灯操作次数的奇偶性。初始为0(偶数次),每次操作相当于翻转一次奇偶性(即奇数次变偶数,偶数变奇数)。所以,对于每个人j,我们对所有j的倍数执行state[k] = not state[k](或者用1-state[k])。但是这样需要布尔数组,而且翻转操作也是O(N/j),总时间不变,但空间可以节约(因为count变成了布尔数组,并且我们避免了取模运算)。
但是,我们也可以这样做:
初始化一个数组light,长度为N+1,全部为0(0表示偶数次操作,那么最终状态为亮;1表示奇数次操作,最终状态为灭)。注意初始全亮,所以操作偶数次为亮,奇数次为灭。
然后,对于每个人j(1到M):
遍历k从j到N,步长为j:
light[k] = 1 - light[k] # 翻转操作
然后,所有light[i]为1的灯i就是关闭的。
但是,这个方法和计数方法本质上一样,但避免了最后对2取模。而且内层循环次数相同。
然而,我们还可以用更高效的方法:利用每个灯的约数个数的奇偶性等于完全平方数的性质?但是这里不是整个自然数区间,而是[1,M]区间,所以不能直接用平方数性质。
但是,我们可以这样:一个灯i在[1,M]区间内的操作次数等于因子个数,而我们只需要奇偶性。而因子个数的奇偶性在[1,M]区间内为奇数,当且仅当i在[1,M]区间内的因子个数为奇数。而因子个数为奇数当且仅当i是完全平方数?不对,比如i=6,在[1,6]区间内因子有1,2,3,6(4个,偶数),而6不是平方数;但是i=4,因子有1,2,4(3个,奇数)。所以,在完整的区间[1,i]内,因子个数为奇数当且仅当i是完全平方数。但是我们的区间是[1,M](M<=i或者M>i),所以只有在M>=i时,我们才可以用完全平方数来判断?不,比如i=6,M=3,那么因子只有1,2,3(3个,奇数),此时尽管6不是平方数,但在[1,3]区间内因子个数为奇数。
因此,我们无法利用平方数的性质。
所以,我们采用双重循环的方法。
但是,我们可以优化翻转操作:由于翻转操作是布尔值的翻转,我们可以用异或(XOR)运算。我们可以用一个布尔数组(或整数数组,0和1)来记录。
算法:
初始化lights = [0]*(N+1) # 下标0不用,1到N,0表示偶数次操作(最终亮),1表示奇数次操作(最终灭)
for j in range(1, M+1):
k = j
while k <= N:
lights[k] ^= 1 # 翻转
k += j
然后,收集lights[i]==1的i。
这个算法和上面用计数再取模相比,内层循环次数一样,但避免了最后遍历N时取模,而是分散到每次操作中。效率差不多。
但是,我们注意到:使用布尔数组的翻转,内层循环只需要一次异或操作,而计数方法需要加法。所以布尔翻转可能更快。
不过,题目N最大5000,M最大5000,所以双重循环的次数约为45450次,无论哪种方法都很快。
因此,我们可以采用任何一种方法。这里采用计数取模的方法(因为逻辑清晰,而且最后只需要一次判断)。
但是,我们也可以尝试用优化:我们不需要保存完整的count数组,只需要保存奇偶性(用一个布尔数组表示奇偶性)。所以我们可以这样:
用一个数组parity(布尔型,长度为N+1),初始为0(偶数)。
对于j从1到M:
遍历每个j的倍数k:parity[k] = not parity[k]
然后,遍历i从1到N:如果parity[i]为True,则加入结果。
代码:
我们选择用计数取模的方法(因为简单直接),但用布尔翻转也可以。这里我们用计数取模。
但是,为了节省空间,我们可以用一个整数数组count,然后最后根据count[i]%2判断。但是这样需要额外的空间存储count(整数数组,每个整数4或8字节),而N=5000,空间占用为5000*4/1024≈20KB,可以接受。
因此,我们决定用计数数组。
代码实现如下:
注意:输入输出。
题目要求:输入N和M,输出关闭的灯编号(从小到大,空格隔开)
例如:
输入:3 3
输出:1
输入:3 2
输出:1 3
但是,我们也可以测试一个更大的例子:比如N=10, M=10,然后手动计算一下?
手动计算(仅计算前几个灯):
灯1:操作的人:1,? -> 1号(1次),2号不是1的倍数(2>1,所以不是),3号也不是?不对,1的倍数:1,2,...,10,所以1号到10号人都会操作灯1?因为1的倍数是所有数。所以灯1被操作10次(偶数)->亮(不关闭)。
灯2:操作的人:1,2,4,6,8,10 -> 6次(偶数)->亮。
灯3:操作的人:1,3,6,9 ->4次(偶数)->亮。
灯4:操作的人:1,2,4,8 ->4次(偶数)->亮。
灯5:操作的人:1,5 ->2次(偶数)->亮。
灯6:操作的人:1,2,3,6 ->4次(偶数)->亮。
灯7:操作的人:1,7 ->2次(偶数)->亮。
灯8:操作的人:1,2,4,8 ->4次(偶数)->亮。
灯9:操作的人:1,3,9 ->3次(奇数)->灭。
灯10:操作的人:1,2,5,10 ->4次(偶数)->亮。
所以关闭的灯只有9。
因此,输出:9
我们再用程序计算一下N=10,M=10,看输出是否为9。
代码:
步骤:
1. 读取输入:一行两个整数N, M
2. 初始化count数组(长度为N+1,初始0)
3. j从1到M(包括M):
对于k=j, j+j, j+2j, ... 直到<=N:
count[k] += 1
4. 遍历i从1到N:
如果count[i] % 2 == 1,则将i加入结果列表
5. 将结果列表转换为字符串输出(用空格连接)
注意:内层循环的写法:
代码(Python):
我们写完整代码:
但是,由于题目没有说明具体输入输出方式,我们假设输入在一行,空格分隔。
代码如下:
但是,我们也可以考虑另一种优化:我们并不需要计算每个灯的操作次数,而是只需要奇偶性。所以我们可以用异或代替加法。但是加法也可以,因为最后我们只关心模2的结果。
所以,我们可以用:
count[k] = (count[k] + 1) % 2 # 这样每次翻转,最后count[k]就是操作次数的奇偶性(0偶1奇)
但是,这样翻转后,最后我们只需要判断count[k]是否为1。
而且,这样避免了最后取模,但是内层循环中需要做一次模2运算(实际上就是异或,但加法模2和异或等价)。不过,这样每次加法模2和直接加最后模2没有区别。
我们这里采用最后统一取模。
因为取模运算在最后,内层循环只做加法,这样内层循环更快。
所以,我们采用先计数,最后再取模判断。
代码:
注意:使用加法然后最后取模,这样内层循环只是整数加法,效率高。
但是,我们也可以用一个布尔数组,然后进行翻转(用异或),翻转操作和加法模2运算其实是一样的。
这里,我们选择最后取模。
编写代码并测试。
测试用例:N=3, M=2 -> 输出"1 3"
但是,在输出时,我们需要注意:题目要求从小到大,我们遍历i就是从1到N,所以自然有序。
输出时,用空格隔开。
代码:
由于题目中N<=5000,所以我们可以用双重循环。
下面是Python代码:
注意:我们使用两层循环,内层循环使用步长j来遍历倍数。
但是,如果M很大(比如M=5000),内层循环次数大约是N/j,总次数为O(N*log(M)),可以接受。
具体代码:
</think>
### 问题分析
给定 $N$ 盏灯(初始全亮)和 $M$ 个人按规则操作:
1. **1号人**:关闭所有灯(全灭)
2. **2号人**:对编号为2的倍数的灯做相反操作(开/关切换)
3. **k号人(k≥3)**:对编号为k的倍数的灯做相反操作
需要输出第 $M$ 个人操作后**关闭的灯**的编号(按升序排列)。
$N \leq 5000$,$M \leq N$。
### 算法思路
#### 核心原理
- **灯的状态**由操作次数的奇偶性决定:
- 操作奇数次:灯关闭(初始亮 → 奇数次操作后灭)
- 操作偶数次:灯保持亮
- **操作次数**:灯 $i$ 被操作 $\Leftrightarrow$ 操作人编号 $j$ 是 $i$ 的约数($1 \leq j \leq M$)
- 问题转化为:**对每个灯 $i$,统计在 $[1, M]$ 范围内约数的个数 $d(i)$,若 $d(i)$ 为奇数则灯关闭**。
#### 优化求解
1. **初始化计数数组**:`count[1..N] = 0`(记录每盏灯的操作次数)
2. **枚举操作人 $j$(1 到 $M$)**:
- 对每个 $j$,更新其倍数灯的操作次数:
$k = j, 2j, 3j, \dots \leq N$ 时,`count[k] += 1`
3. **收集结果**:
- 遍历 $i$ 从 $1$ 到 $N$,若 `count[i] % 2 == 1`,则 $i$ 是关闭的灯
#### 复杂度分析
- **时间复杂度**:$O(M \log N)$
内层循环次数为 $\sum_{j=1}^{M} \lfloor N/j \rfloor \approx O(N \log M)$,$N, M \leq 5000$ 时计算量约 $5 \times 10^4$ 次操作,高效可行。
- **空间复杂度**:$O(N)$,使用数组存储操作次数。
### 代码实现
```python
def find_closed_lamps(N, M):
count = [0] * (N + 1) # 计数数组(索引0未用)
# 枚举操作人j,更新其倍数灯的操作次数
for j in range(1, M + 1):
for k in range(j, N + 1, j):
count[k] += 1
# 收集操作次数为奇数的灯编号
closed_lamps = []
for i in range(1, N + 1):
if count[i] % 2 == 1:
closed_lamps.append(i)
return closed_lamps
# 输入处理
N, M = map(int, input().split())
result = find_closed_lamps(N, M)
print(" ".join(map(str, result)))
```
### 示例演示
#### 输入 `3 2`
1. **操作过程**:
- 初始:灯1(亮), 灯2(亮), 灯3(亮)
- 1号人:全关 → [灭, 灭, 灭]
- 2号人:切换2的倍数(灯2)→ [灭, 亮, 灭]
2. **关闭的灯**:1, 3 → 输出 `1 3`
#### 输入 `10 10`
- **操作次数统计**:
| 灯编号 | 约数(操作人) | 操作次数 | 状态 |
|--------|------------------------------|----------|------|
| 1 | 1,2,...,10 | 10 (偶) | 亮 |
| 9 | 1,3,9 | 3 (奇) | **灭** |
| 其他灯 | 约数个数均为偶数 | - | 亮 |
- **输出**:`9`
### 拓展思考
1. **逆向问题**:若初始状态全灭,如何修改算法得到最终亮着的灯?
2. **高效优化**:当 $N$ 极大时(如 $10^6$),能否用数论性质(完全平方数)进一步优化?[^1]
3. **动态操作**:若操作规则变为随机切换连续子序列,如何设计更复杂的线段树模型?[^2]
---