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.

8.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 4089. 小熊的果篮

一、题目描述

小熊的水果店里摆放着一排 n 个水果。

每个水果只可能是苹果或桔子,从左到右依次用正整数 123、……、n 编号。

连续排在一起的同一种水果称为一个

小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把 每一个 最左边 的水果同时挑出,组成一个果篮。

重复这一操作,直至水果用完。

注意,每次挑完一个果篮后, 可能会发生变化。

比如两个苹果 之间的唯一桔子被挑走后,两个苹果 就变成了一个

请帮小熊计算每个果篮里包含的水果。

输入格式 输入的第一行包含一个正整数 n,表示水果的数量。

输入的第二行包含 n 个空格分隔的整数,其中第 i 个数表示编号为 i 的水果的种类,1 代表苹果,0 代表桔子。

输出格式 输出若干行。

i 行表示第 i 次挑出的水果组成的果篮。

从小到大排序输出该果篮中所有水果的编号,每两个编号之间用一个空格分隔。

数据范围 对于 10% 的数据,n5。对于 30% 的数据,n1000。对于 70% 的数据,n50000。对于 100% 的数据,n2×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--。至于用啥保存,这里用的是队列。

为啥用队列呢?因为 队列不空就一直处理,先进来的先处理 等特点决定,也可以认为是一种经验,这个仔细体会吧,类比下 边长345的考虑是直角三角形 ~。

把整理好的结构体放入队列后怎么处理呢?

当然是逐个弹出队列,然后去掉头,如果去完头还有剩余,需要留下来,如果留下来后与其它的块去头后可以连通,那么需要合并块。

去头好办,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;
}

五、细节