排队-题解

排队-题解

《排队》是一道很优美的贪心题(我不知道它的来源,估计其难度提高+/省选),曾被出在了北京大学计算概论B的考试中。本文的受众主要是学习计算概论的同学以及老师,不假定读者有信息竞赛(OI)经验。

题目描述

有 N 名同学从左到右排成一排,第 i 名同学的身高为 hi。现在张老师想改变排队的顺序,他能进行任意多次(包括0次)如下操作:

  • 如果两名同学相邻,并且他们的身高之差不超过 D,那么老师就能交换他俩的顺序。

请你帮张老师算一算,通过以上操作,字典序最小的所有同学(从左到右)身高序列是什么?

\((1<=N<=10^5, 1<=D<=10^9, 1<=h_i<=10^9)\)

提交链接

初步分析

​ 这是一个最优化问题,需要找到一个贪心准则,然后用其他算法辅助实现。一般的思路是,先找到一个贪心构造最优解的方法,先不管这个方法的复杂度,然后再寻找一些性质,进行优化。

​ 任务:给定一个序列,通过交换元素的位置,找到其字典序最小的一个排列。

​ 约束:任意身高差大于D的元素不对易(不对易的元素的先后关系永远不能改变)。

​ 也就是,每个元素前面都有一些无法翻越的“大山”,这个元素必须排到“大山”的后面。前面没有“大山”的元素,我们称为“自由节点”。

一个显然的算法就是:每次找出所有的”自由节点“,找出其中最小的一个,排在前面,然后把它去掉。假如有多个相同大小的元素,它们必然会被连续地输出掉,所以先后顺序无影响。这个算法本身证明了其正确性(is self-proved)。

​ 每次找出”自由节点“的代价为O(n),只需要维护前缀最大值、最小值(最可能成为”大山“),就可以判断当前点是否自由了,判据为h[i] + D >= maxv and h[i] - D <= minv

​ 总时间复杂度为 \(O(N^2)\) 。代码如下

INF = int(1e9 + 7)
N, D = map(int, input().split())
h = [int(input()) for _ in range(N)]
used = [0] * N

for _ in range(N):
    minv, maxv = INF, -INF
    idx, val = 0, INF
    for i in range(N):
        if used[i]:
            continue
        minv = min(minv, h[i])
        maxv = max(maxv, h[i])
        if (h[i] + D >= maxv and h[i] - D <= minv and h[i] < val):
            val = h[i]
            idx = i
    used[idx] = 1
    print(h[idx])

寻找性质

​ 贪心题的突破点往往在于一些性质。寻找性质最好的方法是写出几个样例,手动寻找最优解,然后总结自己不知不觉发现的贪心准则。

​ 比如说这样一组数据

D=3
h=[3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]

​ 按照刚才的贪心准则,最初的”自由节点“为

[3, 1, 4, 1]

​ 执行初步分析得出的程序,得到的序列为

[1, 1, 3, 4, 5, 9, 2, 3, 5, 5, 6, 7, 8, 9, 9]

​ 我们发现最初的”自由节点“恰好是答案的前4个。我们打印出每次选取时的自由节点列表

1 from [3, 1, 4, 1]
1 from [3, 4, 1]
3 from [3, 4, 5]
4 from [4, 5]
5 from [5]
9 from [9]
2 from [2, 5, 3, 5]
3 from [6, 5, 3, 5]
5 from [6, 5, 5, 8, 7]
5 from [6, 5, 8, 7]
6 from [6, 8, 9, 7, 9]
7 from [8, 9, 7, 9]
8 from [8, 9, 9]
9 from [9, 9]
9 from [9]

​ 观察并分组,我们发现每找一次”自由节点“,就可以把所有的自由节点排序、输出、删掉。然后再找一波自由节点就行了。也就是说,这个序列是分层的

1 from [3, 1, 4, 1]
1 from [3, 4, 1]
3 from [3, 4, 5]
4 from [4, 5]

5 from [5]

9 from [9]

2 from [2, 5, 3, 5]
3 from [6, 5, 3, 5]
5 from [6, 5, 5, 8, 7]
5 from [6, 5, 8, 7]

6 from [6, 8, 9, 7, 9]
7 from [8, 9, 7, 9]
8 from [8, 9, 9]
9 from [9, 9]
9 from [9]

