leetcode-1674-使数组互补的最少操作次数-线段树

文章目录

    • 题目描述
    • 题目剖析&信息挖掘
    • 解题思路
      • 方法一 区间覆盖法
        • 思考过程
        • 分析
        • 思路
        • 注意
        • 知识点
        • 复杂度
        • 参考
        • 代码实现

题目描述

[1674] 使数组互补的最少操作次数

给你一个长度为 偶数 n 的整数数组 nums 和一个整数 limit 。每一次操作,你可以将 nums 中的任何整数替换为 1 到 limit 之间的另一个整数。

如果对于所有下标 i(下标从 0 开始),nums[i] + nums[n - 1 - i] 都等于同一个数,则数组 nums 是 互补的 。例如,数组 [1,2,3,4] 是互补的,因为对于所有下标 i ,nums[i] + nums[n - 1 - i] = 5 。

返回使数组 互补 的 最少 操作次数。

  • n == nums.length
  • 2 <= n <= 105
  • 1 <= nums[i] <= limit <= 105
  • n 是偶数。
Related Topics
  • 数组
  • 线段树
  • 区间覆盖

题目剖析&信息挖掘

先转化成区间覆盖问题,再用相关数据结构去解决。

解题思路

方法一 区间覆盖法

思考过程

  • 首先想到的是,基准问题,第一感觉是其中的某一项为基准,但马上得到了否定。
  • 不一定是取某一项为基准。
  • 举个例子 [1,1,100,100] limit=100,即不是2,也不是200
  • 但是可肯定的是 这个标准点肯定是落在 [2, 2*limit],可以缩小到 min(num[0]+num[n-1,…,]num[i]+num[n-1-i]), max(num[0]+num[n-1,…,]num[i]+num[n-1-i])。
  • 简单证明一下 假设 当基准项取 x = min(num[0]+num[n-1,…,]num[i]+num[n-1-i])时, 各数字的操作(表示是否改变数字)数为 (o0, o1, …on)
  • 现在令x变小,则操作数为(p0, p1, …pn), 可以得到 p[i]+p[n-1-i] >= o[i]+o[n-1-i]。所以在x < min(num[0]+num[n-1,…,]num[i]+num[n-1-i]) 时,不能得到更优的解。
  • x>max(num[0]+num[n-1,…,]num[i]+num[n-1-i]) 也同理。
  • 有了这个基础,我有了一个方向,就是枚举x去求解可行解,最后取最优解。
  • 如果按照普通做法,枚举一个x然后去求解一下各组数据到x需要的操作数,这个复杂度是O(n^2), 对于题目给出的数据模板无法通过。
  • 那么有没有办法把这些操作数存储到一个数组A, A[x]代表所有组数据到x的总操作数呢。B[x]代表能到达x的数组组合(判断是否可行解),可以按如下做法。
  • 对于某组数据 num[i], num[n-1-i](假设num[i]
  • 我们可以知道,其0次操作能达到的区间就是 [num[i]+num[n-1-i], num[i]+num[n-1-i]]
  • 1次操作能达到的区间就是 [num[i]+1,num[i]+num[n-1-i]), (num[i]+num[n-1-i], limit+num[n-1-i]]
  • 2次操作能达到的区间就是 [2, num[i]+1), (limit+num[n-1-i], 2*limit]
  • 遍历以上区间,分别对A, B进行累加
  • 最后遍历B, 如果B[x] == n/2 说明x是一个可行解,在此前提下,取到A[x]的最小值。
  • 但是以上方法的复杂度是O(n^2), 不过可以转化到区间覆盖问题进行求解,可以优化到O(nlogn)

分析

  • 通过上面的解析,我把题目先往区间覆盖问题进行一下转化

  • 有q个区间,每个区间有一个weight属性,求被n个区间覆盖到,并且weihgt总和最小的点。

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mKnFatlu-1607084721711)

  • leetcode-1674-使数组互补的最少操作次数-线段树_第1张图片

  • 这种问题在我所掌握的方法中,可以通过线段树数据结构去解决,总复杂度是 O(nlogn)。

思路

  • 基本思路是先求出所有的区间以及其操作数
  • 初始化线段树,并添加线段
  • 遍历所有的基准x, 并求解到达x的操作数。
  • 这里要使用到线段树的延迟机制,把正常操作分摊到insert, query操作中,线段树相关内容可以查询网络资源,后续我也会出一些文章。
type node struct {
   l, r                    int // 代表树结点代表的区间范围
   leftChild, rightChild   *node
   delay                   int // 延迟标记
   totalCount, totalWeight int // 总覆盖数,及总操作数
}

type SegmentTree struct {
   nodes []node
   root  int
}

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

}

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

