1712 - 得到子序列的最少操作次数 - 哈希 - 线段树

欢迎关注更多精彩
关注我,学习常用算法与数据结构,一题多解,降维打击。

题目描述

[1712] 得到子序列的最少操作次数

  • https://leetcode-cn.com/problems/minimum-operations-to-make-a-subsequence/

题目描述

给你一个数组 target ,包含若干 互不相同 的整数,以及另一个整数数组 arr ,arr 可能 包含重复元素。

每一次操作中,你可以在 arr 的任意位置插入任一整数。比方说,如果 arr = [1,4,1,2] ,那么你可以在中间添加 3 得到 [1,4,3,1,2] 。你可以在数组最开始或最后面添加整数。

请你返回 最少 操作次数,使得 target 成为 arr 的一个子序列。

一个数组的 子序列 指的是删除原数组的某些元素(可能一个元素都不删除),同时不改变其余元素的相对顺序得到的数组。比方说,[2,7,4] 是 [4,2,3,7,2,1,4] 的子序列(加粗元素),但 [2,4,2] 不是子序列。

示例 1:

输入:target = [5,1,3], arr = [9,4,2,3,4]
输出:2
解释:你可以添加 5 和 1 ,使得 arr 变为 [5,9,4,1,2,3,4] ,target 为 arr 的子序列。
示例 2:

输入:target = [6,4,8,1,3,2], arr = [4,7,6,2,3,8,6,1]
输出:3

提示:

1 <= target.length, arr.length <= 10^5
1 <= target[i], arr[i] <= 10^9
target 不包含任何重复元素。

Related Topics
  • 线段树
  • 下标离散化
  • 哈希

题目剖析&信息挖掘

题目考查的是最上升子序列算法的运用。需要用到线段树优化查询效率。

解题思路

方法一 离散化+哈希+线段树

思考

题目说在arr里添加元素使得target成为其子序列。反过来也可以说target删除一些数字达到目的。

想要操作的数最少,就是想arr里的数字得到最大化的利用。就是查找target与arr的最长公共子序列。

原来求这个是用动态规划,复杂度是O(n^2),不成立。

先搞些特殊例子看看。

target = [1,2,3] (是一个上升序列)

arr = [9,4,2,3,4,1,2,3,3,2,1]

公共子序列是1,2,3

对于上面的例子,我们可以先把无用的元素从arr中去掉

arr = [2,3,1,2,3,3,2,1]

由于target是一个上升序列。

所以题目就转化成求arr的最长上升子序列。

这个可以使用线段树优化到O(nlog(n))。

分析

上面的特殊例子中可以发现,arr本身的元素如果能够确定一定的排序规则(优先级),就可以转化到最长上升子序列的方法。

再分析可以发现这个规则是由target决定的。

由于我们的目标是target,而且target中元素不重复。那么我们就可以以target中出现元素的次序来决定数字的优先级。

以 例1 target = [5,1,3], arr = [9,4,2,3,4]为例

由target 得到数字优先级

5->1

1->2

3->3

替换arr = [3] (去掉无用项)

显然最长公共子序列长度为1,所以要补2个。

再看例2,target = [6,4,8,1,3,2], arr = [4,7,6,2,3,8,6,1]

由target 得到数字优先级

6->1

4->2

8->3

1->4

3->5

2->6

替换arr = [2,1,6,5,3,1,4] (去掉无用项)

最长公共子序列长度为3([2,3,4] 或 [1, 3, 4]),所以要补3个。

关键:如何O(nlog(n))解决最升上升子序列问题

原来的方法,dp(i)代表以第i个元素为结尾的最长上升子序列是多长。

dp(i) = max(1+dp(j)) (i>j && arr[i]>arr[j])

现在换一种思路

dp(x) 代表以数字x结尾最长上升子序列是多长。
其中x = arr[i] (i 从0到len(arr)) 慢慢变大。
dp(arr[i]) = 1+max(dp(arr[j])) (i>j && arr[i]>arr[j])
max(dp(arr[j])) 其实是查询 i 之前 以arr[i]-1 到 0结尾中最长子序列。
这是一个区间求最值问题,可以用线段树解决。
用代码表示一下

func lengthOfLIS(arr []int) int {
	ans <- 0
  dp[] <- 0
  seg
	for i <- 0 to len(arr) { // 一定要从前往后算
		// 查询 max(dp(arr[j])) 其实是查询 i 之前 以arr[i]-1 到 0结尾中最长子序列。
		dp[arr[i]] = 1+seg.query(0, arr[i]-1) // 目前在树里的都 < i的。
		seg.insert(arr[i], dp[arr[i]]) // 把当前结果添加到树中
		ans = max(dp[arr[i]], ans)
	}

	return ans
}
思路
const N = int(1e5) * 22

var Ind map[int]int

func InitMap(target []int) {

}

func getInd(n int) int {

}

type node struct {
	l, r                  int // 代表树结点代表的区间范围
	leftChild, rightChild *node
	maxVal                int
}

