#P3899. 第2题-整理书籍
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 20
            Accepted: 9
            Difficulty: 6
            
          
          
          
                       所属公司 : 
                              京东
                                
            
                        
              时间 :2025年10月11日
                              
                      
          
 
- 
                        算法标签>模拟          
 
第2题-整理书籍
解题思路
本题要求两名管理员交替从当前“存活”的书中挑选:
- 小张每次选全局最轻(若并列取最左)
 - 小李每次选全局最重(若并列取最右)
然后以该书为中心,向左右各扩展至多 
k本,一并移除。这个“左右”始终是相对当前尚未被移除的相邻关系。 
要高效实现,需要同时解决两个核心操作:
- 
快速找到当前最轻/最重且仍未被移除的书
- 
用两个堆(优先队列)分别维护:
- 最小堆:键为 
(重量, 位置),并列时位置小的优先(最左)。 - 最大堆:键为 
(-重量, -位置),并列时位置大的优先(最右)。 
 - 最小堆:键为 
 - 
采用惰性删除:堆顶若已被移除则反复弹出,直到堆顶为有效元素。
 
 - 
 - 
从选中的中心位置向两侧各扩展
k本并整体移除- 用数组实现的双向链表维护当前存活的相邻关系:
prev[i]表示当前位置左侧仍存活的邻居下标,next[i]表示右侧邻居。 - 选中下标 
i后,先向左走k步(或走到最左),得到左端点L; 再向右走k步(或走到最右),得到右端点R。 将[L, R]这段在双向链表上整段移除,并把中间每本书标记归属给当轮玩家。 - 维护链表连边:把 
prev[L]与next[R]连接起来。 
 - 用数组实现的双向链表维护当前存活的相邻关系:
 
以上做法的关键是:每本书恰好被删除一次,尽管我们会在堆里多次遇到已删除元素,但总弹出次数线性级,整体复杂度可控。
复杂度分析
- 
每本书最多:
- 被堆弹出(有效一次,失效若干次,但总体仍是线性摊还);
 - 在双向链表中被访问并删除一次。
 
 - 
因此总体时间复杂度为 O(n log n)(主要来自堆操作),空间复杂度为 O(n)(堆、链表、标记与结果存储)。
 
代码实现
Python
# -*- coding: utf-8 -*-
import sys
import heapq
# 功能函数:根据题意进行模拟,返回两位管理员分别搬走的编号(升序)
def solve_books(n, k, a):
    # 1-based 索引
    a = [0] + a
    # 活跃标记:True 表示尚未被移除
    alive = [True] * (n + 1)
    # 记录归属:0=未归属,1=小张,2=小李
    owner = [0] * (n + 1)
    # 双向链表(数组实现),0 作为哨兵(无效端)
    prev = [0] * (n + 1)
    next = [0] * (n + 1)
    for i in range(1, n + 1):
        prev[i] = i - 1 if i > 1 else 0
        next[i] = i + 1 if i < n else 0
    # 两个堆:最小堆(重量小/位置小优先),最大堆(重量大/位置大优先)
    min_heap = []  # (a[i], i)
    max_heap = []  # (-a[i], -i) 使得重量大优先,且并列时 i 大的优先
    for i in range(1, n + 1):
        heapq.heappush(min_heap, (a[i], i))
        heapq.heappush(max_heap, (-a[i], -i))
    remain = n
    turn = 1  # 1 表示小张,2 表示小李
    while remain > 0:
        if turn == 1:
            # 小张:取最小堆
            while min_heap:
                w, idx = heapq.heappop(min_heap)
                if alive[idx] and a[idx] == w:
                    center = idx
                    break
            else:
                break
        else:
            # 小李:取最大堆
            while max_heap:
                nw, ni = heapq.heappop(max_heap)
                w, idx = -nw, -ni
                if alive[idx] and a[idx] == w:
                    center = idx
                    break
            else:
                break
        # 以 center 为中心,向左、右各扩展 k 本(在当前链表中)
        # 找左端点 L
        L = center
        steps = 0
        while steps < k and prev[L] != 0:
            L = prev[L]
            steps += 1
        # 找右端点 R
        R = center
        steps = 0
        while steps < k and next[R] != 0:
            R = next[R]
            steps += 1
        # 记录待链接的外侧邻居
        left_out = prev[L]
        right_out = next[R]
        # 删除 [L..R] 闭区间(沿链表遍历)
        cur = L
        while True:
            alive[cur] = False
            owner[cur] = turn
            remain -= 1
            if cur == R:
                break
            cur = next[cur]
        # 重新连接链表
        if left_out != 0:
            next[left_out] = right_out
        if right_out != 0:
            prev[right_out] = left_out
        # 交换回合
        turn = 2 if turn == 1 else 1
    # 汇总结果并升序输出
    zhang = [i for i in range(1, n + 1) if owner[i] == 1]
    li = [i for i in range(1, n + 1) if owner[i] == 2]
    zhang.sort()
    li.sort()
    return zhang, li
