avatar
substackSubstackthreadsinstagram

[M] 3066. Minimum Operations to Exceed Threshold Value II

目錄

題目

  • LeetCode 連結
  • 主題:Heap (Priority Queue)
  • 難度:Medium
  • Daily:2025-02-13

題目描述

給定一個 0 索引 的整數數組 nums,以及一個整數 k

每次操作,你可以執行以下步驟:

  1. 取出 nums 中最小的兩個整數 xy
  2. nums 中移除 xy
  3. min(x, y) * 2 + max(x, y) 添加回 nums

請注意,當 nums 至少包含兩個元素時,才能執行上述操作。請返回使得 nums 中所有元素都大於等於 k 所需的最少操作次數

範例 1

輸入:nums = [2,11,10,1,3], k = 10
輸出:2
解釋:
- 第一次操作,移除 `1` 和 `2`,然後加入 `1 * 2 + 2 = 4`,nums 變為 `[4, 11, 10, 3]`。
- 第二次操作,移除 `3` 和 `4`,然後加入 `3 * 2 + 4 = 10`,nums 變為 `[10, 11, 10]`。
- 此時所有數字都大於等於 `10`,可以停止操作。
最少需要 2 次操作使所有數字都大於等於 `10`。

範例 2

輸入:nums = [1,1,2,4,9], k = 20
輸出:4
解釋:
- 第一次操作後,nums 變為 `[2, 4, 9, 3]`。
- 第二次操作後,nums 變為 `[7, 4, 9]`。
- 第三次操作後,nums 變為 `[15, 9]`。
- 第四次操作後,nums 變為 `[33]`。
- 此時所有數字都大於等於 `20`,可以停止操作。
最少需要 4 次操作使所有數字都大於等於 `20`。

限制條件

  • 2 <= nums.length <= 2 * 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^9
  • 輸入保證一定存在解,即總能透過某種操作順序,使所有元素大於等於 k

問題釐清

  • 雖然題目限制一定存在符合 k 值條件的解,但為了處理邊界條件,若像是 nums 長度小於 2 或找不到解則回傳 -1 是否可行?

提出思路

這題如果不使用 min heap 來實作的話,直覺應該會是用各種陣列方法來做暴力解,也就是在 while 迴圈中不斷地插入、刪除、檢查當前最小值是否大於等於 k。

而如果能將一開始的 nums 轉成 min heap 的結構,主程式的邏輯就簡潔許多了,效能也好。

實作

稍微學了一下基礎 priority queue 與 heap 的概念後,才發現原來像是 Python 的 heapq、C++ 的 priority_queue、Java、Go 這些語言都有內建 heap 的資料結構,而如果使用 JavaScript/TypeScript 的話,很抱歉,這需要自己實作,甚至有個專門的套件 (@datastructures-js/heap@datastructures-js/priority-queue) 能直接調用。

如果先實作一個簡單版的 MinHeap class 的話會長這樣:

// @/types/Heap.ts
export class MinHeap<T> {
  private heap: T[] = [];
 
  private getParentIndex(i: number): number {
    return Math.floor((i - 1) / 2);
  }
 
  private getLeftChildIndex(i: number): number {
    return 2 * i + 1;
  }
 
  private getRightChildIndex(i: number): number {
    return 2 * i + 2;
  }
 
  private swap(i: number, j: number): void {
    [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
  }
 
  private heapifyUp(): void {
    let index = this.heap.length - 1;
    while (index > 0) {
      const parentIndex = this.getParentIndex(index);
      if (this.heap[index] >= this.heap[parentIndex]) break;
      this.swap(index, parentIndex);
      index = parentIndex;
    }
  }
 
  private heapifyDown(): void {
    let index = 0;
    while (this.getLeftChildIndex(index) < this.heap.length) {
      let smallerChildIndex = this.getLeftChildIndex(index);
      const rightChildIndex = this.getRightChildIndex(index);
 
      if (
        rightChildIndex < this.heap.length &&
        this.heap[rightChildIndex] < this.heap[smallerChildIndex]
      ) {
        smallerChildIndex = rightChildIndex;
      }
 
      if (this.heap[index] <= this.heap[smallerChildIndex]) break;
 
      this.swap(index, smallerChildIndex);
      index = smallerChildIndex;
    }
  }
 
  push(value: T): void {
    this.heap.push(value);
    this.heapifyUp();
  }
 
  pop(): T | undefined {
    if (this.heap.length === 0) return undefined;
    if (this.heap.length === 1) return this.heap.pop();
 
    const minValue = this.heap[0];
    this.heap[0] = this.heap.pop()!;
    this.heapifyDown();
    return minValue;
  }
 
  peek(): T | undefined {
    return this.heap[0];
  }
 
  size(): number {
    return this.heap.length;
  }
 
  isEmpty(): boolean {
    return this.heap.length === 0;
  }
}

有個這個結構後,就能來實作這一題:

import { MinHeap } from '@/types/Heap';
 
function minOperations(nums: number[], k: number): number {
  // handle edge case
  if (nums.length < 2) {
    return -1;
  }
 
  // declare new nums array
  const heap = new MinHeap<number>();
 
  for (let num of nums) {
    heap.push(num);
  }
 
  let count = 0;
  // run while loop until array length smaller than h
  // check whether all new nums larger or equal to k
  while (heap.size() >= 2 && heap.peek()! < k) {
    const x = heap.pop()!;
    const y = heap.pop()!;
    const newNum = Math.min(x, y) * 2 + Math.max(x, y);
 
    heap.push(newNum);
    count++;
  }
 
  // return minimum operation count
  return heap.peek()! < k ? -1 : count;
}

複雜度分析

N 代表 nums 長度:

  • 時間複雜度:O(NlogN)
    • 建立 min heap 結構:每次插入元素是 O(logN),執行 N 次所以是 O(NlogN)
    • while 迴圈:
      • 取最小值 (peek) 時是 O(1)
      • 每次 pop 最小值是 O(logN)
      • 每次 push 新計算的值是 O(logN)
      • 迴圈執行次數接近 N 次,因此總計為 O(NlogN)
  • 空間複雜度:另外用了一個 heap 來處理為 O(N)

程式碼

詳細程式碼可以參考此 GitHub 連結