You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
8.9 KiB

2 years ago
## [$AcWing$ $4089$. 小熊的果篮](https://www.acwing.com/problem/content/4092/)
### 一、题目描述
小熊的水果店里摆放着一排 $n$ 个水果。
每个水果只可能是苹果或桔子,从左到右依次用正整数 $1$、$2$、$3$、……、$n$ 编号。
连续排在一起的同一种水果称为一个 **块**。
小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把 **每一个** **块** 中 **最左边** 的水果同时挑出,组成一个果篮。
重复这一操作,直至水果用完。
注意,每次挑完一个果篮后,**块** 可能会发生变化。
比如两个苹果 **块** 之间的唯一桔子被挑走后,两个苹果 **块** 就变成了一个 **块**。
请帮小熊计算每个果篮里包含的水果。
**输入格式**
输入的第一行包含一个正整数 $n$,表示水果的数量。
输入的第二行包含 $n$ 个空格分隔的整数,其中第 $i$ 个数表示编号为 $i$ 的水果的种类,$1$ 代表苹果,$0$ 代表桔子。
**输出格式**
输出若干行。
第 $i$ 行表示第 $i$ 次挑出的水果组成的果篮。
从小到大排序输出该果篮中所有水果的编号,每两个编号之间用一个空格分隔。
**数据范围**
对于 $10$% 的数据,$n$≤$5$。对于 $30$% 的数据,$n$≤$1000$。对于 $70$%
的数据,$n$≤$50000$。对于 $100$% 的数据,$n$≤$2$×$105$。
**输入样例**1
```cpp {.line-numbers}
12
1 1 0 0 1 1 1 0 1 1 0 0
```
**输出样例**1
```cpp {.line-numbers}
1 3 5 8 9 11
2 4 6 12
7
10
```
**输入样例**2
```cpp {.line-numbers}
20
1 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 0 0
```
**输出样例**2
```cpp {.line-numbers}
1 5 8 11 13 14 15 17
2 6 9 12 16 18
3 7 10 19
4 20
```
### 二、解题流程
#### 1、读题
- ① 仔细读题,提取关键信息
- ② 查看测试用例,理解题意
- ③ 构造 **边界** 测试数据,深入理解题意
#### 2、框架
-**正向思考** 主体流程
- ② 会用到哪些知识点
- ③ 辅助函数有哪些
- ④ 以伪代码形式把上面的$3$点写下来
#### 3、编码
- ① 主框架、输入输出完成代码编写,核心部分、调用子函数部分用注释占位
- ② 对每个子函数完成代码
#### 4、测试
- ① 在$IDE$中完成给定测试用例的测试
- ② 完成自已构造测试用例的测试
### 三、本题思路
依照上面的 解题流程,思考本题:
1、块的概念是核心如果我们能维护好这个块是不是就能搞定本题呢
2、块是一连串的数字可能是连续的$1$,或者连续的$0$。
3、首先这些连续的$1$或者连续的$0$,我们该怎么维护下来的?不维护下来,后面的工作肯定没法做,用什么样的数据结构呢?
要想一下我们维护的是啥,有啥核心的信息需要记录:
① 块的开始位置
② 块的结束位置
③ 块里装的是啥数字,是$1$还是$0$
还有吗?好像没有了,那么就用结构体吧:
```cpp {.line-numbers}
struct Node { // 块
int st, ed, th;
};
```
那用了结构体,是不是需要把输入的数据样例转化为这样的形式并且保存下来呢?
也就是从头开始读取,获取一下当前的数字是啥,然后一直向后走,发现与当前的数字不一样了,就表示换了一个新块,这时,把走完的块记录下来就行了。
问题来了,如果我们将数据读取到一个数据$a[]$中,默认的值是$0$,如果我们从头开始到最后结束,用上面的逻辑判断是不是完成了一个新块,那么,在边界上会不会有问题出现呢?
比如最后一个块假设它的值是0那么由于 $a[]$数组的默认值是$0$,而且,数组一般的处理办法是多开几个,那会不会在编码时造成程序不知道是真的是输入的$0$,还是数组默认的$0$呢?这时如果不处理,就会出问题。如果加上判断,当然也可以解决问题,但更普遍的办法是加上$-1$的默认值,这样,就相当于$n+1$的位置上加上了一个 **哨兵** !使得所有数字的处理逻辑就一样了,代码更短了,不用特判了!
```cpp {.line-numbers}
memset(a, -1, sizeof a);
for (int i = 1; i <= n; i++) cin >> a[i];
```
那么,如何把块保存下来呢?保存下来放到哪里呢?因为按小熊这么干,它最后肯定会把所有的块合并成一个块,然后不断的取走这个块的第$1$个,同时,由于是每次取走$1$个,所以,它一共需要取$n$次。看来循环$n$次是没跑了
```cpp {.line-numbers}
while (n)
```
至于啥时候$n--$,就看是不是找到了一个块的开始位置,然后输出这个开始位置后才能$n--$。至于用啥保存,这里用的是队列。
为啥用队列呢?因为 **队列不空就一直处理,先进来的先处理** 等特点决定,也可以认为是一种经验,这个仔细体会吧,类比下 **边长$345$的考虑是直角三角形** ~。
把整理好的结构体放入队列后怎么处理呢?
当然是逐个弹出队列,然后去掉头,如果去完头还有剩余,需要留下来,如果留下来后与其它的块去头后可以连通,那么需要合并块。
去头好办,`k.st++`就行,如果没有剩余也好办:`if(k.st>k.ed)`就是没有剩余了
下面就遇到了本题的困难点:
两头大,中间小!
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages//202310201050987.png)
这样的测试用例,如果取一次,那么中间$0$的黄色区域就没有了!而新的区间就是$B \sim I$
这就有问题了!啥问题呢?因为如果你这么记录$B \sim I$,你就把一个无用的位置$E$包含进去了,而它却已经不能再取了!!
所以,需要一个`used[N]`来记录哪个位置是不是用过了,然后遇到时需要跳过它!
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202310201052674.png)
那重复整理合并块该怎么做呢?怎么循环啊?
```cpp {.line-numbers}
while (q2.size()) {
Node k = q2.front();
q2.pop();
while (q2.size()) {
Node x = q2.front();
if (x.th == k.th) { // 能合并就合并
k.ed = x.ed;
q2.pop();
} else
break;
}
q1.push(k); // 丢回 q1 里
}
```
每个块,都向后尽量多的找出与自己数字一样的块,然后合并到,放回到第一个队列中去。
### 四、实现代码
```cpp {.line-numbers}
#pragma GCC optimize(2) // 累了就吸点氧吧~
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
struct Node { // 块
int st, ed, th;
};
int n, a[N];
queue<Node> q1, q2;
bool used[N]; // 记录是否被取出
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n;
memset(a, -1, sizeof a);
for (int i = 1; i <= n; i++) cin >> a[i];
// 很经典的代码
for (int i = 2, x = 1; i <= n; i++)
if (a[i] != a[i + 1])
q1.push({x, i, a[i]}), x = i + 1; // 把连续一段相同的元素合成一个块
// 取没拉倒
while (n) {
while (q1.size()) { // 有块存在
Node k = q1.front(); // k这个块
q1.pop();
// 1111 00 1111 :取两轮后第1块和第3块就会连上
while (used[k.st]) k.st++; // 跳过块中已取过的水果
if (k.st > k.ed) continue; // 取没了,这个块就放过,看看下一个块吧
printf("%d ", k.st); // 如果没有取没,那就取这个号的水果
n--; // 取走一个水果
used[k.st] = 1; // 标识已取出
k.st++; // 不是最后一个,取完后块的开始位置
if (k.st > k.ed) continue; // 取没了,这个块就放过,看看下一个块吧
q2.push(k); // 先将缩减的小块存到 q2 里,因为可能会有些区间连上了,需要进行合并
}
puts("");
// 1111 00 1111 :取两轮后第1块和第3块就会连上
// 完成合并任务
while (q2.size()) {
Node k = q2.front();
q2.pop();
while (q2.size()) {
Node x = q2.front();
if (x.th == k.th) { // 能合并就合并
k.ed = x.ed;
q2.pop();
} else
break;
}
q1.push(k); // 丢回 q1 里
}
}
return 0;
}
```
### 五、细节
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310171602940.png)