def main():
    data = sys.stdin.read().strip().split()
    n = int(data[0]); k = int(data[1])
    a = list(map(int, data[2:2+n]))
    zhang, li = solve_books(n, k, a)
    print(" ".join(map(str, zhang)))
    print(" ".join(map(str, li)))
if __name__ == "__main__":
    main()
Java
// ACM 风格,类名固定为 Main
// 思路:双堆(惰性删除)+ 数组双向链表,整体 O(n log n)
import java.io.*;
import java.util.*;
public class Main {
    // 记录一本书
    static class Node {
        int w;  // 重量
        int i;  // 位置(1-based)
        Node(int w, int i) { this.w = w; this.i = i; }
    }
    // 主逻辑:返回两位玩家搬走的编号(升序)
    static List<List<Integer>> solveBooks(int n, int k, int[] a) {
        // alive: 是否仍在书架
        boolean[] alive = new boolean[n + 1];
        Arrays.fill(alive, true);
        // owner: 0 未归属,1 小张,2 小李
        int[] owner = new int[n + 1];
        // 双向链表
        int[] prev = new int[n + 1];
        int[] next = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            prev[i] = (i == 1 ? 0 : i - 1);
            next[i] = (i == n ? 0 : i + 1);
        }
        // 最小堆(重量小优先,位置小优先)
        PriorityQueue<Node> minHeap = new PriorityQueue<>(new Comparator<Node>() {
            public int compare(Node x, Node y) {
                if (x.w != y.w) return Integer.compare(x.w, y.w);
                return Integer.compare(x.i, y.i);
            }
        });
        // 最大堆(重量大优先;并列时位置大优先)
        PriorityQueue<Node> maxHeap = new PriorityQueue<>(new Comparator<Node>() {
            public int compare(Node x, Node y) {
                if (x.w != y.w) return Integer.compare(y.w, x.w); // 重量降序
                return Integer.compare(y.i, x.i); // 位置降序(右侧优先)
            }
        });
        for (int i = 1; i <= n; i++) {
            minHeap.add(new Node(a[i], i));
            maxHeap.add(new Node(a[i], i));
        }
        int remain = n;
        int turn = 1; // 1=张,2=李
        while (remain > 0) {
            int center = -1;
            if (turn == 1) {
                // 小张:取最小堆
                while (!minHeap.isEmpty()) {
                    Node top = minHeap.poll();
                    if (alive[top.i] && a[top.i] == top.w) {
                        center = top.i;
                        break;
                    }
                }
            } else {
                // 小李:取最大堆
                while (!maxHeap.isEmpty()) {
                    Node top = maxHeap.poll();
                    if (alive[top.i] && a[top.i] == top.w) {
                        center = top.i;
                        break;
                    }
                }
            }
            if (center == -1) break;
            // 找左端点 L
            int L = center;
            int steps = 0;
            while (steps < k && prev[L] != 0) {
                L = prev[L];
                steps++;
            }
            // 找右端点 R
            int R = center;
            steps = 0;
            while (steps < k && next[R] != 0) {
                R = next[R];
                steps++;
            }
            int leftOut = prev[L];
            int rightOut = next[R];
            // 删除 [L..R]
            int cur = L;
            while (true) {
                alive[cur] = false;
                owner[cur] = turn;
                remain--;
                if (cur == R) break;
                cur = next[cur];
            }
            // 重新连边
            if (leftOut != 0) next[leftOut] = rightOut;
            if (rightOut != 0) prev[rightOut] = leftOut;
            // 交换回合
            turn = (turn == 1 ? 2 : 1);
        }
        List<Integer> zhang = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        for (int i = 1; i <= n; i++) {
            if (owner[i] == 1) zhang.add(i);
            else if (owner[i] == 2) li.add(i);
        }
        Collections.sort(zhang);
        Collections.sort(li);
        List<List<Integer>> ans = new ArrayList<>();
        ans.add(zhang);
        ans.add(li);
        return ans;
    }
    // 简单快速读入(基于缓冲输入)
    static class FastReader {
        BufferedInputStream in = new BufferedInputStream(System.in);
        byte[] buffer = new byte[1 << 16];
        int ptr = 0, len = 0;
        int read() throws IOException {
            if (ptr >= len) {
                len = in.read(buffer);
                ptr = 0;
                if (len <= 0) return -1;
            }
            return buffer[ptr++];
        }
        int nextInt() throws IOException {
            int c, s = 1, x = 0;
            do { c = read(); } while (c <= 32);
            if (c == '-') { s = -1; c = read(); }
            while (c > 32) {
                x = x * 10 + (c - '0');
                c = read();
            }
            return x * s;
        }
    }
    public static void main(String[] args) throws Exception {
        FastReader fr = new FastReader();
        int n = fr.nextInt();
        int k = fr.nextInt();
        int[] a = new int[n + 1];
        for (int i = 1; i <= n; i++) a[i] = fr.nextInt();
        List<List<Integer>> ans = solveBooks(n, k, a);
        List<Integer> zhang = ans.get(0);
        List<Integer> li = ans.get(1);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < zhang.size(); i++) {
            if (i > 0) sb.append(' ');
            sb.append(zhang.get(i));
        }
        System.out.println(sb.toString());
        sb.setLength(0);
        for (int i = 0; i < li.size(); i++) {
            if (i > 0) sb.append(' ');
            sb.append(li.get(i));
        }
        System.out.println(sb.toString());
    }
}
C++
// ACM 风格,主函数读取输入,核心逻辑在外部函数
// 思路:两个优先队列(惰性删除)+ 数组双向链表
#include <bits/stdc++.h>
using namespace std;
struct Node {
    int w, i; // 重量与位置
};
// 自定义比较器
struct MinCmp { // 最小堆:重量小优先,位置小优先
    bool operator()(const Node& a, const Node& b) const {
        if (a.w != b.w) return a.w > b.w;      // 小顶堆效果
        return a.i > b.i;
    }
};
struct MaxCmp { // 最大堆:重量大优先;并列时位置大优先
    bool operator()(const Node& a, const Node& b) const {
        if (a.w != b.w) return a.w < b.w;      // 大顶堆效果
        return a.i < b.i;                      // 位置大优先
    }
};
// 功能函数:返回两人搬走的编号(升序)
pair<vector<int>, vector<int>> solve_books(int n, int k, const vector<int>& a1) {
    vector<int> a(n + 1);
    for (int i = 1; i <= n; ++i) a[i] = a1[i];
    vector<char> alive(n + 1, 1); // 是否还在书架
    vector<int> owner(n + 1, 0);  // 归属:0/1/2
    // 双向链表
    vector<int> prev(n + 1), nxt(n + 1);
    for (int i = 1; i <= n; ++i) {
        prev[i] = (i == 1 ? 0 : i - 1);
        nxt[i] = (i == n ? 0 : i + 1);
    }
    priority_queue<Node, vector<Node>, MinCmp> minHeap;
    priority_queue<Node, vector<Node>, MaxCmp> maxHeap;
    for (int i = 1; i <= n; ++i) {
        minHeap.push({a[i], i});
        maxHeap.push({a[i], i});
    }
    int remain = n;
    int turn = 1; // 1=张,2=李
    while (remain > 0) {
        int center = -1;
        if (turn == 1) {
            // 小张:取最小堆
            while (!minHeap.empty()) {
                Node top = minHeap.top(); minHeap.pop();
                if (alive[top.i] && a[top.i] == top.w) {
                    center = top.i; break;
                }
            }
        } else {
            // 小李:取最大堆
            while (!maxHeap.empty()) {
                Node top = maxHeap.top(); maxHeap.pop();
                if (alive[top.i] && a[top.i] == top.w) {
                    center = top.i; break;
                }
            }
        }
        if (center == -1) break;
        // 找左端 L
        int L = center, steps = 0;
        while (steps < k && prev[L] != 0) { L = prev[L]; ++steps; }
        // 找右端 R
        int R = center; steps = 0;
        while (steps < k && nxt[R] != 0) { R = nxt[R]; ++steps; }
        int leftOut = prev[L], rightOut = nxt[R];
        // 删除 [L..R]
        int cur = L;
        while (true) {
            alive[cur] = 0;
            owner[cur] = turn;
            --remain;
            if (cur == R) break;
            cur = nxt[cur];
        }
        // 重新连边
        if (leftOut != 0) nxt[leftOut] = rightOut;
        if (rightOut != 0) prev[rightOut] = leftOut;
        // 交换回合
        turn = (turn == 1 ? 2 : 1);
    }
    vector<int> zhang, li;
    for (int i = 1; i <= n; ++i) {
        if (owner[i] == 1) zhang.push_back(i);
        else if (owner[i] == 2) li.push_back(i);
    }
    sort(zhang.begin(), zhang.end());
    sort(li.begin(), li.end());
    return {zhang, li};
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, k;
    if (!(cin >> n >> k)) return 0;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; ++i) cin >> a[i];
    auto res = solve_books(n, k, a);
    auto& zhang = res.first;
    auto& li = res.second;
    for (int i = 0; i < (int)zhang.size(); ++i) {
        if (i) cout << ' ';
        cout << zhang[i];
    }
    cout << "\n";
    for (int i = 0; i < (int)li.size(); ++i) {
        if (i) cout << ' ';
        cout << li[i];
    }
    cout << "\n";
    return 0;
}
        题目内容
