leetcode - 1673 - 查找最有竞争力的子序列 - RMQ - 单调栈

文章目录

    • 题目描述
    • 题目代码
    • 题目剖析&信息挖掘
    • 解题思路
      • 方法一 模拟构造法
        • 分析
        • 思路
        • 注意
        • 知识点
        • 复杂度
        • 参考
        • 代码实现
      • 方法二 有序栈构造法
        • 分析
        • 思路
        • 注意
        • 知识点
        • 复杂度
        • 参考
        • 代码实现

题目描述

  • https://leetcode-cn.com/problems/find-the-most-competitive-subsequence

给你一个整数数组 nums 和一个正整数 k ,返回长度为 k 且最具 竞争力 的 nums 子序列。

数组的子序列是从数组中删除一些元素(可能不删除元素)得到的序列。

在子序列 a 和子序列 b 第一个不相同的位置上,如果 a 中的数字小于 b 中对应的数字,那么我们称子序列 a 比子序列 b(相同长度下)更具 竞争力 。 例如,[1,3,4] 比 [1,3,5] 更具竞争力,在第一个不相同的位置,也就是最后一个位置上, 4 小于 5 。

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^9
  • 1 <= k <= nums.length
Related Topics
  • 数组
  • 最小栈
  • 区间最值(RMQ)

题目代码

func mostCompetitive(nums []int, k int) []int {

}

题目剖析&信息挖掘

本题是一个构造题,这种一般需要发现一些规则。

主要有2种解法,一种是通过模拟构造,需要用到区间查询的数据结构。

另一种是利用最小栈去构造。

解题思路

方法一 模拟构造法

分析

  • 根据题意我们可以从最左边l=0开始选择,每次尽量选最小的,注意尾部要留下k-1个数字。
  • 每次选择完一个,会有一selecteInd, 令l=selecteInd+1,k–, 再重复上面的过程,直到k=0。

思路

func mostCompetitive(nums []int, k int) []int {
  res := []int{}
  for l:=-1;k>0;k-- {
    ind := getMin(nums, l+1, l-(k-1)) // 遍历法求最小值
    l=ind
    res = append(res, nums[ind])
  }
  return res
}
  • 按照上面的算法,每次选择复杂度是O(n), 一共要进行k次,极端情况下复杂度是O(n^2)。
  • 这里的问题关键是选择算法复杂度太高了,通过分析我们可以发现这个选择算法是一个求最值问题
  • 区间最值问题可以优化到nlog(n)的复杂度
  • 主要的算法有线段树和RMQ(ST) 算法,可网上搜索了解,我后续也会写一些文章专门讲这个。

注意

  • 边界情况
  • 大规模数据

知识点

  • 数组
  • 区间最值

复杂度

  • 时间复杂度:O(nlog(n))
  • 空间复杂度:O(nlog(n))

参考

  • https://baike.baidu.com/item/rmq/1797559?fr=aladdin RMQ
  • https://baike.baidu.com/item/%E7%BA%BF%E6%AE%B5%E6%A0%91/10983506?fr=aladdin 线段树

代码实现

/*
区间最值问题,指的是查询数组某一段中数字最大或最小值,
时间复杂度: 初始化 nlog(n), 查询O(1)
空间复杂度:nlog(n)
*/

func MinFunc(a, b int) int {
	if a == b {
		return 0
	}
	if a < b {
		return -1
	}
	return 1
}

func MaxFunc(a, b int) int {
	return MinFunc(b, a)
}

type RangeM struct {
	array       []int              // 存储原始数据
	store       [][]int            // 存储计算最值结果, store [i][j] 存储的是起点为j长度为2^i 的区间 [j, j+2^i)的最值(下标)。
	compareFunc func(a, b int) int // 自定义比较函数
}

func (r *RangeM) Init(arr []int, comFunc func(a, b int) int) error {
	if comFunc == nil {
		return errors.New("comFunc is empty")
	}
	r.compareFunc = comFunc
	r.array = arr
	if err := r.assignStore(); nil != err {
		return err
	}

	if err := r.calStore(); nil != err {
		return err
	}

	return nil
}

