## [$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--$。至于用啥保存,这里用的是队列。 为啥用队列呢?因为 **队列不空就一直处理,先进来的先处理** 等特点决定,也可以认为是一种经验,这个仔细体会吧,类比下 **边长$3,4,5$的考虑是直角三角形** ~。 把整理好的结构体放入队列后怎么处理呢? 当然是逐个弹出队列,然后去掉头,如果去完头还有剩余,需要留下来,如果留下来后与其它的块去头后可以连通,那么需要合并块。 去头好办,`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 using namespace std; const int N = 200010; struct Node { // 块 int st, ed, th; }; int n, a[N]; queue 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)