欢迎关注更多精彩
关注我,学习常用算法与数据结构,一题多解,降维打击。
文章目录
题目描述
[1712] 得到子序列的最少操作次数
题目描述
给你一个数组 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 不包含任何重复元素。
- 线段树
- 下标离散化
- 哈希
题目剖析&信息挖掘
题目考查的是最上升子序列算法的运用。需要用到线段树优化查询效率。
解题思路
方法一 离散化+哈希+线段树
思考
题目说在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/
本人码农,希望通过自己的分享,让大家更容易学懂计算机知识。