[JOISC 2021] Event Hopping 2

GitHub跳转原题关系图返回列表

按编号做字典序贪心,以区间调度倍增作为可扩展性判定,并用 Treap 维护空闲时间段。

OJ: luogu

题目 ID: P7562

难度:NOI/NOI+/CTSC

标签:字典序贪心倍增Treappython

日期: 2026-07-16 18:28

题意

从按编号给出的活动中选择恰好 k 个两两不冲突的活动。若可行,输出编号递增且字典序最小的方案。

思路

依次扫描活动编号。若当前活动与已选活动兼容,并且选它之后仍能补足剩余数量,就立刻选择;这是构造字典序最小序列的标准“能选则选”。难点是快速判断“还能选多少个”。

对任意空闲时间段 [left, right],最多活动数由经典区间调度贪心得到:每次选择开始不早于当前位置且结束最早的活动。离散化端点后,next_end[x] 表示这一步会到达的最早结束坐标;对它建立倍增,query(left, right) 就能在 O(logn)O(\log n) 内计算最多可放活动数。

已选活动把时间轴切成若干互不相交的空闲段,总可选数是各段 query 之和。尝试加入 [L,R] 时,只需把它所在空闲段的贡献替换为 [free_left,L][R,free_right] 两段的贡献。

空闲段需要按左端点查询前驱、删除和插入,代码用随机 Treap 维护。若新的总容量仍不少于所需活动数,就正式选择当前编号并分裂空闲段。

Python 知识

  • 集合推导式收集所有端点,sortedbisect_left 完成离散化。
  • array("i") 保存倍增表,在 n=10^5 时比整数列表节省很多内存。
  • Treap 的左右儿子、优先级、键和值分别存在列表中,用整数编号代替大量节点对象。
  • 固定 random.seed(...) 使随机树形可复现;期望复杂度仍为对数级。
  • "\n".join(map(str, chosen)) 一次输出选择序列。

代码

python
import random
import sys
from array import array
from bisect import bisect_left


data = iter(map(int, sys.stdin.buffer.read().split()))
n, required = next(data), next(data)
events = [(next(data), next(data)) for _ in range(n)]
coordinates = sorted({value for event in events for value in event})
compressed = [(bisect_left(coordinates, left), bisect_left(coordinates, right))
              for left, right in events]
coordinate_count = len(coordinates)

next_end = array("i", [coordinate_count]) * (coordinate_count + 1)
for left, right in compressed:
    if right < next_end[left]:
        next_end[left] = right
for i in range(coordinate_count - 1, -1, -1):
    if next_end[i + 1] < next_end[i]:
        next_end[i] = next_end[i + 1]

jump = [next_end]
for _ in range(1, n.bit_length() + 1):
    previous = jump[-1]
    jump.append(array("i", map(previous.__getitem__, previous)))


def query(left, right):
    count = 0
    current = left
    for level in range(len(jump) - 1, -1, -1):
        destination = jump[level][current]
        if destination <= right:
            current = destination
            count += 1 << level
    return count


# Randomized treap of disjoint free intervals, keyed by left endpoint.
random.seed(20260716)
left_child = [0]
right_child = [0]
priority = [0]
key = [0]
value = [0]


def new_node(left, right):
    left_child.append(0)
    right_child.append(0)
    priority.append(random.randrange(1 << 30))
    key.append(left)
    value.append(right)
    return len(key) - 1


def split(root, split_key):
    if not root:
        return 0, 0
    if key[root] < split_key:
        left_tree, right_tree = split(right_child[root], split_key)
        right_child[root] = left_tree
        return root, right_tree
    left_tree, right_tree = split(left_child[root], split_key)
    left_child[root] = right_tree
    return left_tree, root


def merge(left_root, right_root):
    if not left_root or not right_root:
        return left_root or right_root
    if priority[left_root] < priority[right_root]:
        right_child[left_root] = merge(right_child[left_root], right_root)
        return left_root
    left_child[right_root] = merge(left_root, left_child[right_root])
    return right_root


def insert(root, node):
    if not root or priority[node] < priority[root]:
        left_child[node], right_child[node] = split(root, key[node])
        return node
    if key[node] < key[root]:
        left_child[root] = insert(left_child[root], node)
    else:
        right_child[root] = insert(right_child[root], node)
    return root


def erase(root, target_key):
    if key[root] == target_key:
        return merge(left_child[root], right_child[root])
    if target_key < key[root]:
        left_child[root] = erase(left_child[root], target_key)
    else:
        right_child[root] = erase(right_child[root], target_key)
    return root


def predecessor(root, target_key):
    result = 0
    while root:
        if key[root] <= target_key:
            result = root
            root = right_child[root]
        else:
            root = left_child[root]
    return result


root = new_node(0, coordinate_count - 1)
available = query(0, coordinate_count - 1)
chosen = []

for identity, (event_left, event_right) in enumerate(compressed, 1):
    node = predecessor(root, event_left)
    if not node or value[node] < event_right:
        continue
    free_left, free_right = key[node], value[node]
    remaining = (available - query(free_left, free_right)
                 + query(free_left, event_left) + query(event_right, free_right))
    if remaining >= required - len(chosen) - 1:
        chosen.append(identity)
        available = remaining
        root = erase(root, free_left)
        root = insert(root, new_node(free_left, event_left))
        root = insert(root, new_node(event_right, free_right))
        if len(chosen) == required:
            break

print("\n".join(map(str, chosen)) if len(chosen) == required else -1)

复杂度

离散化、倍增和逐活动判定共 O(nlogn)O(n\log n) 期望时间。倍增表占 O(nlogn)O(n\log n) 空间,Treap 与其他数组占 O(n)O(n)

总结

字典序贪心本身只有一句“能选则选”,真正的工程核心是提供快速的可扩展性判定:区间调度倍增计算容量,Treap 维护被已选活动切开的空闲段。