【OD机试题解法笔记】贪心歌手

题目描述

一个歌手准备从A城去B城参加演出。

  1. 按照合同,他必须在 T 天内赶到
  2. 歌手途经 N 座城市
  3. 歌手不能往回走
  4. 每两座城市之间需要的天数都可以提前获知。
  5. 歌手在每座城市都可以在路边卖唱赚钱。
    经过调研,歌手提前获知了每座城市卖唱的收入预期: 如果在一座城市第一天卖唱可以赚M,后续每天的收入会减少D(第二天赚的钱是 M - D,第三天是 M - 2D …)。如果收入减少到 0 就不会再少了。
  6. 歌手到达后的第二天才能开始卖唱。如果今天卖过唱,第二天才能出发。
    贪心的歌手最多可以赚多少钱

输入描述
第一行两个数字 T 和 N,中间用空格隔开。

  • T 代表总天数,0 < T < 1000
  • N 代表路上经过 N 座城市,0 < N < 100

第二行 N+1 个数字,中间用空格隔开。代表每两座城市之间耗费的时间。

  • 其总和 ≤ T。

接下来 N 行,每行两个数字 M 和 D,中间用空格隔开。代表每个城市的收入预期。

  • 0 < M < 1000
  • 0 < D < 100

用例
输入

10 2
1 1 2
120 20
90 10

输出

540

说明
总共10天,路上经过2座城市。
路上共花 1+1+2=4 天
剩余6天最好的计划是在第一座城市待3天,在第二座城市待3天。
在第一座城市赚的钱:120 + 100 + 80 = 300
在第二座城市赚的钱:90 + 80 + 70 = 240
一共 300 + 240 = 540。

思考

先计算除去路上花费的天数后剩余可分配天数,在 N 个城市中按顺序卖唱的最大收益。用例中的数据,城市1的日收益如下:
120 100 80 60 40 20,城市2 的日收益: 90 80 70 60 50 40,假如剩余天数是6天,都在第一座城市卖唱,那么收益数组为:
[120,100,80,60,40,20],显然后3天的收益 [60,40,20] 小于第二座城市前3天的收益 [90,80,70],那么最大收益就是第一座城市停留3天收益+第二座城市停留3天的收益,即[120,100,80,90,80,70]。具体算法应该是先按顺序遍历每座经过的城市,对每个城市,假设剩余天数都停留在这座城市,得到一个收益列表 profits;下一轮循环遍历下一座城市时重复之前操作,但把收益列表中最小的收益数替换为当前更大的收益,这样最终的收益列表profits尽可能包含了每座城市中最大的收益数,即是整个歌手能获得的最大卖唱收益。可以用优先队列快速获取最小收益,并将较大收益数入队,优化时间复杂度。有些编程语言没有内置优先队列,不熟悉实现起来比较耗费时间,比如JavaScript,可以用数组模拟下,如:let minIndex = queue.lastIndexOf(Math.min(...queue)), queue[minIndex] = curElem;,只要做题时能通过就行。

算法过程

该算法基于最小优先队列(Min-Heap) 实现,核心思路是:从所有城市的可能卖唱日收益中,筛选出最大的leftDays个收益(leftDays为可用于卖唱的剩余天数),总和即为最大总收益。具体步骤如下:

步骤1:计算可用于卖唱的剩余天数
  • 首先计算总路程耗时:将输入的N+1段路程时间求和(记为totalRoadDays)。
  • 剩余可卖唱天数为:leftDays = T - totalRoadDays。若leftDays ≤ 0,则无时间卖唱,直接返回0。
步骤2:初始化最小优先队列
  • 使用最小优先队列(堆)存储当前筛选出的最大日收益,队列最多容纳leftDays个元素(因为总共有leftDays天可卖唱)。
  • 最小优先队列的特性是:顶部元素始终是当前队列中最小的元素,便于快速替换为更大的收益。
步骤3:遍历所有城市,计算并筛选日收益

对于每座城市,按顺序计算在该城市停留第1天、第2天……直到第leftDays天的日收益(超过leftDays天的收益无需计算,因为总天数有限),并按规则加入队列:

  • 日收益计算规则:第j天(j从1开始)的收益为 max(M - D*(j-1), 0)M为初始收益,D为每日递减值,收益不能为负)。
  • 队列操作规则
    • 若队列元素数量 < leftDays:直接将当前日收益加入队列(此时需要填充所有可能的天数)。
    • 若队列已满(元素数量 = leftDays):比较当前日收益与队列顶部的最小收益。若当前收益更大,则移除队列中最小的收益,将当前收益加入队列(保证队列始终保留目前最大的leftDays个收益)。
步骤4:计算最大总收益

当所有城市的所有可能日收益都处理完毕后,队列中存储的是最大的leftDays个日收益。将这些收益求和,即为歌手能获得的最大总收益。

示例验证(对应题目用例)

  1. 计算剩余天数:总天数T=10,路程时间1+1+2=4,故leftDays=6
  2. 处理第一座城市(M=120,D=20)
    • 日收益依次为:120(第1天)、100(第2天)、80(第3天)、60(第4天)、40(第5天)、20(第6天)。
    • 队列初始为空,依次加入这6个收益,队列元素为[20,40,80,60,100,120](堆顶为最小元素20)。
  3. 处理第二座城市(M=90,D=10)
    • 日收益依次为:90(第1天)、80(第2天)、70(第3天)、60(第4天)、50(第5天)、40(第6天)。
    • 逐个处理:
      • 90 > 堆顶20 → 移除20,加入90 → 队列元素更新(堆顶40)。
      • 80 > 堆顶40 → 移除40,加入80 → 队列元素更新(堆顶60)。
      • 70 > 堆顶60 → 移除60,加入70 → 队列元素更新(堆顶70)。
      • 60、50、40均小于堆顶70 → 不加入队列。
  4. 求和队列元素:最终队列元素为70,80,80,90,100,120,总和为70+80+80+90+100+120=540,与用例结果一致。

