<think>我们考虑使用BFS(广度优先搜索)结合状态压缩和哈希表(或数组)来记录状态。由于题目要求最少的玩具数,并且当玩具数相同时要求字典序最小的序列,BFS天然满足按玩具数递增的顺序搜索,因此可以保证最先找到的解使用的玩具数最少。但是,题目还要求当玩具数相同时输出字典序最小的序列,因此我们需要在每一层(即每个玩具数)中按字典序搜索,并找到第一个可行解。
具体思路:
1. 状态表示:用一个整数state的二进制位表示每只猫的状态(0表示生气,1表示高兴)。初始状态为0(所有二进制位为0,表示所有猫都生气),目标状态为(1<<n)-1(所有二进制位为1,表示所有猫都高兴)。
2. 预处理:对每个玩具i,我们预处理两个掩码:
- set_mask[i]:表示使用玩具i后,哪些猫会被设置为高兴(即输入为1的位置)。
- clear_mask[i]:表示使用玩具i后,哪些猫会被设置为生气(即输入为-1的位置)。注意:0表示无感,不改变状态。
3. 状态转移:对于当前状态state,使用玩具i后的新状态为:
new_state = (state | set_mask[i]) & ~clear_mask[i]
4. BFS过程:
- 使用队列,每个元素记录当前状态、已使用的玩具序列(或路径)。
- 由于要求输出序列,我们需要记录到达当前状态的路径(即玩具序列)。
- 为了避免重复状态,我们使用一个visited数组(或哈希表)记录每个状态是否被访问过。注意:同一个状态可能由不同的玩具序列达到,但我们要保证最小玩具数,所以第一次到达这个状态时就是最少玩具数,后续再遇到相同状态可以跳过。但是,题目还要求最小字典序,因此我们在同一层(即相同玩具数)中需要按字典序从小到大的顺序进行搜索,并且当遇到一个状态时,如果已经访问过,那么当前路径的字典序一定比之前的大(因为我们是按玩具编号从小到大枚举的,所以第一次访问到该状态时的序列字典序一定最小),因此可以跳过。
- 然而,同一个状态可能由多个玩具序列达到,且玩具数相同,但字典序不同。我们希望在第一次访问到某个状态时就记录下该状态对应的最小字典序序列。因此,我们可以在状态中记录到达该状态的路径,并且当我们访问一个状态时,如果已经访问过,那么当前路径的字典序一定不会更小(因为BFS按玩具数分层,同一层中按玩具编号从小到大枚举,所以第一次访问到该状态的路径就是最小字典序的路径)。所以,我们可以使用一个visited数组记录每个状态是否被访问过,一旦访问过就不再处理。
5. 字典序处理:在BFS中,我们按玩具编号从小到大的顺序枚举下一个玩具。这样,在同一层中,我们优先选择编号小的玩具,从而保证最先找到的到达目标状态的序列是字典序最小的。
6. 优化:由于n<=10,状态总数为2^10=1024,玩具数m<=10,所以状态空间不大。
具体步骤:
- 初始化队列,初始状态state=0,路径为空。
- 将初始状态标记为已访问。
- 当队列非空:
* 取出队首元素(包含当前状态和路径)
* 如果当前状态为目标状态,则输出路径(即玩具序列),并结束。
* 否则,按玩具编号从小到大的顺序(1到m)枚举下一个玩具(注意:不能使用已经使用过的玩具?题目没有说玩具不能重复使用,但输入样例中玩具是不重复的。题目要求是“最少玩具”,显然重复使用玩具不可能使玩具数更少,因为我们可以去掉重复的玩具得到更少的解。所以题目隐含玩具不重复使用。因此,我们需要记录哪些玩具已经使用过。但是,在BFS中,我们每次扩展时,只能使用尚未使用过的玩具(因为每个玩具只能使用一次)?注意:题目没有明确说,但输入是给出M个玩具,每个玩具使用一次。所以每个玩具最多使用一次,且不能重复使用。
然而,题目要求:输出最少玩具的解,且当解不唯一时输出最小编号序列。因此,我们使用每个玩具最多一次。
所以,我们需要记录已经使用过哪些玩具。但是,在状态中,除了记录当前猫的状态state,还需要记录已经使用过的玩具集合(因为不同的玩具组合可能达到相同的状态,但后续可用的玩具不同)。但是,这样状态空间就变成了(状态state,已使用玩具集合),状态数量为1024*2^10=1024*1024=1e6,在m=10时,2^10=1024,所以总状态数为1024*1024=1e6,可以接受。
但是,我们还需要记录路径(为了输出序列),所以状态中需要保存路径(或者保存到达该状态时使用的玩具序列)。这样空间可能较大。
另一种思路:我们不需要记录已使用玩具的集合,而是记录当前状态state,然后枚举下一个未使用的玩具。但是,在BFS中,不同路径可能以相同的状态结束,但已使用的玩具集合不同。然而,我们关心的是状态,因为状态决定了猫的心情,而后续操作只与当前状态有关,与之前使用过哪些玩具无关。所以,我们可以这样设计:
- 状态:state(猫的状态)
- 但是,同一个state,可能由不同的玩具集合达到,而且我们要求最少玩具数,所以当我们第一次到达state时,使用的玩具数是最少的,并且此时记录下到达该状态的最小字典序路径。那么,如果再次以相同的玩具数(或更多)到达相同的state,且路径的字典序更大,我们可以跳过。
然而,问题在于:我们要求的是最少玩具数,所以BFS是按玩具数分层(即每使用一个玩具,玩具数+1,进入下一层)。所以,当我们在第k层第一次到达状态state时,那么之后在第k层再次到达state的路径,其玩具数相同,但字典序可能更大(因为我们是按字典序从小到大枚举的,所以第一次到达的路径字典序最小),所以我们可以跳过。而在第k+1层再次到达state,由于我们已经找到过k层到达state的路径,那么k+1层的路径玩具数更多,所以不是最优解,直接跳过。
因此,我们只需要记录每个状态state是否在某一层被访问过(即该状态是否已经以更少或相同的玩具数访问过,且相同玩具数时字典序更小)。具体:我们用一个数组visited[state]表示该状态是否被访问过(不论玩具数多少,一旦访问过,后面再遇到都可以跳过?)但是,注意:有可能在玩具数相同的情况下,不同的路径到达同一状态,但先访问的路径字典序更小,所以后访问的路径可以跳过。
所以,我们在BFS中,每扩展到一个新状态,如果该状态没有被访问过,则标记为已访问,并记录当前路径(用于后续输出)。由于BFS按玩具数分层,所以当我们第一次访问到目标状态时,玩具数最少,并且由于同一层中按字典序从小到大扩展,所以最先访问到目标状态的路径就是字典序最小的。
具体实现:
- 使用队列:存储(当前状态state,当前路径path)
- 使用visited数组:visited[state] = true,表示状态state已经访问过。
- 初始化:state=0, path为空,visited[0]=true。
- 按层扩展(BFS):每一层对应使用了一个玩具。
- 对于当前状态,枚举所有未使用过的玩具?不对,因为我们在路径中已经记录了使用过的玩具,所以不能重复使用。但是,在BFS过程中,我们如何知道哪些玩具未使用过?我们可以记录当前路径,然后枚举不在路径中的玩具。但是这样每次需要检查m个玩具是否在路径中,而路径长度最多为m(最多10),所以可以接受。
- 但是,我们要求字典序最小,所以枚举玩具时按编号从小到大枚举,并且只枚举不在当前路径中的玩具。
步骤:
while (队列不空) {
取出队首元素(state, path)
if (state == 目标状态) 输出path,返回。
否则,枚举下一个玩具i(从1到m,且i不在path中):
计算新状态:new_state = (state | set_mask[i]) & ~clear_mask[i]
如果visited[new_state]为false,则:
标记visited[new_state]=true
将新状态和路径(path+i)加入队列
}
注意:这样同一层中,由于我们按玩具编号从小到大枚举,所以队列中的状态,其路径的字典序也是从小到大的。但是,由于BFS是一层一层地扩展,所以当我们第一次到达目标状态时,玩具数最少,且在同一层中字典序最小。
但是,这样会有一个问题:同一个状态可能由多个路径以相同的玩具数达到,但我们只访问一次(第一次访问时标记,后面不再访问)。而第一次访问时,由于我们按玩具编号从小到大枚举,所以第一次访问该状态的路径字典序最小。因此,后续路径即使玩具数相同,但字典序更大,所以可以跳过。
因此,这个BFS可以保证找到玩具数最少且字典序最小的解。
但是,空间问题:路径最多有10个玩具,状态有1024种,所以队列中元素最多1024个?不对,因为不同路径可能导致相同状态,但我们在第一次访问状态时就标记了,所以每个状态最多出现一次。因此队列中最多有1024个元素。
时间复杂度:每个状态最多扩展m次(m<=10),所以总操作数为1024*10=10240,非常快。
但是,注意:在扩展时,我们需要枚举所有未使用过的玩具(最多10个),然后检查新状态是否被访问过。所以总操作数在10240次左右。
具体代码步骤:
1. 预处理每个玩具的set_mask和clear_mask。
2. 目标状态:target = (1<<n)-1
3. 初始化:
queue< pair<int, vector<int>> > q; // 状态和路径
q.push({0, {}});
visited[0] = true;
4. 当队列非空:
取出队首:state, path
if (state == target) 输出path(空格分隔),结束程序。
枚举每个玩具i(1~m):
如果i已经在path中,跳过。
否则,计算新状态new_state = (state | set_mask[i]) & ~clear_mask[i]
如果visited[new_state]为false,则:
visited[new_state] = true
new_path = path + [i]
q.push({new_state, new_path})
5. 注意:由于我们按玩具编号从小到大枚举,所以第一个找到的目标状态的路径就是字典序最小的最少玩具解。
但是,这样找到的解是否一定是最少玩具数?因为BFS是按层(路径长度递增)进行的,所以最先到达目标状态的路径长度(玩具数)一定是最少的。
然而,题目要求:当最少玩具的解不唯一时,输出最小编号序列。而我们的BFS在同一层中按字典序从小到大扩展,所以第一个找到的序列就是字典序最小的。
但是,我们是否可能漏掉解?不会,因为BFS会尝试所有可能。
但是,注意:同一个玩具不能使用两次,我们在枚举时跳过了已经在路径中的玩具,所以不会重复使用。
我们使用visited数组记录状态,避免重复访问相同状态。但是,同一个状态可能由不同的玩具组合达到,而我们只记录第一次达到的路径。由于第一次达到的路径玩具数最少(BFS按层)且字典序最小(同一层中按编号从小到大枚举),所以正确。
但是,考虑:有可能在较少的玩具数下,某个状态被访问,那么之后更多的玩具数再到达这个状态就没有意义了,因为我们已经有一条更短的路径。
因此,BFS+状态压缩可以高效解决。
输入样例测试:
玩具1: set_mask=000? 不对,我们按照二进制位:题目中第一只猫对应第0位,第二只猫对应第1位,第三只猫对应第2位。
样例输入:
3 4
-1 1 0 -> 玩具1:
猫0: -1 -> clear_mask的第0位设为1(即clear_mask[1] |= 1<<0)
猫1: 1 -> set_mask[1] |= 1<<1
猫2: 0 -> 不操作
所以:set_mask[1] = 0b010 (2), clear_mask[1] = 0b001 (1)
玩具2: 1 1 -1 ->
猫0:1 -> set_mask[2]|=1<<0 (1)
猫1:1 -> set_mask[2]|=1<<1 (2) -> 所以set_mask[2]=0b011 (3)
猫2:-1 -> clear_mask[2]|=1<<2 (4)
所以:set_mask[2]=3, clear_mask[2]=4
玩具3: 0 -1 1 ->
猫0:0 -> 忽略
猫1:-1 -> clear_mask[3]|=1<<1 (2)
猫2:1 -> set_mask[3]|=1<<2 (4)
所以:set_mask[3]=4, clear_mask[3]=2
玩具4: 1 0 0 ->
猫0:1 -> set_mask[4]|=1<<0 (1)
猫1:0 -> 忽略
猫2:0 -> 忽略
所以:set_mask[4]=1, clear_mask[4]=0
初始状态:0(二进制000)
目标状态:0b111=7
我们模拟BFS:
初始:state=0, path=[]
扩展:枚举4个玩具(1,2,3,4)
玩具1:new_state = (0 | 2) & ~1 = 2 & 0b110 = 2 (二进制010) -> 状态2
玩具2:new_state = (0 | 3) & ~4 = 3 (二进制011) -> 状态3
玩具3:new_state = (0 | 4) & ~2 = 4 (二进制100) -> 状态4
玩具4:new_state = (0 | 1) & ~0 = 1 (二进制001) -> 状态1
第一层:四个状态都加入队列(状态2,3,4,1),并标记已访问。
第二层:从第一个状态(状态2)开始,此时path=[1],然后枚举其他玩具(2,3,4):
玩具2:new_state = (2 | 3) & ~4 = (0b010 | 0b011)=0b011,然后& ~0b100=0b011 -> 3(状态3,但已访问,跳过)
玩具3:new_state = (2 | 4) & ~2 = (0b010 | 0b100)=0b110, 然后& ~0b010=0b100(状态4,已访问,跳过)
玩具4:new_state = (2 | 1) & ~0 = 0b010 | 0b001 = 0b011(状态3,已访问,跳过)
接着处理状态3(path=[2]):
枚举其他玩具(1,3,4):
玩具1:new_state = (3 | 2) & ~1 = 3 | 2=3(二进制011),然后& ~1(即~0b001=0b110)-> 0b010(状态2,已访问)
玩具3:new_state = (3 | 4) & ~2 = (0b011 | 0b100)=0b111, 然后& ~0b010=0b101(状态5,未访问,加入队列,path=[2,3])
玩具4:new_state = (3 | 1) & ~0 = 0b011 | 0b001 = 0b011(状态3,已访问,跳过)
处理状态4(path=[3]):
枚举其他玩具(1,2,4):
玩具1:new_state = (4 | 2) & ~1 = (0b100|0b010)=0b110, & ~0b001=0b110(状态6,未访问,加入队列,path=[3,1])
玩具2:new_state = (4 | 3) & ~4 = (0b100|0b011)=0b111, 然后& ~0b100=0b011(状态3,已访问)
玩具4:new_state = (4 | 1) & ~0 = 0b101(状态5,未访问?不对,状态5是0b101,而之前状态3扩展时产生了状态5(0b101)?注意:状态5是0b101,状态6是0b110,状态3是0b011,状态4是0b100,状态1是0b001,状态2是0b010)
处理状态1(path=[4]):
枚举其他玩具(1,2,3):
玩具1:new_state = (1 | 2) & ~1 = (0b001|0b010)=0b011, & ~0b001=0b010(状态2,已访问)
玩具2:new_state = (1 | 3) & ~4 = 0b011(状态3,已访问)
玩具3:new_state = (1 | 4) & ~2 = (0b001|0b100)=0b101, & ~0b010=0b101(状态5,未访问,加入队列,path=[4,3])
第二层队列中还有:状态5(由状态3扩展:path=[2,3])、状态6(由状态4扩展:path=[3,1])、状态5(由状态1扩展:path=[4,3])等。
第三层:从状态5(0b101)开始,它可能由两条路径到达:[2,3]和[4,3]。我们按BFS顺序,先处理队列中先加入的(但注意队列是先进先出,第二层中先加入的是状态2扩展的,然后是状态3扩展的,状态4扩展的,状态1扩展的。所以第三层先处理的是状态2扩展的?不对,第二层我们处理了状态2、状态3、状态4、状态1,然后它们扩展出的新状态在队列中顺序是:状态5(由状态3扩展)、状态6(由状态4扩展)、状态5(由状态1扩展)。所以先处理状态5(path=[2,3]):
state=5(0b101),枚举其他玩具(1,4):
玩具1:new_state = (5 | 2) & ~1 = (0b101 | 0b010)=0b111, 然后& ~0b001=0b110(状态6,已访问?因为之前状态4扩展出了状态6,所以已访问)
玩具4:new_state = (5 | 1) & ~0 = 0b101 | 0b001 = 0b101(状态5,已访问)
所以没有新状态。
然后处理状态6(path=[3,1]):
state=6(0b110),枚举其他玩具(2,4):
玩具2:new_state = (6 | 3) & ~4 = (0b110 | 0b011)=0b111, & ~0b100=0b011(状态3,已访问)
玩具4:new_state = (6 | 1) & ~0 = 0b111(状态7,目标状态!)
所以找到解:path=[3,1,4] -> 输出3 1 4
因此,我们输出3 1 4,与样例一致。
但是,注意:我们枚举玩具时是按编号从小到大枚举,所以状态6扩展时,先枚举玩具2(因为玩具2没在path中?path=[3,1]中已有3和1,所以剩下2和4,先枚举2再枚举4),所以先扩展玩具2(得到状态3,已访问),然后扩展玩具4(得到目标状态)。
因此,代码实现正确。
但是,注意:在扩展一个状态时,我们枚举的玩具是当前未使用的(即不在path中的),并且按编号从小到大枚举。
代码实现:
由于n和m都很小,我们可以用数组实现visited,大小为(1<<n)(最大1024)。
注意:我们使用一个队列,每个元素是(state, path),其中path是一个vector<int>。
但是,在C++中,vector在队列中复制可能会影响性能,但由于状态数最多1024,且路径长度最多10,所以可以接受。
代码:
注意:我们使用visited数组,大小为1<<n,初始化为false。
注意:同一个状态,如果已经访问过,则不再加入队列。
注意:当找到目标状态时,立即输出路径并结束。
由于题目保证有解,所以一定会找到解。
但是,我们是否可能漏掉解?比如,某个状态在第一次被访问时,使用的玩具数最少,但字典序不是最小?不会,因为我们在每一层中,按玩具编号从小到大枚举,所以第一次到达某个状态的路径字典序一定最小。
因此,代码实现如下:
注意:我们使用一个队列,队列中元素为pair<int, vector<int>>,其中int是状态,vector<int>是路径(已使用的玩具序列)。
初始化:将初始状态0和空路径入队,标记visited[0]=true。
然后BFS:
while (!q.empty()) {
auto [state, path] = q.front(); q.pop();
if (state == target) {
// 输出路径
for (int i=0; i<path.size(); i++) {
if (i>0) cout << " ";
cout << path[i];
}
cout << endl;
return 0;
}
// 枚举所有玩具(1~m),且不在path中的
for (int i=1; i<=m; i++) {
if (find(path.begin(), path.end(), i) != path.end())
continue;
int new_state = (state | set_mask[i]) & ~clear_mask[i];
if (!visited[new_state]) {
visited[new_state] = true;
vector<int> new_path = path;
new_path.push_back(i);
q.push({new_state, new_path});
}
}
}
但是,这样枚举玩具时,我们按1~m的顺序,所以是字典序从小到大的顺序。
但是,这个BFS不是按层数递增的顺序?队列是先进先出,但是第一层(玩具数1)先入队,然后第二层(玩具数2)入队,所以队列中玩具数是递增的。因此,当找到目标状态时,玩具数一定最少。
但是,同一层的状态,玩具数相同,但队列中先入队的先出队,所以玩具数1的状态处理完后,再处理玩具数2的状态。因此,我们按玩具数递增的顺序搜索。
但是,在同一层中,我们按玩具编号从小到大的顺序扩展,所以同一玩具数的状态,先扩展玩具编号小的路径,所以字典序小的路径先入队,因此先出队。
因此,当目标状态第一次出现时,其路径玩具数最少,且字典序最小。
然而,我们并没有显式地按层处理(即一层一层地处理),而是直接用一个队列。由于BFS的性质(队列中同一层的状态是连续的),所以没有问题。
但是,注意:同一玩具数的状态,其路径长度相同,而队列中先入队的路径(即字典序小的路径)先出队,所以正确。
但是,我们如何保证同一层中,字典序小的路径先出队?因为我们在扩展时,对于当前状态,我们按玩具编号从小到大的顺序枚举下一个玩具,所以生成的下一层状态(玩具数多1)的路径也是按字典序从小到大的顺序加入队列。而队列是先进先出,所以同一层中,玩具数相同的状态,先入队的是字典序更小的路径。
因此,正确。
但是,注意:在扩展一个状态时,我们按玩具编号从小到大的顺序枚举,所以生成的多个新状态(对应多个新路径)也是按玩具编号从小到大的顺序入队。因此,队列中同一层(玩具数相同)的状态,其路径的字典序也是递增的(因为队列中前面的路径字典序小,后面的大)。所以,当我们处理队列时,先处理字典序小的路径。
因此,代码实现正确。
但是,我们有一个问题:在扩展时,我们枚举了所有未使用的玩具,但未使用的玩具很多,而且我们按编号顺序枚举,所以生成的路径按玩具编号递增。
因此,我们按照上述思路编写代码。
注意:由于状态数最多1024,而每个状态最多扩展m次(m<=10),所以队列中最多有1024个状态,每个状态扩展最多10次,总操作数10240,效率很高。
但是,我们使用vector来存储路径,并且每次扩展时复制整个路径(最多10个元素),所以空间和时间都可以接受。
代码实现:
预处理set_mask和clear_mask数组(大小为m+1,索引1到m)
目标状态:target = (1<<n)-1
初始化visited数组(大小为1<<n,初始为false)
队列:queue<pair<int, vector<int>>>
注意:我们使用STL队列。
但是,由于路径复制,我们也可以使用一个状态数组记录到达该状态的最短路径(但这样更新路径比较麻烦),所以直接使用队列存储路径。
输入样例测试通过。
但是,考虑另一种情况:如果使用玩具的顺序不同,但可能达到相同的状态,而我们的visited数组会阻止后续路径,但我们的路径可能不同(字典序不同),但状态相同,而后续扩展可能不同。但根据我们的分析,第一次到达该状态的路径玩具数最少且字典序最小,所以后续路径可以跳过。
因此,代码实现如下:
注意:我们使用一个visited数组,记录每个状态是否被访问过(一旦访问过,就不再入队)。
但是,如果某个状态在玩具数更多时被访问,那么它一定不是最优解(因为玩具数更少时已经访问过)。所以visited数组可以记录该状态是否被访问过(不论玩具数多少),因为玩具数更少的状态已经访问过,那么玩具数更多的状态就不需要再访问了。
因此,我们只需要一个一维的visited数组。
代码:
注意:在C++中,位运算的优先级:按位或和按位与的优先级低于比较运算符,所以需要加括号。
完整代码:
注意:输入时,猫的个数n,玩具个数m,然后m行,每行n个整数。
注意:我们使用0-indexed的二进制位:第0位对应第一只猫,第1位对应第二只猫,...,第n-1位对应第n只猫。
例如:玩具1的输入:-1 1 0 -> 第一只猫(0位)为-1,第二只猫(1位)为1,第三只猫(2位)为0。
因此,在预处理时,对于第i个玩具(从1开始),读入n个数,第j个数(0-indexed)对应猫j(即二进制第j位)。
具体:
for (int i=1; i<=m; i++) {
for (int j=0; j<n; j++) {
int val;
cin >> val;
if (val == 1) {
set_mask[i] |= (1 << j);
} else if (val == -1) {
clear_mask[i] |= (1 << j);
}
}
}
然后进行BFS。
但是,注意:题目要求输出最小编号序列,且玩具编号从1到M。
我们按照上述BFS,找到的第一个解就是答案。
输出时,按空格分隔,行末无空格。
我们提交代码,注意题目要求代码长度限制16KB,时间400ms,内存64MB。我们的BFS状态数最多1024,队列中元素最多1024个,每个元素存储一个状态和一个最多10个元素的vector,所以空间占用很小(1024*(4+10*4)=1024*44=45KB),在内存限制内。
但是,如果使用vector,复制路径可能会稍微慢一点,但n和m很小,所以没问题。
另外,我们使用一个visited数组,大小为1024,用bool类型。
代码:
注意:在BFS中,我们每次扩展时,要跳过已经在路径中的玩具。我们使用find函数在vector中查找,由于路径长度最多10,所以查找10次,总共操作数10240*10=102400,可以接受。
但是,我们可以用另一种方法:记录路径是为了输出,而在扩展时,我们只需要知道哪些玩具已经使用过。我们可以用一个整数used_mask(位掩码)表示已经使用过的玩具集合(玩具编号1~m,用0~m-1位表示),这样判断玩具i是否使用过:if (used_mask & (1<<(i-1))) 跳过。同时,我们可以将used_mask和state一起作为状态?不行,因为状态state只表示猫的状态,而used_mask表示已经使用过的玩具,那么状态就变成了(state, used_mask),状态数量为(1<<n) * (1<<m) = 1024 * 1024 = 1e6,在m=10时,1024*1024=1e6,也是可以接受的。而且这样我们就可以不用vector存储路径,而是用一个数组记录每个状态的路径(即从哪个状态转移过来,以及使用了什么玩具),然后最后回溯路径。但是这样需要记录路径,实现稍复杂。
为了简单,我们仍然使用vector存储路径,因为m很小。
因此,我们选择使用vector存储路径,并在扩展时使用find判断玩具是否使用过。
但是,我们也可以将路径改为使用整数掩码(表示使用过的玩具集合)和到达当前状态的最后一个玩具,然后记录前驱状态。这样在找到目标状态时,需要递归重建路径。但这样代码复杂,且题目要求输出路径序列,所以我们用vector存储路径。
最终代码:
注意:我们使用C++,包含头文件:<iostream>, <vector>, <queue>, <algorithm>, <cstring>,以及<bits/stdc++.h>(如果允许)。
但题目没有指定编译器,所以使用标准头文件。
由于题目要求输出最小编号序列,且我们按玩具编号从小到大枚举,所以正确。
我们编写代码并测试样例。
但是,注意:样例输入中,玩具3的输入是0 -1 1,我们处理为set_mask[3]=4(即二进制100,对应第三只猫),clear_mask[3]=2(即二进制010,对应第二只猫)。然后玩具1:set_mask=2(二进制010),clear_mask=1(二进制001)。玩具4:set_mask=1(二进制001),clear_mask=0。
测试:初始状态0,使用玩具3:new_state=(0|4)&~2=4(二进制100)-> 状态4(第三只猫高兴)。
然后使用玩具1:new_state=(4|2)&~1= (0b100|0b010)=0b110, 然后&~0b001=0b110(状态6:第二只和第三只猫高兴)。
然后使用玩具4:new_state=(6|1)&~0=0b111(目标状态)。
所以路径为[3,1,4],输出3 1 4。
但是,在队列中,第一层使用玩具3后得到状态4,然后第二层由状态4扩展:使用玩具1得到状态6(路径[3,1]),然后状态6扩展时使用玩具4得到目标状态(路径[3,1,4])。同时,状态4扩展时,还会尝试玩具2和玩具4?不对,在状态4(路径为[3])时,枚举玩具1,2,4(跳过3):
玩具1:得到状态6(未访问,加入队列,路径[3,1])
玩具2:new_state=(4|3)&~4= (0b100|0b011)=0b111, 然后&~0b100=0b011(状态3,已访问?因为第一层使用玩具2得到状态3,所以已访问,跳过)
玩具4:new_state=(4|1)&~0=0b101(状态5,未访问,加入队列,路径[3,4])
所以队列中第二层有:[3,1](状态6)和[3,4](状态5)。然后第三层先处理[3,1](状态6),然后扩展玩具4得到目标状态,输出[3,1,4]。而[3,4]在第三层扩展时,可能得到解,但玩具数相同,但字典序[3,1,4]比[3,4,?]小(因为1<4),所以先输出[3,1,4]就结束了。
因此,正确。
但是,我们还需要考虑:是否存在玩具数更少的解?BFS会先找到玩具数为3的解,而不会去检查玩具数为2的解?因为BFS是按层进行的,第一层(玩具数1)没有解,第二层(玩具数2)也没有解(我们在第二层没有达到目标状态),所以第三层才找到解。因此,玩具数3是最少的。
因此,我们按照上述思路编写代码。
但是,注意:在第二层中,状态6([3,1])还没有达到目标状态,所以不会输出,继续扩展第三层。
代码实现:
注意:我们使用标准BFS,队列中存储状态和路径。
由于状态数最多1024,所以不会超时。
提交代码。
但是,原题中m<=10,n<=10,所以我们的BFS可以接受。
但是,如果m=10,状态数1024,每个状态最多扩展10次,总操作数10240,而每次扩展中,查找未使用的玩具需要遍历当前路径(最多10个元素),所以总操作数102400,时间足够。
因此,我们编写代码。
注意:输出格式,行末无空格。
代码:
由于题目要求代码长度限制16KB,所以我们的代码要尽量简洁。
我们使用bits/stdc++.h头文件。
但是,为了规范,也可以使用具体头文件。
由于题目没有指定,我们使用bits/stdc++.h。
代码:
注意:在BFS中,我们使用一个队列,队列元素为pair<int, vector<int>>。
另外,使用visited数组。
最后,如果找到解,输出并退出;如果队列空还没找到,则无解(但题目保证有解)。
测试样例。
但是,我们考虑一个边界:n=0或m=0的情况?题目保证有解,且n>=1,m>=1。
因此,代码:
注意:数组大小:visited数组开1<<10(1024)即可。
set_mask和clear_mask数组开11(索引0到10,我们使用1到10)。
代码:
由于题目输入样例为3 4,所以n=3,m=4。
我们运行样例,确保输出3 1 4。
如果样例通过,则提交。
但是,我们之前模拟的样例中,玩具3的编号是3,玩具1的编号是1,玩具4的编号是4,所以路径[3,1,4]输出为3 1 4。
因此,我们编写代码。
注意:在枚举玩具时,从1到m,按顺序。
注意:在BFS中,队列是先进先出,所以先入队的先出队。
代码实现:
初始化队列:queue<pair<int, vector<int>>> q;
q.push({0, vector<int>()});
然后循环直到队列为空。
但是,我们担心vector的复制开销,但m最大10,所以可以接受。
完整代码:
注意:使用C++11,因为使用了auto。
如果OJ不支持C++11,我们可以显式声明队列元素类型。
例如:queue<pair<int, vector<int> > > q;
然后,在取出元素时:
int state = q.front().first;
vector<int> path = q.front().second;
q.pop();
然后扩展。
代码:
由于题目要求,我们使用以下头文件:
#include<bits/stdc++.h>
using namespace std;
然后定义数组。
但是,我们也可以不用bits/stdc++.h,而用:
#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
#include<cstring>
为了保险,我们使用具体头文件。
最终代码:
注意:在BFS中,我们使用visited数组,大小为1<<10(n<=10,所以1<<10=1024)。
另外,set_mask和clear_mask数组大小为11(m<=10,索引1~10)。
代码:
注意:输入顺序:先读n和m,然后循环m行,每行n个整数。
注意:在预处理时,对于每个玩具,我们按列读入(每行有n个整数,对应n只猫)。
注意:二进制位:第0位对应第一只猫(输入的第一个数),第1位对应第二只猫,等等。
因此,在循环中,对于第i个玩具(从1开始),读入n个数,第j个数(j从0到n-1)对应猫j。
代码:
我们写完整代码,并测试样例。
测试样例:
输入:
3 4
-1 1 0
1 1 -1
0 -1 1
1 0 0
输出:3 1 4
代码:
注意:在扩展时,我们按玩具编号从小到大枚举(i从1到m),所以对于每个状态,我们枚举的玩具编号是递增的,因此生成的路径字典序递增。
但是,我们枚举玩具时,跳过了已经使用过的玩具(在path中的),所以需要判断。
我们使用find函数:if (find(path.begin(), path.end(), i) != path.end()) continue;
代码:
由于n和m很小,所以直接使用。
但是,为了避免重复查找,我们可以用空间换时间:在状态中记录一个used_mask(表示哪些玩具已经使用),但这样状态就变成(state, used_mask),状态数量为1024*1024(因为m<=10,所以used_mask有2^10=1024种),这样状态总数是1024*1024=1e6,也可以接受。而且这样查找下一个玩具时,只需要检查used_mask的位即可,不需要遍历vector。
但是,这样我们就需要存储used_mask,并且要记录路径(因为输出需要玩具序列)。我们可以额外存储一个数组pre[],记录每个状态的前驱状态和使用的玩具,然后最后重建路径。这样空间较大,且实现复杂。
因此,我们选择用vector存储路径,因为m很小。
最终代码:</think>### BFS + 状态压缩优化方案
#### 超时原因分析
原代码使用全排列枚举(`next_permutation`)导致时间复杂度为 O(M! × M × N),在 M=10 时达到 3,628,800 种排列,远超 400ms 限制。
#### 优化方案:BFS + 状态压缩 + 哈希判重
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <unordered_map>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> set_mask(m+1, 0), clear_mask(m+1, 0);
// 预处理:计算每个玩具的位掩码
for (int i = 1; i <= m; i++) {
for (int j = 0; j < n; j++) {
int val;
cin >> val;
if (val == 1) set_mask[i] |= (1 << j);
else if (val == -1) clear_mask[i] |= (1 << j);
}
}
const int target = (1 << n) - 1; // 目标状态:所有猫高兴
queue<pair<int, vector<int>>> q; // 状态+路径
vector<bool> visited(1 << n, false); // 状态访问标记
// BFS初始化
q.push({0, {}});
visited[0] = true;
while (!q.empty()) {
auto [state, path] = q.front();
q.pop();
// 找到目标状态
if (state == target) {
for (int i = 0; i < path.size(); i++) {
if (i > 0) cout << " ";
cout << path[i];
}
cout << endl;
return 0;
}
// 枚举所有玩具
for (int i = 1; i <= m; i++) {
// 跳过已使用的玩具
if (find(path.begin(), path.end(), i) != path.end())
continue;
// 状态转移:位运算更新猫状态
int new_state = (state | set_mask[i]) & ~clear_mask[i];
// 新状态未访问过
if (!visited[new_state]) {
visited[new_state] = true;
vector<int> new_path = path;
new_path.push_back(i);
q.push({new_state, new_path});
}
}
}
return 0; // 题目保证有解,此处不会执行
}
```
#### 优化核心原理
1. **状态压缩**:
- 使用整数二进制位表示猫状态(0=生气,1=高兴)
- 初始状态:`0`(全生气)
- 目标状态:`(1<<n)-1`(全高兴)
2. **位运算高效转移**:
```cpp
new_state = (state | set_mask[i]) & ~clear_mask[i];
```
- `set_mask`:设置高兴位(输入1)
- `clear_mask`:清除高兴位(输入-1)
- 输入0时不影响原状态
3. **BFS特性保证最优**:
- 按玩具数量分层搜索(最少玩具优先)
- 字典序最小:按编号1~m顺序枚举
- 哈希判重:`visited`数组避免重复状态
4. **复杂度对比**:
| 指标 | 原方案 | 优化后方案 |
|------------|---------------------|---------------------|
| 时间复杂度 | O(M! × M × N) | O(2^N × M × N) |
| M=10计算量 | ≈3.6e8 | 1024×10×3=30,720 |
| 空间复杂度 | O(M!) | O(2^N) |
#### 样例解析(输入样例)
```plaintext
3 4
-1 1 0 -> 玩具1: set_mask=010, clear_mask=001
1 1 -1 -> 玩具2: set_mask=011, clear_mask=100
0 -1 1 -> 玩具3: set_mask=100, clear_mask=010
1 0 0 -> 玩具4: set_mask=001, clear_mask=000
```
- BFS执行过程:
1. 初始状态:`000`
2. 第一层扩展:
- 玩具3:`(000|100)&~010=100` → 状态4
- 玩具1:`(000|010)&~001=010` → 状态2
- 玩具2:`(000|011)&~100=011` → 状态3
- 玩具4:`(000|001)&~000=001` → 状态1
3. 第二层扩展(以状态4`100`为例):
- 玩具1:`(100|010)&~001=110` → 状态6(新状态)
- 玩具4:`(100|001)&~000=101` → 状态5(新状态)
4. 第三层扩展(状态6`110`):
- 玩具4:`(110|001)&~000=111` → 目标状态
- 输出路径:`[3,1,4]`
#### 为什么不用DFS?
- BFS天然满足**最少玩具优先**,而DFS可能先找到非最优解
- 状态空间仅1024种,BFS队列空间可控
- 字典序处理:BFS按编号顺序扩展保证最小字典序