// 预先分配内在
func (r *RangeM) assignStore() error {
	l := len(r.array)
	if l <= 0 {
		return nil
	}

	r.store = make([][]int, int(math.Log2(float64(l)))+2) // 需要 log(l) 行,保险起见,+2
	for i, _ := range r.store {
		r.store[i] = make([]int, l)
	}

	return nil
}

func (r *RangeM) calStore() error {
	l := len(r.array)
	// 第一行长度为1,赋值为自身
	for i := 0; i < l; i++ {
		r.store[0][i] = i
	}

	// store[i][j] = max | min (store[i-1][j], store[i-1][j+1<<(i-1)])
	for i := uint(1); 1<<i <= l; i++ {
		for j := 0; j < l; j++ {
			r.store[i][j] = r.store[i-1][j] // 默认赋值左半边最值
			right := j + int(1<<(i-1))
			// 如果有右半边,且值比左半边更优,则取右半边值
			if right < l && r.compareFunc(r.array[r.store[i-1][j]], r.array[r.store[i-1][right]]) > 0 {
				r.store[i][j] = r.store[i-1][right]
			}
		}
	}

	return nil
}

func (r *RangeM) GetValue(start, end int) (value int, index int, err error) {
	if r.store == nil {
		return 0, -1, errors.New("not init yet")
	}
	bit := uint(math.Log2(float64(end - start))) // 找到一个长度,使得从前后2边伸展,并最终可以交叉
	// 比较前后2边最值,取最优解。
	index = r.store[bit][start]
	right := end - (1 << bit)
	if r.compareFunc(r.array[index], r.array[r.store[bit][right]]) > 0 {
		index = r.store[bit][right]
	}

	return r.array[index], index, nil
}

func getRangeMin(nums []int, start, end int) (num, ind int) {
	num, ind = nums[start], start
	for ; start < end; start++ {
		if nums[start] < num {
			num, ind = nums[start], start
		}
	}
	return
}

func mostCompetitive(nums []int, k int) []int {
	res := []int{}
	l := len(nums)
	rm := &RangeM{}
	rm.Init(nums, MinFunc)

	for ind := -1; k > 0; k-- {
		var minNum int
		minNum, ind, _ = rm.GetValue(ind+1, l-(k-1))
		res = append(res, minNum)
	}

	return res
}
/*
[0]
1
*/

方法二 有序栈构造法

分析

  • 我们先选择第一个元素打入到栈里,如果后面有比第一小的就要进行替换,如果没有就要排到后面。
  • 替换的时候要从栈顶到栈底依次比较。因为栈是有序的。所以可以保证越在前面的数字是越小的。
  • 每个数字最多入栈出栈一次:空间复杂度是O(n),时间复杂度是O(n)

思路

func mostCompetitive(nums []int, k int) []int {
  st
	for n in nums {
    while len(st)>0 && st.top>n && len(st) + nums剩余个数 >k {
			st.pop
		}
    st.push(n)
	}

  return st的前k个元素
}

注意

  • 剩余个数条件
  • 判断栈为空的情况

知识点

  • 数组

复杂度

  • 时间复杂度:O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

参考

代码实现

func mostCompetitive(nums []int, k int) []int {
	lastIndex :=-1
	l := len(nums)

	for ind := 0; ind<l; ind++ {
		for lastIndex>=0 && nums[ind]<nums[lastIndex] && lastIndex +1 + l-ind >k {
			lastIndex--
		}
		lastIndex++
		nums[lastIndex] = nums[ind]
	}

	return nums[:k]
}
/*
[0]
1
*/

本人公众号:彬彬魔坊

leetcode - 1673 - 查找最有竞争力的子序列 - RMQ - 单调栈_第1张图片

你可能感兴趣的:(leetcode,leetcode,算法,数据结构,贪心算法)