算法核心逻辑

通过最小优先队列动态维护最大的leftDays个日收益,利用每个城市日收益单调递减的特性(每天收益≤前一天),确保筛选出的收益是全局最优的。该方法时间复杂度为O(N×leftDays×log(leftDays)),高效适用于题目约束(N<100leftDays<T<1000)。

参考代码

class MinPriorityQueue {

  constructor() {
    this._data = [];
  }

  enqueue(e) {
    this._data.push(e);
    this.swim();
  }

  dequeue() {
    this._data.shift();
    if (this.isEmpty()) return;
    let lastElem = this._data.pop();
    this._data.unshift(lastElem);
    this.sink();
  }

  swim() {
    const n = this._data.length;
    let index = n - 1;
    while (index > 0) {
      let parentIndex = Math.floor((index-1)/2);
      if (this._data[index] < this._data[parentIndex]) {
        [this._data[parentIndex], this._data[index]] = [this._data[index], this._data[parentIndex]];
        index = parentIndex;
        continue;
      }
      break;
    }

  }

  sink() {
    let index = 0;
    const n = this._data.length;
    while (true) {
      let left = 2 * index + 1;
      let right = left + 1;
      let smallest = index;

      if (left < n && this._data[left] < this._data[index]) {
        smallest = left;
      }

      if (right < n && this._data[right] < this._data[smallest]) {
        smallest = right;
      }

      if (smallest !== index) {
        [this._data[smallest], this._data[index]] = [this._data[index], this._data[smallest]];
        index = smallest;
        continue;
      }

      break;
    }

  }

  top() {
    return this._data[0];
  }

  isEmpty() {
    return this._data.length === 0;
  }

  size() {
    return this._data.length;
  }
}




function solution() {
  const [T, N] = readline().split(' ').map(Number);
  const times = readline().split(' ').map(Number);
  const cities = [];
  for (let i = 0; i < N; i++) {
    cities[i] = readline().split(' ').map(Number);
  }

  const leftDaysNum = T - times.reduce((cur,acc) => cur + acc);

  const profits = new MinPriorityQueue();

  for (let i = 0; i < N; i++) {
    for (let j = 0; j < leftDaysNum; j++) {
      let curProfit = Math.max(cities[i][0] - cities[i][1] * j, 0);
      if (curProfit === 0) {
        break;
      }
      if (profits.size() < leftDaysNum) {
        profits.enqueue(curProfit);
      } else {
        if (curProfit > profits.top()) {
          profits.dequeue();
          profits.enqueue(curProfit);
        }
      }
    }
  }

  let result = 0;
  while (profits.size()) {
    result += profits.top();
    profits.dequeue();
  }

  console.log(result);
}

const cases = [
  `10 2
1 1 2
120 20
90 10`,
];

let caseIndex = 0;
let lineIndex = 0;

const readline = (function () {
  let lines = [];
  return function () {
    if (lineIndex === 0) {
      lines = cases[caseIndex]
        .trim()
        .split("\n")
        .map((line) => line.trim());
    }
    return lines[lineIndex++];
  };
})();

cases.forEach((_, i) => {
  caseIndex = i;
  lineIndex = 0;
  solution();
});

### 华为 OD 试题及解答 #### Java 题目解析 在华为 OD 考中,Java 是一种常见的编程语言。以下是一个关于二进制位操作的经典题目及其解决方案。 给定一个整数 `num`,计算其二进制表示中有多少个 `1` 的方法可以通过逐位检查实现[^2]。 以下是具体的代码示例: ```java import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int num = in.nextInt(); // 输入数字 int count = 0; // 计数器初始化 for (int i = 0; i < 32; i++) { // 假设输入的是标准的 32 位整数 if ((num & 1) == 1) { // 如果当前最低位是 1,则增加计数 count++; } num = num >>> 1; // 将数字无符号右移一位 } System.out.println(count); // 输出最终的结果 } } ``` 此程序的核心在于利用按位与运算符 (`&`) 和无符号右移运算符 (`>>>`) 来逐步提取并统计二进制中的每一位是否为 `1`。 --- #### 字符串处理题目解析 另一个常见问题是字符串的操作,比如将字符串中的每个单词首字母大写化[^3]。下面是一段 Python 实现的例子: ```python while True: try: words = input().split() # 获取用户输入并分割成列表 result = [] for word in words: if len(word) > 0: # 确保单词长度大于零 capitalized_word = f"{word[0].upper()}{word[1:]}" result.append(capitalized_word) print(" ".join(result)) # 打印结果 except Exception as e: break # 捕获异常后退出循环 ``` 该脚本通过遍历每一个单词,并将其第一个字符转为大写字母来完成任务。 --- #### 准备建议 为了更好地应对华为 OD 试,可以采取如下策略: - **熟悉常用算法**:掌握基本的数据结构(数组、链表、堆栈等)以及经典算法(排序、查找等),这些知识点经常会在实际测试中被考察到。 - **多练习编码能力**:针对不同类型的题目进行专项训练,尤其是时间复杂度优化方面的技巧学习。 - **理解业务场景需求**:部分试题可能结合具体应用场景设计,因此除了技术层面外还需要考虑逻辑思维的应用。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值