题目链接

组建一支最强篮球队

题目描述

一位篮球教练需要从一份按考察顺序排列的 位候选球员名单中,挑选 名球员组成一支队伍。每位球员都有一个实力评分。

挑选规则:

  1. 保持相对顺序:如果球员 A 在名单中先于球员 B,那么在最终队伍中,如果两人都被选中,A 的位置也必须在 B 之前。
  2. 实力最强:队伍的实力通过字典序进行比较。从两队的第一名球员开始逐一比较,第一个出现评分差异的位置上,评分较高的球员所在的队伍实力更强。

任务: 找出实力最强的 人队伍,并输出他们的评分列表。

输入描述:

  • 第一行:一串由空格分隔的整数,代表候选球员的实力评分。
  • 第二行:一个正整数 ,代表队伍人数。
  • 约束:输入的评分必须是十进制整数,否则视为无效输入。

输出描述:

  • 输出实力最强队伍的球员评分列表,各评分之间用空格隔开。
  • 如果输入包含非十进制数字或格式错误,则输出 error

解题思路

本题的目标是从一个数字序列中,选择 个数组成一个新的子序列,要求在保持原相对顺序的前提下,使这个子序列的字典序最大。这是一个经典的单调栈应用问题,也可以理解为一种贪心策略。

核心思想:

为了让最终选出的 人队伍的字典序尽可能大,我们应该让排在前面的球员实力评分尽可能高。

我们可以遍历所有候选球员,同时维护一个结果栈(或列表),这个栈将用来构建最终的队伍。

  1. 遍历与决策

    • 当我们考察第 位球员时,我们看结果栈的栈顶元素。
    • 如果当前球员的实力比栈顶球员强,并且我们还有“淘汰名额”(即 (总人数 - 当前考察位置) + 栈中人数 > K),那么栈顶球员就可以被淘汰(出栈),因为用当前更强的球员替换他,可以使结果的字典序变得更大或保持不变(如果前面都一样)。
    • 这个淘汰过程需要持续进行,直到栈顶球员实力不弱于当前球员,或者淘汰名额用完。
    • 之后,将当前球员压入栈中。
  2. 淘汰名额的计算

    • 设总球员数为 ,需要挑选 人。那么我们总共可以淘汰 人。
    • 更精细的判断是,在遍历到第 个球员时(从 0 开始),后面还剩下 个候选人。当前栈中有 len(stack) 个人。为了凑齐 个人,我们最多还需要 个人。只要后面剩下的人数 N - 1 - i 大于我们还需要的人数,就说明我们还有选择的余地,可以淘汰栈顶较弱的球员。
    • 一个更简洁的判断条件是:只要 len(stack) + (N - i) > K,就说明我们有淘汰的资本。
  3. 构建单调栈

    • 创建一个空栈 result_stack
    • 遍历输入的球员评分列表 scores
    • 对于每个球员 score
      • 当栈不为空,score 大于栈顶元素,且我们还有淘汰名额(len(result_stack) + (N - i) > K)时,就不断地将栈顶元素弹出。
      • 然后将当前 score 压入栈中。
    • 遍历结束后,栈中的元素可能多于 个(因为可能有些较小的数无法被淘汰),因此需要截取前 个元素作为最终结果。
  4. 输入校验

    • 在读取输入时,需要检查每个评分字符串是否为纯十进制数字。这可以通过尝试将字符串转换为整数并捕获异常来实现。如果任何一个字符串转换失败,或者 不是一个正整数,都应输出 error

示例演练: scores = [88, 83, 82, 84, 84, 83, 90, 85], K = 4

  • 88入栈: [88]
  • 83入栈: [88, 83]
  • 82入栈: [88, 83, 82]
  • 84 > 82, 淘汰82. 84 > 83, 淘汰83. 84 < 88, 停止. 84入栈: [88, 84]
  • 84 == 84, 84入栈: [88, 84, 84]
  • 83入栈: [88, 84, 84, 83]
  • 90 > 83, 淘汰83. 90 > 84, 淘汰84. 90 > 84, 淘汰84. 90 > 88, 淘汰88. 90入栈: [90] (这里淘汰名额充足)
  • 85入栈: [90, 85]

这里贪心选择似乎有问题,我们重新审视逻辑。可淘汰的人数是

修正后的贪心/单调栈逻辑:

  1. 可淘汰人数 removals = N - K
  2. 遍历球员,维护结果栈 stack
  3. 对于每个球员 score
    • 当栈不为空,score > stack.top(),且 removals > 0 时:
      • stack.pop()
      • removals--
    • stack.push(score)
  4. 遍历结束后,如果栈中元素多于 个(removals 可能未用完),则从栈底开始取 个元素。

再次演练: scores = [88, 83, 82, 84, 84, 83, 90, 85], K = 4, removals = 4

  • 88入栈: [88]
  • 83 < 88, 83入栈: [88, 83]
  • 82 < 83, 82入栈: [88, 83, 82]
  • 84 > 82, 82出栈, removals=3. [88, 83]. 84 > 83, 83出栈, removals=2. [88]. 84入栈: [88, 84]
  • 84 == 84, 84入栈: [88, 84, 84]
  • 83 < 84, 83入栈: [88, 84, 84, 83]
  • 90 > 83, 83出栈, removals=1. [88, 84, 84]. 90 > 84, 84出栈, removals=0. [88, 84]. 此时removals用完. 90入栈: [88, 84, 90]
  • 85 < 90, 85入栈: [88, 84, 90, 85]
  • 遍历结束,栈中正好4个元素。结果: 88 84 90 85. 正确。