type SegmentTree struct {
	nodes []node // 事先申请结点,加事内存分配
	root  int    //根结点编号
}

// 初始化线段树,分配内存大小, 构造树型
func (tree *SegmentTree) Init(l, r int) {

}

// 构造树型
func (tree *SegmentTree) buildNode(l, r, root int) *node {
	
}

func (tree *SegmentTree) InsertSegment(l, r, weight int) {
	tree.insert(l, r, weight, tree.root)
}

func (tree *SegmentTree) insert(l, r, weight, root int)  {
	
}

func (tree *SegmentTree) Query(l, r int) int {
	return tree.query(l, r, tree.root)
}

func (tree *SegmentTree) query(l, r, root int) int {

}

func max(a, b int) int {
}

func minOperations(target []int, arr []int) int {
	InitMap(target)
	maxSub := 0
	seg := &SegmentTree{}
	seg.Init(0, len(target))

	for _, v := range arr {
		ind := getInd(v) // 获取优先级
		if ind<0 { // 说明是无用数字,直接跳过
			continue
		}
		preMax := seg.Query(0, ind-1) // 查询0, ind 最长上升子序列
		maxSub = max(maxSub, preMax+1) // 更新最大值
		seg.InsertSegment(ind, ind, 1+preMax) // 把最新结果加入到树中
	}
	return len(target) - maxSub
}

注意
  • 需要事先申请树结点,加快效率。
  • 查询到的是公共个数,最终结果要的是补齐个数。
知识点
  • 最升上升子序列
  • 线段树
  • 离散化
  • 哈希
复杂度
  • 时间复杂度:O(nlog(n))
  • 空间复杂度:O(n)
参考
代码实现
const N = int(1e5) * 22

var Ind map[int]int

func InitMap(target []int) {
	Ind = make(map[int]int)
	for i, v := range target {
		Ind[v] = i
	}
}

func getInd(n int) int {
	if i, ok :=Ind[n]; ok {
		return i
	}

	return -1
}

type node struct {
	l, r                  int // 代表树结点代表的区间范围
	leftChild, rightChild *node
	maxVal                int
}

type SegmentTree struct {
	nodes []node // 事先申请结点,加事内存分配
	root  int    //根结点编号
}

// 初始化线段树,分配内存大小, 构造树型
func (tree *SegmentTree) Init(l, r int) {
	tree.nodes = make([]node, (r-l+1)*4)
	tree.root = 1 //
	tree.buildNode(l, r, tree.root)
}

// 构造树型
func (tree *SegmentTree) buildNode(l, r, root int) *node {
	if l > r {
		return nil
	}

	mid := (l + r) >> 1
	tree.nodes[root].l, tree.nodes[root].r = l, r
	tree.nodes[root].maxVal = 0
	if l == r {
		return &tree.nodes[root]
	}
	// 构造左右子树
	tree.nodes[root].leftChild = tree.buildNode(l, mid, root<<1)
	tree.nodes[root].rightChild = tree.buildNode(mid+1, r, root<<1|1)
	return &tree.nodes[root]
}

func (tree *SegmentTree) InsertSegment(l, r, weight int) {
	tree.insert(l, r, weight, tree.root)
}

func (tree *SegmentTree) insert(l, r, weight, root int)  {
	if l > tree.nodes[root].r || r < tree.nodes[root].l {
		return
	}

	if l <= tree.nodes[root].l && tree.nodes[root].r <= r {
		tree.nodes[root].maxVal = weight
		return
	}

	tree.insert(l, r, weight, root<<1)
	tree.insert(l, r, weight, root<<1|1)
	/*
		更新本区间的最大值
	*/
	tree.nodes[root].maxVal = max(tree.nodes[root<<1].maxVal , tree.nodes[root<<1|1].maxVal)
}

func (tree *SegmentTree) Query(l, r int) int {
	return tree.query(l, r, tree.root)
}

func (tree *SegmentTree) query(l, r, root int) int {
	if l > tree.nodes[root].r || r < tree.nodes[root].l {
		return 0
	}

	if l <= tree.nodes[root].l && tree.nodes[root].r <= r {
		return tree.nodes[root].maxVal
	}

	leftVal := tree.query(l, r, root<<1)
	rightVal := tree.query(l, r, root<<1|1)

	return max(leftVal, rightVal)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func minOperations(target []int, arr []int) int {
	InitMap(target)
	maxSub := 0
	seg := &SegmentTree{}
	seg.Init(0, len(target))

	for _, v := range arr {
		ind := getInd(v)
		if ind<0 {
			continue
		}
		preMax := seg.Query(0, ind-1)
		maxSub = max(maxSub, preMax+1)
		seg.InsertSegment(ind, ind, 1+preMax)
	}
	return len(target) - maxSub
}

相关题目

https://leetcode-cn.com/problems/longest-increasing-subsequence/
https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/


本人码农,希望通过自己的分享,让大家更容易学懂计算机知识。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值