单调性

单调性是数学中使用的一种常见性质,通常用于描述函数,在高等数学中的定义常常为:

设函数f(x)在区间I上有定义,如果对于I上的任意两个数x1和x2,当x1<x2时,有f(x1)<f(x2)(或者f(x1)>f(x2)),则称函数f(x)在区间I上是单调递增的(或者单调递减的)。

例如如下图像就是两个单调函数。

利用单调性我们可以减少很多重复的运算。例如,对于如下函数,我们给定其定义域为[0,+∞),现在要求查找出在其定义域内所有f(x)即y大于0.5的区间

  • 如果不借助单调性,我们需要采用遍历的方法,依次遍历定义域中的所有点x,判断其f(x)是否满足条件(大于0.5)。
  • 如果借助单调性,我们知道上述函数是严格单调递增的,其图像如下:



    绿线表示y=0.5的图像,处理该问题,只需要找到方程0.5=(1/3)x^3的解x0,由于函数具有单调性,且单调递增,因此,所有大于x0的区间内的x其f(x)都满足大于0.5。

对于计算机语言来说,用于表示函数的常见数据结构就是数组,我们可以通过

  1. 原数组本身的单调性
  2. 构造单调性

简化许多运算。下面引入几个例子:

15. 三数之和

823. 带因子的二叉树

15. 三数之和

题目:给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意

  1. 答案中不可以包含重复的三元组
  2. 3 <= nums.length <= 3000
  3. -10^5 <= nums[i] <= 10^5

按照最朴素的解决方法,三层循环,循环遍历整个数组,然后再对整个结果进行去重,便可以解决该问题,但是时间复杂度为O(n^3),由于过于简单,这里给出伪代码:

list = [][]int{}
for i:=0;i <= len(nums)-3;i ++ {
for j:=i+1;j <= len(nums)-2;j ++ {
for k:= j+1;k < len(nums)-1;k ++ {
if nums[i]+nums[j]+nums[k] == 0 {
list = append(list, []int{nums[i],nums[j],nums[k]})
}
}
}
}
对list去重

本题目首先要求我们去重,因为返回结果要求不重复,对于去重常见的做法:

  1. 使用数据结构set、map进行处理,但是会额外占用内存
  2. 对原始数据排序,然后按序处理跳过重复项

优化掉去重问题后,我们可以尝试对内层的两层for循环进行优化,这里就引入了一个经典的方法:构造单调性,根据单调性进行查找

巧妙的方法

如果nums[i]确定,那么我们只需要寻找满足条件nums[j]+nums[k]=-nums[i]j、k值,这就变成了一个二数之和的问题,暴力算法是直接进行遍历,然后查找该值,但是由于数组的有序性,我们有一种更加巧妙的方法:

  • 由于当前数组的有序性,保证了数组本身是单调递增(或递减的,这里以递增为例)
  • 设置指针p1、p2指向数组开头p1=i+1和结尾p2=len(nums)-1
  • pred=nums[p1]+nums[p2],target=-nums[i]
    • if target < pred,由于数组递增,nums[p2-1]<num[p2],因此,p2 --
    • if target > pred,由于数组递增,nums[p1+1]>num[p1],因此,p1 ++
    • if target == pred,找到了目标,但为防止遗漏数据还要继续查找,此时指针向任意方向移动都没有影响,可以p1 ++或者p2 --
  • 直到p1 >p2则可以停止查找(=取决于需求,如果有需求可以>=或<=)

这个模式可以应用于很多地方,实际上具有单调性的函数一般都可以通过该办法查找,例如nums[j]*nums[k]=target,查找j、k。

例如,在[-1,0,1,2,-1, 3]这个数组中,查找nums[j]+nums[k]=4nums[j]和nums[k]的值,现对其进行排序,然后用上述方法进行处理:

了解了这个模式后,我们给出解决该问题的代码:

解题代码以及注释

import "sort"

func threeSum(nums []int) [][]int {
result := [][]int{}
sort.Ints(nums)
// 尝试固定i,然后将3数之和转化为两数之和
for i := 0; i < len(nums)-2; i++ {
// 对nums[i]进行去重
if i-1 >= 0 && nums[i-1] == nums[i] {
continue
}
sum := -nums[i]
left := i + 1
right := len(nums) - 1
// 解决两数之和问题,寻找left、right使得nums[left]+nums[right]==sum
for left < right {
temp := nums[left] + nums[right]
if temp == sum {
result = append(result, []int{nums[i], nums[left], nums[right]})
// 去重nums[left]
for left < right && nums[left] == nums[left+1] {
left++
}
// 去重nums[right]
for left < right && nums[right] == nums[right-1] {
right--
}
left++
right--
} else if temp > sum {
right--
} else {
left++
}
}
}
return result
}

