8.9 KiB
AcWing
4089
. 小熊的果篮
一、题目描述
小熊的水果店里摆放着一排 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:
12
1 1 0 0 1 1 1 0 1 1 0 0
输出样例1:
1 3 5 8 9 11
2 4 6 12
7
10
输入样例2:
20
1 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 0 0
输出样例2:
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
还有吗?好像没有了,那么就用结构体吧:
struct Node { // 块
int st, ed, th;
};
那用了结构体,是不是需要把输入的数据样例转化为这样的形式并且保存下来呢? 也就是从头开始读取,获取一下当前的数字是啥,然后一直向后走,发现与当前的数字不一样了,就表示换了一个新块,这时,把走完的块记录下来就行了。
问题来了,如果我们将数据读取到一个数据a[]
中,默认的值是0
,如果我们从头开始到最后结束,用上面的逻辑判断是不是完成了一个新块,那么,在边界上会不会有问题出现呢?
比如最后一个块,假设它的值是0,那么,由于 a[]
数组的默认值是0
,而且,数组一般的处理办法是多开几个,那会不会在编码时造成程序不知道是真的是输入的0
,还是数组默认的0
呢?这时如果不处理,就会出问题。如果加上判断,当然也可以解决问题,但更普遍的办法是加上-1
的默认值,这样,就相当于n+1
的位置上加上了一个 哨兵 !使得所有数字的处理逻辑就一样了,代码更短了,不用特判了!
memset(a, -1, sizeof a);
for (int i = 1; i <= n; i++) cin >> a[i];
那么,如何把块保存下来呢?保存下来放到哪里呢?因为按小熊这么干,它最后肯定会把所有的块合并成一个块,然后不断的取走这个块的第1
个,同时,由于是每次取走1
个,所以,它一共需要取n
次。看来循环n
次是没跑了
while (n)
至于啥时候n--
,就看是不是找到了一个块的开始位置,然后输出这个开始位置后才能n--
。至于用啥保存,这里用的是队列。
为啥用队列呢?因为 队列不空就一直处理,先进来的先处理 等特点决定,也可以认为是一种经验,这个仔细体会吧,类比下 边长3,4,5
的考虑是直角三角形 ~。
把整理好的结构体放入队列后怎么处理呢?
当然是逐个弹出队列,然后去掉头,如果去完头还有剩余,需要留下来,如果留下来后与其它的块去头后可以连通,那么需要合并块。
去头好办,k.st++
就行,如果没有剩余也好办:if(k.st>k.ed)
就是没有剩余了
下面就遇到了本题的困难点: 两头大,中间小!
这样的测试用例,如果取一次,那么中间0
的黄色区域就没有了!而新的区间就是B \sim I
!
这就有问题了!啥问题呢?因为如果你这么记录B \sim I
,你就把一个无用的位置E
包含进去了,而它却已经不能再取了!!
所以,需要一个used[N]
来记录哪个位置是不是用过了,然后遇到时需要跳过它!
那重复整理合并块该怎么做呢?怎么循环啊?
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 里
}
每个块,都向后尽量多的找出与自己数字一样的块,然后合并到,放回到第一个队列中去。
四、实现代码
#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;
}