图书馆有 n 本书横向排成一排,从左到右编号为 1 到 n ,每本书的重量为 ai 千克。图书管理员小张和小李轮流整理这些书籍,规则如下:
首先是小张整理,小张的策略是找到当前书架上最轻的一本书(如果有多个重量相同,则选择最左侧的那本),然后搬走这本书以及它左侧的 k 本书(如果左侧不足 k 本,则搬走左侧全部)和它右侧的 k 本书(如果右侧不足 k 本,则搬走右侧全部)。
然后是小李整理,小李的策略是找到当前书架上最重的一本书(如果有多个重量相同,则选择最右侧的那本),然后搬走这本书以及它左侧的 k 本书和右侧的 k 本书(不足时的处理情况同上)。
两人轮流整理,直到所有书籍都被搬走。图书馆馆长要记录工作情况,他想知道最后两人分别搬走了哪些书籍。
输入描述
第一行为两个整数 n(1≤n≤100000) 和 k(0≤k≤n) ,分别表示书籍的总数量和每次搬书时向两侧扩展的书籍数量。
第二行为 n 个空格隔开的整数,其中第 i 个数 ai(1≤ai≤1000) 表示第 i 本书的重量。
输出描述
输出的第一行为小张搬走的书籍编号,按升序排列。
输出的第二行为小李搬走的书籍编号,按升序排列。
数据保证每人都至少搬走了一本书。
样例1
输入
10 2
3 1 4 1 5 9 2 6 5 3
输出
1 2 3 4 9 10
5 6 7 8