823. 带因子的二叉树

题目:给出一个含有不重复整数元素的数组arr,每个整数arr[i]均大于 1。用这些整数来构建二叉树,每个整数可以使用任意次数。其中:每个非叶结点的值应等于它的两个子结点的值的乘积。满足条件的二叉树一共有多少个?答案可能很大,返回 对 10^9+7 取余 的结果。

例如:输入: arr = [2, 4, 5, 10]

输出: 7

解释: 可以得到这些二叉树: [2], [4], [5], [10], [4, 2, 2], [10, 2, 5], [10, 5, 2]

该问题是一个树相关的问题,并且对于父子结点处理过程是类似的。举例说明这件事:

对于输入arr=[18, 3, 6, 2],页结点可以为[2][3][6][18],可以把未显示的结点看做空结点,对于顶点为6的树可以为[6,2,3]或者[6,3,2],就需要借助叶节点信息。对于顶点为18的树可以为[18,3,6],[18,6,3],而组成以[6]的顶点的组合有3个。可以看到该问题是个动态规划问题。

f(18)=f(3)*f(6)
f(18)=f(6)*f(3)
f(6)=f(3)*f(2)
f(6)=f(2)*f(3)
f(3)=1
f(2)=1

状态转换方程为:

\(f(a*b)= \begin{array}{ll}
f(a)*f(b)*2+1 & a!=b,a为左子树b为右子树,和a为右子树b为左子树\\
f(a)*f(b)+1, & a==b\\
\end{array}\)

那最后的问题就是查找在index属于[0,i-1]的数组中,哪些a,b满足arr[a]*arr[b]==arr[i],我们就可以使用上面提到的巧妙的方法类比解决该问题。这里就不再赘述。

解题代码和注释

func numFactoredBinaryTrees(arr []int) int {
sort.Ints(arr)
dp := make([]int64, len(arr))
res, mod := int64(0), int64(1e9 + 7)
for i := 0; i < len(arr); i++ {
dp[i] = 1
// 查找arr[left]*arr[right]==arr[right]*arr[left]
for left, right := 0, i - 1; left <= right; left++ {
for left <= right && int64(arr[left]) * int64(arr[right]) > int64(arr[i]) {
right--
}
if left <= right && int64(arr[left]) * int64(arr[right]) == int64(arr[i]) {
if left == right {
dp[i] = (dp[i] + dp[left] * dp[right]) % mod
} else {
dp[i] = (dp[i] + dp[left] * dp[right] * 2) % mod
}
}
}
res = (res + dp[i]) % mod
}
return int(res)
}