代码

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>

using namespace std;

// 检查字符串是否为有效的十进制整数
bool is_valid_integer(const string& s) {
    if (s.empty() || (s.length() > 1 && s[0] == '0') && s.find_first_not_of("0123456789") == string::npos) {
        // 允许单个'0', 但不允许'01'这样的前导零
        if (s.length() > 1 && s[0] == '0') return false;
    }
    if (s.length() == 1 && s[0] == '0') return true;
    
    // 检查是否有非数字字符,除了可能有的负号
    string temp = s;
    if (s[0] == '-') {
        temp = s.substr(1);
        if(temp.empty()) return false;
    }
    
    return temp.find_first_not_of("0123456789") == string::npos;
}

int main() {
    string line;
    if (!getline(cin, line)) {
        cout << "error" << endl;
        return 0;
    }

    stringstream ss(line);
    string score_str;
    vector<int> scores;
    while (ss >> score_str) {
        if (!is_valid_integer(score_str)) {
            cout << "error" << endl;
            return 0;
        }
        scores.push_back(stoi(score_str));
    }

    string k_str;
    if (!(cin >> k_str) || !is_valid_integer(k_str) || k_str[0] == '-') {
        cout << "error" << endl;
        return 0;
    }
    int k = stoi(k_str);

    if (k <= 0 || k > scores.size()) {
        // 虽然题目约束 k>0,但防御性编程处理
        return 0;
    }

    vector<int> stack;
    int n = scores.size();
    int removals = n - k;

    for (int score : scores) {
        while (!stack.empty() && score > stack.back() && removals > 0) {
            stack.pop_back();
            removals--;
        }
        stack.push_back(score);
    }
    
    // 如果淘汰名额没用完,栈中可能多于k个,截取前k个
    stack.resize(k);

    for (int i = 0; i < k; ++i) {
        cout << stack[i] << (i == k - 1 ? "" : " ");
    }
    cout << endl;

    return 0;
}

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        try {
            // 读取第一行球员评分
            String line = sc.nextLine();
            String[] score_strs = line.split(" ");
            List<Integer> scores = new ArrayList<>();
            for (String s : score_strs) {
                // 检查是否为有效十进制整数
                if (!s.matches("-?\\d+")) {
                    System.out.println("error");
                    return;
                }
                scores.add(Integer.parseInt(s));
            }

            // 读取第二行队伍人数
            int k = sc.nextInt();
            
            if (k <= 0 || k > scores.size()) {
                return; // 根据题目约束,k应为正数且不大于N
            }
            
            int n = scores.size();
            // 可以淘汰的人数
            int removals = n - k;
            
            // 使用 Deque 作为单调栈
            Deque<Integer> stack = new ArrayDeque<>();
            
            for (int score : scores) {
                // 当栈不为空,当前分数>栈顶,且还有淘汰名额时,淘汰栈顶
                while (!stack.isEmpty() && score > stack.peekLast() && removals > 0) {
                    stack.removeLast();
                    removals--;
                }
                stack.addLast(score);
            }
            
            // 最终结果为栈中的前 k 个元素
            StringBuilder sb = new StringBuilder();
            Iterator<Integer> it = stack.iterator();
            for (int i = 0; i < k; i++) {
                sb.append(it.next()).append(i == k - 1 ? "" : " ");
            }
            System.out.println(sb.toString());

        } catch (Exception e) {
            // 捕获所有可能的异常,如 NumberFormatException, NoSuchElementException 等
            System.out.println("error");
        }
    }
}

def solve():
    # 读取第一行并进行合法性校验
    try:
        score_strs = input().split()
        if not score_strs:
            print("error")
            return
        scores = [int(s) for s in score_strs]
    except ValueError:
        print("error")
        return
    
    # 读取第二行并进行合法性校验
    try:
        k_str = input()
        if not k_str.isdigit() or int(k_str) <= 0:
            print("error")
            return
        k = int(k_str)
    except (ValueError, IndexError):
        print("error")
        return

    n = len(scores)
    if k > n:
        return

    # 可淘汰的人数
    removals = n - k
    # 单调栈
    stack = []

    for score in scores:
        # 当栈不为空,当前分数大于栈顶,且还有淘汰名额
        while stack and score > stack[-1] and removals > 0:
            stack.pop()
            removals -= 1
        stack.append(score)
    
    # 截取前 k 个元素作为结果
    result = stack[:k]
    
    # 格式化输出
    print(*result)

solve()

算法及复杂度

  • 算法:本题使用了单调栈的思路,结合贪心策略。为了使最终子序列的字典序最大,我们遍历原序列,并维护一个结果栈。对于每个新元素,如果它比栈顶元素大,并且我们还有淘汰名额,就将栈顶元素弹出,以此来保证栈中靠前的元素尽可能大。
  • 时间复杂度,其中 是候选球员的数量。每个球员最多被入栈和出栈一次,所以整个过程是线性的。
  • 空间复杂度,在最坏的情况下(例如输入序列是降序的),所有球员都会被压入栈中,因此栈的空间复杂度是