【题解】牛牛的和平年代

题意

我们定义一个整数可重集合是好的,当且仅当对于集合中任意两个元素 ,所有满足 的元素 c 都在集合中出现过。
现在,给你一个数组 mSet,你需要做的是,对于这个数组的每一个前缀,判断这个前缀是不是一个好的集合。所以,你将计算出的是一个数组,为布尔类型。

题解

题目到底想让我干啥?

在做这道题目之前,我们需要先转化一下题意。如同标题所说,这个好的集合,也就是类似一种元素连续的集合。任意两个元素之间都会被其他元素占满,说的就是在最大元素和最小元素之间的所有值,都可以在集合中取到

我会暴力!

这简单,判断一个集合是不是好的简直太简单了。
因为在一个集合中元素的顺序是不重要的,我们可以随意打乱元素的顺序,那么按照递增或者递减的顺序来排列的元素一定是更有利于我们判断元素的相对大小的。
在一个已经排好序的集合里,这个集合现在可以说是一个列表了。那么这个列标显然有一个性质,那就是对于一个元素,排在左边的一定是不大于它的,排在右边的一定是不小于它的
基于这个基本的认识,我们可以加以推广,来定义这个连续集合的概念。
一、单一元素的集合是连续的集合。
二、如果两个连续的集合是相邻的,那么整体也连续
所以我们也就因此找到了判断一个集合整体是否连续的方法,那就是在排序之后,任意在数组中相邻的元素的差值都不会超过1,也就是说他们之间是紧邻的,不可以插入另外的元素。

基于这个事实,我们就可以来编写我们的暴力程序了。我们将这个数组的每一个前缀都排好序,然后暴力扫描一遍,判断位置相邻的元素是不是值也相邻就好了。代码如下:

class Solution {
public:
    /**
     * 检查数组的每个前缀是不是一个好的集合
     * @param mSet int整型vector 
     * @return bool布尔型vector
     */
    vector<bool> continuousSet(vector<int>& mSet) {
        // write code here
        int n = mSet.size(), *tmp = new int[n];
        vector<bool>ans;
        for (int i = 0; i < n; i++) {
            tmp[i] = mSet[i];
            sort(tmp, tmp + i + 1);
            bool ok = true;
            for (int j = 0; j < i; j++)
                if (tmp[j] + 1 < tmp[j + 1])
                    ok = false;
            ans.push_back(ok);
        }
        return ans;
    }
};

当然,这样的代码时不可以通过这道题目的。因为在最坏情况下,我们需要对于每一个前缀从头到尾都扫描一遍,甚至排序一次。假设数组大小为 ,那么我们这样的时间复杂度就是 的,需要额外 的空间。虽然我们可以做一些小小的优化,比如把每次重新排序改成向有序数组中插入一个元素的插入排序,可以把时间复杂度降低到 ,但是依旧不可以通过此题。

你当我不会平衡树?

回想一下我们刚刚的需求,我们需要在每次插入新的元素之后,依旧获取一个有序的数组。为了达到这样的需求,我们直接弄一棵平衡树来维护这个排序数组,它不香么?更何况系统中的map,set等容器,全部都是以平衡树作为底层实现的东西,我们直接套用过来不就好了?

嗯,所以我们只需要把给前缀排序,修改成把数组的前缀元素,依次插入,形成一个前缀的平衡树就好了。但是我么你需要如何判断这个平衡树中的相邻元素都是相邻的呢?这时可能需要引入另外一个概念了。我们统计这个有序数组断开的地方的数量,如果断开的数量变成了0,那么我们就可以认为这个集合时连续的

而做到这件事情,是非常容易的。考虑到每次只会新增加一个元素,那么我们只需要看看这个元素的添加会不会修改断开的地方的数量就好了。比如在元素 和元素 之间有一个断开,但是如果我们如果插入的元素是 ,我们会发现 连续,和 也连续,所以也就消除了一个断点。就此同理维护断点的数量,而且断点的数量也只会在新增元素的左右两侧发生改变,所以整体的时间复杂度就是 的,不过需要一个 空间复杂度为 的set来维护这个排序数组,已经可以通过此题。

我还有线性做法!