Leetcode刷题笔记——单调性的更多相关文章

  1. LeetCode刷题笔记和想法(C++)

    主要用于记录在LeetCode刷题的过程中学习到的一些思想和自己的想法,希望通过leetcode提升自己的编程素养 :p 高效leetcode刷题小诀窍(这只是目前对我自己而言的小方法,之后会根据自己 ...

  2. 18.9.10 LeetCode刷题笔记

    本人算法还是比较菜的,因此大部分在刷基础题,高手勿喷 选择Python进行刷题,因为坑少,所以不太想用CPP: 1.买股票的最佳时期2 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. ...

  3. LeetCode刷题笔记 - 12. 整数转罗马数字

    学好算法很重要,然后要学好算法,大量的练习是必不可少的,LeetCode是我经常去的一个刷题网站,上面的题目非常详细,各个标签的题目都有,可以整体练习,本公众号后续会带大家做一做上面的算法题. 官方链 ...

  4. LeetCode刷题笔记 - 2022

    这篇博客集中整理在LeetCode的刷题记录,方便查阅 258. 各位相加 - 力扣(LeetCode) (leetcode-cn.com) 代码 class Solution { public: i ...

  5. Leetcode刷题笔记(双指针)

    1.何为双指针 双指针主要用来遍历数组,两个指针指向不同的元素,从而协同完成任务.我们也可以类比这个概念,推广到多个数组的多个指针. 若两个指针指向同一数组,遍历方向相同且不会相交,可以称之为滑动窗口 ...

  6. LeetCode刷题笔记(1-9)

    LeetCode1-9 本文更多是作为一个习题笔记,没有太多讲解 1.两数之和 题目请点击链接 ↑ 最先想到暴力解法,直接双循环,但是这样复杂度为n平方 public int[] twoSum(int ...

  7. leetcode刷题笔记

    (1)Best Time to Buy and Sell Stock Total Accepted: 10430 Total Submissions: 33800My Submissions Say ...

  8. leetcode刷题笔记08 字符串转整数 (atoi)

    题目描述 实现 atoi,将字符串转为整数. 在找到第一个非空字符之前,需要移除掉字符串中的空格字符.如果第一个非空字符是正号或负号,选取该符号,并将其与后面尽可能多的连续的数字组合起来,这部分字符即 ...

  9. LeetCode刷题笔记-回溯法-分割回文串

    题目描述: 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串. 返回 s 所有可能的分割方案. 示例: 输入: "aab"输出:[ ["aa", ...

  10. leetcode刷题笔记231 2的幂

    题目描述: 给定一个整数,写一个函数来判断它是否是2的幂. 题目分析: 判断一个整数是不是2的幂,可根据二进制来分析.2的幂如2,4,8,等有一个特点: 二进制数首位为1,其他位为0,如2为10,4为 ...

随机推荐

  1. AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from

    函数里面return response问题: 1.没有 2.写错 3.位置错 例如: return Response('确认成功')

  2. 柏林噪声&幻想大陆地图生成

    序言 之前介绍过perlin噪声的实现,现在应用实践一下--程序化生成幻想大陆 这里使用的是perlin噪声倍频技术(也称分形噪声),详情传送门:柏林噪声算法 代码示例使用的是shadertoy的语法 ...

  3. Springboot+actuator+prometheus+Grafana集成

    本次示例以Windows示例 推荐到官网去下载:Windows版的应用程序 下载最新版 prometheus-2.37.8.windows-amd64 压缩包:解压就行 下载最新版 grafana-9 ...

  4. Go语言中的init函数: 特点、用途和注意事项

    1. 引言 在Go语言中,init()函数是一种特殊的函数,用于在程序启动时自动执行一次.它的存在为我们提供了一种机制,可以在程序启动时进行一些必要的初始化操作,为程序的正常运行做好准备. 在这篇文章 ...

  5. 2023-06-20:给定一个长度为N的数组arr,arr[i]表示宝石的价值 你在某天遇到X价值的宝石, X价值如果是所有剩余宝石价值中的最小值,你会将该宝石送人 X价值如果不是所有剩余宝石价值中的

    2023-06-20:给定一个长度为N的数组arr,arr[i]表示宝石的价值 你在某天遇到X价值的宝石, X价值如果是所有剩余宝石价值中的最小值,你会将该宝石送人 X价值如果不是所有剩余宝石价值中的 ...

  6. vue前端预览pdf并加水印、ofd文件,控制打印、下载、另存,vue-pdf的使用方法以及在开发中所踩过的坑合集

    根据公司的实际项目需求,要求实现对pdf和ofd文件的预览,并且需要限制用户是否可以下载.打印.另存pdf.ofd文件,如果该用户可以打印.下载需要控制每个用户的下载次数以及可打印的次数.正常的预览p ...

  7. Dev 使用RibbonForm打开多标签窗体,主窗体的Text显示一个

    最近在开发Dev的项目,一般我们主窗体上边只需要显示应用程序的名称就行了,不需要显示打开Tab页签的名称,百度了很久不知道怎么解决,官方文档只说,RibbonForm的标题是一个组合文本,由Ribbo ...

  8. prometheus Histogram 统计原理

    大家好,我是蓝胖子,书接上文,我在prometheus描点原理那一篇文章里,留了一个思考题: 我们通常会用到histogram_quantile去计算服务接口时间的耗时情况. histogram_qu ...

  9. WPF入门教程系列三十 ——DataGrid验证

    WPF入门教程系列目录 WPF入门教程系列二--Application介绍 WPF入门教程系列三--Application介绍(续) WPF入门教程系列四--Dispatcher介绍 WPF入门教程系 ...

  10. 基于DirectX11+ImGui的Win32桌面程序开发

    一.常见图形界面框架(DirectUI.GUI) 1.题外话,纯属扯O 举两个常用的开发框架,MFC和Qt Widget里面每个控件都是Window,这是和DirectUI最大的区别.下面简单梳理下这 ...