<think>
我们有两个输入样例:
输入样例1:
3 3
0 2 1
输出样例1: 3
输入样例2:
3 5
0 0 0 0 0
输出样例2: 1 2
问题描述:有N个学生(编号从0到N-1),进行M次报号。每次报号可能报0到N-1之间的数(包括0),也可能报到无效数字(如负数或大于等于N的数,但样例中只出现了0和有效数字)。需要输出未报到的学生编号(按升序输出,用空格分隔),但如果所有学生都报到了,则输出N(注意不是输出所有学生编号,而是输出学生总数N)。
但是,注意输入样例2:N=3,M=5,报了5次0(注意0是有效编号,代表学生0)。那么实际报到的学生只有0号学生,因此未报到的是1号和2号,输出“1 2”。
而输入样例1:N=3,M=3,报了0、2、1。这三个都是有效编号,且覆盖了0、1、2(即三个学生都报到了),所以输出3(因为N=3)。
然而,在输入样例1中,报到的编号是0,2,1,对应学生0、1、2(注意编号0对应第一个学生,编号1对应第二个学生,编号2对应第三个学生)。所以全部学生都报到了,输出3(即学生总数)。
所以算法步骤:
1. 输入N和M。
2. 创建一个大小为N的布尔数组(或整数数组)arrived,初始值都为false(表示未报到)。
3. 接下来M次输入,每次输入一个数字x。
- 如果x在[0, N-1]范围内,则将arrived[x]标记为true(表示该学生已报到)。
- 如果x不在该范围内,则忽略(因为无效)。
4. 遍历arrived数组(从0到N-1),统计未报到的学生编号(即arrived[i]为false的i)。
5. 如果没有未报到的学生(即全部报到),则输出N(注意是数字N,不是输出所有学生编号)。
6. 否则,将未报到的学生编号按升序输出(注意:题目要求编号从0开始,但输出时我们输出的是实际编号,但注意样例输出中,输出的是1,2,而不是0,1?)
这里有一个关键点:题目中的学生编号是0~N-1,但输出样例2的输出是“1 2”,而不是“0 1 2”或者“1 2 3”?注意,输出样例2中N=3,未报到的学生是1和2,说明编号0的学生报到了(因为输入了0),而编号1和2没有报到。
但是,输出时我们输出的是编号本身吗?注意,编号0对应的是第一个学生,编号1对应第二个,编号2对应第三个。输出时,我们输出的是编号值,所以应该是0,1,2这些数字。
然而,输出样例1输出的是3(当全部报到时),输出样例2输出的是1和2(注意这里没有0,因为0报到了)。那么为什么输出样例2输出的不是0,1,2?因为0报到了,所以未报到的只有1和2。
所以输出的是未报到的学生编号(即0~N-1中的某些数字)。但是,在输出样例2中,输出的是“1 2”,而不是“0 1”或“0 2”等,因为0报到了,所以未报到的是1和2。
那么为什么输出样例1输出的是3?因为题目要求:如果所有学生都报到了,则输出学生总数(即N)。注意,这里输出3,而不是输出0,1,2(因为全部报到了,所以没有未报到的学生,但题目要求此时输出N)。
因此,代码逻辑如下:
但是,注意输入样例2:输入了5个0,0是有效编号,所以0号学生被标记为报到(多次报0,但只需标记一次即可,重复报同一个有效编号不影响结果)。
所以,我们只需要一个布尔数组记录每个学生是否报到过。
然而,问题中提到“小朋友代码中0~n-1编号且0为有效报号”,所以说明编号0是有效的,代表第一个学生。
现在,我们考虑如何实现:
步骤:
1. 读入N, M。
2. 定义布尔数组arrived,大小为N,初始为false。
3. 循环M次,每次读入一个数x:
如果x>=0且x<N,则arrived[x]=true。
4. 然后,我们检查arrived数组,记录未报到的学生编号(即arrived[i]为false的i)。
5. 如果未报到学生数为0(即所有学生都报到了),则输出N(注意是整数N)。
6. 否则,按顺序输出未报到的学生编号(升序,因为编号0~N-1本来就是升序的,我们按索引从小到大输出即可)。
但是,输出格式:如果有多个未报到的学生,则用空格分隔。注意第一个输出前面不要有空格,最后一个输出后面也不要有多余空格。
然而,输出样例1:全部报到,输出3(一个整数)。
输出样例2:未报到的是1和2,输出“1 2”(注意中间空格,没有其他字符)。
注意:编号0在输出时就是0,编号1就是1,编号2就是2。但是输出样例2中输出的是1和2,所以就是数字本身。
但是,为什么输出样例1输出3?因为题目要求全部报到时输出学生总数(即N=3)。
因此,我们可以这样写代码:
伪代码:
int N, M;
cin >> N >> M;
vector<bool> arrived(N, false); // 或者使用动态数组
for (int i=0; i<M; i++) {
int x;
cin >> x;
if (x>=0 && x<N) {
arrived[x] = true;
}
}
int count = 0;
for (int i=0; i<N; i++) {
if (!arrived[i]) {
count++;
}
}
if (count == 0) {
cout << N;
} else {
// 需要输出所有未报到的学生编号
bool first = true;
for (int i=0; i<N; i++) {
if (!arrived[i]) {
if (first) {
cout << i;
first = false;
} else {
cout << " " << i;
}
}
}
}
但是,我们也可以不用count,而是先遍历一遍看是否有未报到的,然后再输出。或者直接输出,但这样就要在输出时判断是否是第一次,以保证空格。
然而,上面的代码在输出样例1:N=3,M=3,输入0,2,1(注意顺序无所谓),那么arrived[0]=true, arrived[1]=true, arrived[2]=true,所以count=0,输出3。
输出样例2:N=3,M=5,输入5个0,那么arrived[0]=true,arrived[1]和arrived[2]为false。所以count=2,然后输出1和2(注意,i=1时输出"1",然后i=2时输出" 2")。
但是,注意:我们的循环是从0开始,所以第一个未报到的是1(因为0已经报到),然后第二个是2。
所以输出样例2是“1 2”,符合要求。
但是,这里有一个问题:题目中的学生编号是0~N-1,而输出时,我们直接输出数字,所以0号学生就是0。但是在输出样例2中,输出的是1和2,而不是0和1?为什么没有0?因为0报到了,所以未报到的是1和2,所以输出1和2。
因此,代码逻辑正确。
但是,为什么在输出样例2中,输入的是5个0,而0是有效编号,所以0号学生被标记为报到,而1号和2号没有。
所以,我们输出的未报到学生就是1和2。
因此,我们按照上述逻辑编写代码即可。
注意:题目中M次报号,可能包含无效数字(如负数或大于等于N的数),我们忽略即可。
但是,注意输入样例1中的第一个报号是0,第二个是2,第三个是1,都是有效数字,所以全部报到。
现在,考虑另一个问题:如果输入的数字有重复的有效数字,比如输入两个0,那么0号学生还是只报到一次,所以重复不影响。
所以,我们只需要标记,而不需要计数报到次数。
因此,上述代码可以满足要求。
但是,注意题目中输出样例2的输出是“1 2”,中间有空格,且没有换行。
我们按照上述代码,输出样例1:输出3(不带空格和换行)
输出样例2:输出“1 2”(不带换行,但题目样例输出也没有换行)
所以,我们按照上述代码实现即可。
但是,注意题目中的输入样例1和2,以及输出格式。
下面给出C++代码(使用vector):
如果使用数组,也可以,但要注意动态分配。
注意:N最大10的6次方,所以用vector<bool>比较节省空间。
代码实现:
</think>
### 问题分析
根据输入输出样例,小朋友代码中涉及的核心算法问题是:**处理0~n-1编号系统中的学生报到状态统计**。关键特征包括:
1. **0是有效编号**:0代表第一个学生(而非无效标记)
2. **报到机制**:M次报号中,有效的报号范围是[0, n-1]
3. **输出规则**:
- 未报到学生:升序输出其原始编号
- 全部报到:输出学生总数n
### 算法逻辑解析
用布尔数组标记学生报到状态:
```cpp
bool arrived[n]; // 索引0~n-1对应学生编号
for (每个报号x) {
if (x >= 0 && x < n) arrived[x] = true; // 标记有效报号
}
```
#### 状态判定与输出
```mermaid
graph TD
A[开始] --> B{所有arrived[i]==true?}
B -->|是| C[输出n]
B -->|否| D[升序输出未报到编号]
```
#### 输入输出样例验证
1. **样例1**:`3 3\n0 2 1`
- 报号覆盖0,1,2 → 全部报到 → 输出总数`3`
- 验证:$ \forall i \in [0,2], arrived[i]=true $
2. **样例2**:`3 5\n0 0 0 0 0`
- 仅0号报到 → 未报到:1,2 → 输出`1 2`
- 验证:$ arrived[0]=true,\ arrived[1]=false,\ arrived[2]=false $
### 潜在问题分析
1. **编号范围混淆**:
- 错误假设:0是无效编号(实际0代表第一个学生)
- 正确认知:有效编号空间$ S=\{x \mid 0 \leq x \leq n-1\} $
2. **边界处理缺陷**:
```cpp
// 错误代码:忽略0号学生
if (x > 0) arrived[x-1] = true; // 会导致0永远不被标记
```
3. **重复报号处理**:
- 多次报同一有效编号不影响结果(布尔数组自动去重)
- 时间复杂度:$ O(n+m) $,空间复杂度:$ O(n) $
### 正确实现代码
```cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<bool> arrived(n, false); // 初始化n个false
// 处理m次报号
while (m--) {
int x;
cin >> x;
if (x >= 0 && x < n) arrived[x] = true; // 标记有效报号
}
// 检查未报到学生
bool all_arrived = true;
for (int i = 0; i < n; ++i) {
if (!arrived[i]) {
if (!all_arrived) cout << " "; // 非首个输出加空格
cout << i;
all_arrived = false;
}
}
// 全部报到则输出n
if (all_arrived) cout << n;
return 0;
}
```
### 关键测试用例
| 输入 | 预期输出 | 说明 |
|-----------------------|----------|-------------------------------|
| `4 2`<br>`0 3` | `1 2` | 中间编号未报到 |
| `3 1`<br>`5` | `0 1 2` | 无效报号被忽略 |
| `2 3`<br>`0 1 0` | `2` | 重复报号不影响,全部报到 |
| `1 1`<br>`0` | `1` | 唯一学生报到,输出总数 |
| `3 0` | `0 1 2` | 无有效报号,全部未报到 |
### 相关问题思考
1. 当$ n=10^6 $且$ m=10^6 $时,如何优化空间复杂度至$ O(1) $?
- 提示:用位图代替布尔数组[^4]
2. 如果报号包含负数或超大整数($ |x| \leq 10^9 $),当前验证逻辑`if (x>=0 && x<n)`是否高效?
- 分析:比较操作$ O(1) $,不影响整体复杂度$ O(n+m) $[^1]
3. 若要求输出未报到学生的**连续区间**(如`1-3,5`而非`1 2 3 5`),算法应如何修改?
- 思路:遍历时合并连续未报到区间
4. 为什么全部报到时要输出$ n $而非$ 0 $?
- 语义区分:输出$ n $表示"全部$ n $人到齐",输出$ 0 $易误解为"0号未到"[^2]
5. 如果学生编号改为1~n(0无效),输出未报到学生时是否需`+1`?
- 关键:需保持编号系统一致性,输入输出使用同一基准[^3]