func (tree *SegmentTree) InsertSegment(l, r, weight int) {

}

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

}

func (tree *SegmentTree) QueryPoint(x int) (totalCount, totalWeight int) {
   return 0, 0
}

func (tree *SegmentTree) query(l, r, root int) *node {
   return nil
}


func minMoves(nums []int, limit int) int {
	segmentTree := &SegmentTree{}
	segmentTree.Init(2, 2*limit) // 初始化线段树,范围是2- 2*limit
	/* 构造线段添加到树中*/
	n := len(nums)
	for i := 0; i < n/2; i++ {
		n1, n2 := getSort(nums[i], nums[n-1-i])
		segmentTree.InsertSegment(n1+n2, n1+n2, 0) // 本身操作数为0
		//- 1次操作能达到的区间就是 [num[i]+1,num[i]+num[n-1-i]), (num[i]+num[n-1-i], limit+num[n-1-i]]
		segmentTree.InsertSegment(n1+1, n1+n2-1, 1)
		segmentTree.InsertSegment(n1+n2+1, limit+n2, 1)
		//- 2次操作能达到的区间就是 [2, num[i]+1), (limit+num[n-1-i], 2*limit]
		segmentTree.InsertSegment(2, n1, 2)
		segmentTree.InsertSegment(limit+n2+1, 2*limit, 2)
	}

	minOp := len(nums)
	for x := 2; x <= 2*limit; x++ {
		totCnt, opCnt := segmentTree.QueryPoint(x)
		if totCnt == n/2 && opCnt < minOp {
			minOp = opCnt
		}
	}

	return minOp
}

注意

  • 大规模数据

知识点

  • 数组
  • 区间最值

复杂度

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

参考

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

代码实现

type node struct {
	l, r                    int // 代表树结点代表的区间范围
	leftChild, rightChild   *node
	totalCount, totalWeight 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].totalCount, tree.nodes[root].totalWeight = 0, 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].totalWeight += weight
		tree.nodes[root].totalCount++
		return
	}

	tree.insert(l, r, weight, root<<1)
	tree.insert(l, r, weight, root<<1|1)
}

func (tree *SegmentTree) QueryPoint(x int) (totalCount, totalWeight int) {
	n := tree.query(x, x, tree.root)
	if n != nil {
		return n.totalCount, n.totalWeight
	}
	return 0, 0
}

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

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

	tree.pushDown(root)
	mid := (tree.nodes[root].l + tree.nodes[root].r) >> 1
	if l <= mid {
		return tree.query(l, r, root<<1)
	}

	return tree.query(l, r, root<<1|1)
}

func (tree *SegmentTree) pushDown(root int) {
	totCnt, totWeight := tree.nodes[root].totalCount, tree.nodes[root].totalWeight
	tree.nodes[root].totalCount, tree.nodes[root].totalWeight = 0, 0

	tree.nodes[root<<1].totalCount += totCnt
	tree.nodes[root<<1].totalWeight += totWeight

	tree.nodes[root<<1|1].totalCount += totCnt
	tree.nodes[root<<1|1].totalWeight += totWeight
}

func getSort(a, b int) (int, int) {
	if a < b {
		return a, b
	}
	return b, a
}

func minMoves(nums []int, limit int) int {
	segmentTree := &SegmentTree{}
	segmentTree.Init(2, 2*limit) // 初始化线段树,范围是2- 2*limit
	/* 构造线段添加到树中*/
	n := len(nums)
	for i := 0; i < n/2; i++ {
		n1, n2 := getSort(nums[i], nums[n-1-i])
		segmentTree.InsertSegment(n1+n2, n1+n2, 0) // 本身操作数为0
		//- 1次操作能达到的区间就是 [num[i]+1,num[i]+num[n-1-i]), (num[i]+num[n-1-i], limit+num[n-1-i]]
		segmentTree.InsertSegment(n1+1, n1+n2-1, 1)
		segmentTree.InsertSegment(n1+n2+1, limit+n2, 1)
		//- 2次操作能达到的区间就是 [2, num[i]+1), (limit+num[n-1-i], 2*limit]
		segmentTree.InsertSegment(2, n1, 2)
		segmentTree.InsertSegment(limit+n2+1, 2*limit, 2)
	}

	minOp := len(nums)
	for x := 2; x <= 2*limit; x++ {
		totCnt, opCnt := segmentTree.QueryPoint(x)
		if totCnt == n/2 && opCnt < minOp {
			minOp = opCnt
		}
	}

	return minOp
}

欢迎添加公众号:彬彬魔坊,更多数据结构,象棋,台球,魔方,计算机相关知识
leetcode-1674-使数组互补的最少操作次数-线段树_第2张图片

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