啥?你这还可以线性?光一个排序你都 了吧?吹啥牛呢?
嗯。。。确实,如果继续沿用上面的思路,是没办法绕过排序这个坎的。我们先把排序的问题放在一边,想想一些其他的线性数据结构,最好是支持 修改的。显然,很多高级的数据结构都不支持这个 修改。我们还得从最基本的数据结构入手。这大概就只能从数组和链表中做出选择了吧。
我们先看看,在不进行排序的前提下,我们可以对哪些前缀做出好坏的判断呢?
首先,第一个单元素的前缀一定是连续的。这个结论可以根据我们的定义得到。同时我们也可以从整体来进行考虑,一个集合是连续的,那么可以认为,在最大值和最小值之间的空隙,是全部都被填满了的。这样一个看似废话的话,却是优化的关键。这意味着,如果一个集合的值域范围,大于元素的个数,我们可以直接进行判断。而**一个值域范围比较小的集合,我们又是可以使用时间复杂度仅为 的基数排序的!**
这样,结合链表 的删除特性,我们可以直接将整个数组进行基数排序,然后倒序处理每个前缀,做到 的修改, 统计新的前缀的断点个数
这样最终的时间复杂度就变成了 ,空间复杂度也是

代码

class Solution {
public:
    /**
     * 检查数组的每个前缀是不是一个好的集合
     * @param mSet int整型vector 
     * @return bool布尔型vector
     */
    vector<bool> continuousSet(vector<int>& mSet) {
        // write code here

        int n = mSet.size(), m = 1;
        vector<bool>ans(n);
        if (mSet.empty()) return ans;

        vector<int>pMax, pMin;
        pMax.push_back(mSet[0]);
        pMin.push_back(mSet[0]);

        for (int i = 1; i < n; i++) {
            pMax.push_back(max(pMax[i - 1], mSet[i]));
            pMin.push_back(min(pMin[i - 1], mSet[i]));
            if (0ll + pMax[i] - pMin[i] <= 0ll + i) m = i + 1;
        }

        for (int i = m; i < n; i++) ans[i] = false;

        struct Node {
            Node *last, *next;
            int value;
            int cnt;
            Node() {
                last = next = NULL;
                cnt = 0;
            }
        };

        int Min = pMin[m - 1];
        int Max = pMax[m - 1];
        vector<Node>node(Max - Min + 1);

        for (int i = 0; i < m; i++) 
            node[mSet[i] - Min].cnt++;

        Node head, tail;
        head.next = &tail;
        tail.last = &head;

        for (int v = Min; v <= Max; v++) {
            node[v - Min].value = v;
            if (v != Min) node[v - Min].last = &node[v - Min - 1];
            else node[v - Min].last = &head, head.next = &node[v - Min];
            if (v != Max) node[v - Min].next = &node[v - Min + 1];
            else node[v - Min].next = &tail, tail.last = &node[v - Min];
        }

        for (Node *cur = head.next; cur != &tail; cur = cur->next) 
            if (cur->cnt == 0) {
                cur->last->next = cur->next;
                cur->next->last = cur->last;
            }

        int cnt = 0;
        for (Node *cur = head.next; cur != &tail; cur = cur->next) {
            cnt++;
            if (cur->last != &head && cur->value - 1 == cur->last->value) 
                cnt--;
        }

        for (int i = m - 1; i >= 0; i--) {
            if (cnt == 1) {
                ans[i] = true;
            } else {
                ans[i] = false;
            }
            if (!--node[mSet[i] - Min].cnt) {
                Node *cur = &node[mSet[i] - Min];
                cur->next->last = cur->last;
                cur->last->next = cur->next;
                cnt--;
                if (cur->last != &head && cur->value - 1 == cur->last->value)
                    cnt++;
                if (cur->next != &tail && cur->value + 1 == cur->next->value)
                    cnt++;
            }
        }
        return ans;
    }
};
全部评论

相关推荐

02-22 20:28
重庆大学 Java
程序员牛肉:首先不要焦虑,你肯定是有希望的。 首先我觉得你得好好想一想自己想要什么。找不到开发岗就一定是失败的吗?那开发岗的35岁危机怎么说?因此无论是找工作还是考公我觉得你都需要慎重的想一想。但你一定要避开这样一个误区:“我是因为找不到工作所以不得不选择考公”。 千万不要这么想。你这个学历挺好的了,因此你投后端岗肯定是有面试机会的。有多少人简历写的再牛逼,直接连机筛简历都过不去有啥用?因此你先保持自信一点。 以你现在的水平的话,其实如果想要找到暑期实习就两个月:一个月做项目+深挖,并且不断的背八股。只要自己辛苦一点,五月份之前肯定是可以找到暑期实习的,你有点太过于高看大家之间的技术差距了。不要焦虑不要焦虑。 除此之外说回你这个简历内容的话,基本可以全丢了。如果想做后端,先踏踏实实做两个项目再说+背八股再说。如果想考公,那就直接备战考公。 但是但是就像我前面说的:你考公的理由可以是因为想追求稳定,想追求轻松。但唯独不能是因为觉得自己找不到工作。不能这么小瞧自己和自己的学历。
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务