再看一些数据(我们不妨把D改成2),我们发现以上准则依然成立。

1 from [3, 1]
3 from [3, 4]

4 from [4]

1 from [1]

5 from [5]

9 from [9]

2 from [2]

5 from [6, 5, 5]
5 from [6, 5]
6 from [6]

3 from [3]

7 from [8, 9, 7, 9]
8 from [8, 9, 9]
9 from [9, 9]
9 from [9]

​ 于是我们可以扫描一次,输出一层。时间复杂度\(O(N\max(\log N,d))\),其中d是图的层数,假如D很小,d可能接近N,复杂度还是太高。(ps:由于OJ数据比较弱,层数很小,这个方法写出的代码可以直接AC,并且速度秒杀正解)

​ 我们可以不加证明地认为,这一准则是无比正确的(OI选手往往不会追求形式化的证明)。假如你不放心,我在这儿勉为其难地给出一个证明。

​ 思路很简单:我们每次拎出所有的自由节点,只要证明它们会在被其他节点之前被输出就可以了。

​ 首先,后面的因为”过高“被挡住的点肯定不急着输出,它们提前输出只会增大字典序。

​ 然后考虑”太矮“而被限制的节点,它们前面有一个比较高的”大山“,只要这个”大山“被输出了,它们就可以排到前面,减小字典序了。但是按照我们最初的贪心准则,我们每次要输出最小的自由节点,所有比”大山“小的节点都得放在“大山”之前输出,比大山大的节点也可以把那个”太矮“的节点挡住(除非它在太矮的节点的后面,但那样它就会被挡住,从而不可能是自由节点)。所以这个”太矮“的节点必须等到这一时刻的所有自由节点输出完毕才会被输出。由于同一层的自由节点都是对易的,输出它们的顺序自然是从小到大。

​ 可以看到,虽然这个性质的证明很简单,但我们是很难不做任何猜测,直接推导出这个性质的。所以做贪心题还是得找性质。这真的很难,这个题我想了几个月才做出来。

优化

​ 我们还能发现一点,就是每一层元素都有一个”大山“来自于上一层元素。换句话说,我们只需要找到一个点的“大山”中最大的层数,将其层数+1就可以得到这个元素的层数。算出了每个元素的层数,就可以快速将其归类,然后分别排序输出了。

​ 找”层数最大的大山的层数“,变成了一个求最大值问题,有很多数据结构可以以O(logN)的代价维护区间最大值。

​ 我们定量描述一下”h[i]的层数最大的大山的层数“。”大山”意味着h>h[i]+D或h<h[i]-D,“最大层数”就在这两个区间求最大值就行了。我们确定下i的层数之后,再把h[i]位置的最大层数更新一下就行了。

​ 我们需要一种数据结构,支持我们查询一个区间的最大值,并且可以修改一个点的值(支持“区间查询,单点修改”)。

法1:线段树

​ 最直接的数据结构就是线段树了(支持区间修改,区间查询;上网搜,或者看老师的课件),不过由于h的范围很大,内存不支持我们开一个长达1e9的数组,我们必须使用“动态开点线段树”。时间复杂度\(O(N\log \text{MAXH})\)其中\(\text{MAXH}\)是H的值域宽度。代码如下:(面向对象的设计可能对初学者很不友好)

# 使用class写一棵线段树
class SegmentTreeNode:
    def __init__(self, start, end, val):
        self.start = start
        self.end = end
        self.val = val
        self.lson = None
        self.rson = None


class SegmentTree:
    def __init__(self, start, end):
        self.root = SegmentTreeNode(start, end, 0)
        self.start = start
        self.end = end

    # 返回修改后的节点的对象。注意对象是浅拷贝,里面存储了一些指针。
    # 改变了子对象的指针之后,需要重新赋值给父对象,不然子对象的子对象就会被释放
    def modify(self, node, pos, value, start=None, end=None):
        if (start is None):
            start, end = self.start, self.end
        if (node is None):
            node = SegmentTreeNode(start, end, value)
        if (start == end):
            node.val = max(node.val, value)
            return node
        mid = (node.start + node.end) // 2
        if pos <= mid:
            node.lson = self.modify(node.lson, pos, value, start, mid)
        else:
            node.rson = self.modify(node.rson, pos, value, mid + 1, end)
        node.val = max(node.val, value)
        return node

    def query(self, node, qstart, qend):
        if ((node is None) or qstart > node.end or qend < node.start):
            return 0
        if (qstart <= node.start and node.end <= qend):
            return node.val
        ret = max(self.query(node.lson, qstart, qend), self.query(node.rson, qstart, qend))
        return ret


# 主程序非常短
N, D = map(int, input().split())
h = [int(input()) for _ in range(N)]
# 求出每个点的层数,用一个线段树维护值域上的层数,这样就可以求出挡在前面的大山的最大的层数了
MAXH = max(h)
layers = SegmentTree(1, MAXH)
members = [[]]  # 存储每一层里面的元素
for hi in h:
    current_layer = max(layers.query(layers.root, 1, hi - D - 1), layers.query(layers.root, hi + D + 1, MAXH)) + 1
    if (current_layer >= len(members)):
        members.append([])
    members[current_layer].append(hi)
    layers.modify(layers.root, hi, current_layer)

# 直接一层层排序、输出即可
for layer in members:
    for _ in sorted(layer):
        print(_)

法2:树状数组

​ 这种方法思路需要更多技巧,不过代码比较短,而且跑得更快。

​ 树状数组支持维护前缀最大值,以及单点修改。我们要查询的恰好是前缀、后缀最大值(h>h[i]+D或h<h[i]-D),其中后缀最大值只需要把数列反过来就可以了。

​ 还有一个问题,内存不允许我们开一个1e9的数组,树状数组也不能动态开点。所以我们必须使用另一个技巧——“离散化”。(离散化也可以用于线段树,这样就不需要动态开点了)

​ 我们把所有h排序、去重,构造“权值序列”。然后所有h都可以替换成其在“权值序列”中的排位。比如说我们要查找<h[i]-D的点的最大层数,只需要查找到权值序列中比h[i]-D小的最大的元素的位置index,在树状数组中求出前缀最大值get_max(index)即可

​ 代码如下:感谢23-数院-胡睿诚

# by 23-数院-胡睿诚
import bisect


def low_bit(n):
    if n == 0:
        return 0
    elif n % 2 == 1:
        return 1
    else:
        return 2*low_bit(n // 2)


low_bit_list = [low_bit(n) for n in range(10**5+1)]


class SearchMaxList:
    def __init__(self,length):
        self.list = [-1]*length
        self.length = length
        self.assistant_list = [-1]*length

    def get_max(self,index):
        output_max = -1
        while index > 0:
           output_max = max(self.assistant_list[index-1],output_max)
           index -= low_bit_list[index]
        return output_max

    def update(self,index,new_value):
        self.list[index-1] = new_value
        while index <= self.length:
            self.assistant_list[index-1] = max(self.assistant_list[index-1],new_value)
            index += low_bit_list[index]


N,D = map(int,input().split())
heights = []
heights_dict = {}
classification_dict = {}
for i in range(N):
    heights.append(int(input()))
sorted_heights_set = sorted(list(set(heights)))
reversed_heights_set = sorted(list(set(heights)),reverse=True)
M = len(sorted_heights_set)
for i in range(M):
    heights_dict[sorted_heights_set[i]] = i

sorted_heights = SearchMaxList(M)
reversed_heights = SearchMaxList(M)

for height in heights:
    sorted_index = bisect.bisect_right(sorted_heights_set,height-D-1)
    reversed_index = M-bisect.bisect_left(sorted_heights_set,height+D+1)
    max_1 = sorted_heights.get_max(sorted_index)
    max_2 = reversed_heights.get_max(reversed_index)
    path_length = max(max_1,max_2) + 1
    sorted_heights.update(heights_dict[height]+1,path_length)
    reversed_heights.update(M-heights_dict[height],path_length)
    try:
        classification_dict[path_length].append(height)
    except KeyError:
        classification_dict[path_length]=[height]

for path_length in sorted(classification_dict.keys()):
    for height in sorted(classification_dict[path_length]):
        print(height)
posted @ 2023-11-10 16:11  guoshaoyang  阅读(581)  评论(0编辑  收藏  举报