木厶笔记存放站木厶笔记存放站
首页
文章
首页
文章
  • Web

    • 网络

      • Cookie 和 Session
      • HTTP 优化
      • HTTP
      • HTTP1.1 首部字段
      • HTTPS
      • TCP 和 UDP
      • URI 和 URL
      • 输入URL_至页面显示的过程
    • 安全

      • CSRF 攻击
      • JSON Web Token
      • sql 注入
      • XSS 攻击
    • 浏览器

      • 提升SEO
      • 浏览器内核
      • 浏览器的进程和线程
      • 浏览器缓存机制
      • 跨域
    • 解决方案

      • 优化大量图片加载
      • 扫码登录
    • 性能优化

      • 性能分析
      • 资源请求优化
      • 运行时优化
      • 重排重绘
      • 静态资源优化
  • HTML

    • 基础

      • Doctype
      • src 和 href
      • title 和 alt
    • HTML5

      • Audio 和 Video
      • Canvas
      • cookie session Storage localStorage
      • Drag
      • Svg
      • WebSocket
      • WebWorker
      • 标签
      • 离线缓存manifest
  • CSS

    • 基础

      • BFC
      • CSS 动画
      • CSS 架构
      • display float position
      • float 浮动
      • 元素不可见
      • 块级元素和行内元素
      • 层叠顺序与堆栈上下文
      • 引入外部CSS
      • 盒模型
      • 雪碧图
    • 布局

      • flex 布局--实用
      • flex 布局
      • Grid 布局--实用
      • Grid 布局
      • 三栏布局
      • 两栏布局
      • 响应式布局
      • 移动端布局
    • 实用

      • 修改svg图片颜色
      • 初始化 CSS
      • 换肤
      • 文本省略号
      • 消除 inline-block 间隙
      • 自定义字体
    • CSS-揭秘

      • 2-1 半透明边框
      • 2-2 多重边框
      • 2-3 灵活的背景定位
      • 2-4 边框内圆角
      • 2-5 条纹背景
      • 2-6 复杂的背景图案
      • 2-8 连续的图像边框
      • 3-1 自适应的椭圆
      • 3-2 平行四边形
      • 3-3 菱形图片
      • 3-4 切角效果
      • 3-5 梯形标签页
      • 3-6 简单的饼图
      • 4-1 单侧投影
      • 4-2 不规则投影
      • 4-3 染色效果
      • 4-4 毛玻璃效果
      • 4-5 折角效果
      • 5-1 连字符断行
      • 5-2 插入换行
      • 5-3 文本行的斑马线条
      • 5-6 华丽的 & 符号
      • 5-7 自定义下划线
      • 5-8 现实中的文字效果
      • 5-9 环形文字
      • 6-1 选用合适的鼠标光标
      • 6-2 扩大可点击区域
      • 6-3 自定义复选框
      • 6-4-5 通过阴影(模糊)来弱化背景
      • 6-6 滚动提示
      • 6-7 交互式的图片对比控件
      • 7-1 自适应内部元素
      • 7-2 精确控制表格列宽
      • 7-3 根据兄弟元素的数量来设置样式
      • 7-4 满幅的背景,定宽的内容
      • 7-5 垂直居中
      • 7-6 紧贴底部的页脚
      • 8-1 缓动效果
      • 8-2 逐帧动画
      • 8-3 闪烁效果
      • 8-4 打字动画
      • 8-5 状态平滑的动画
      • 8-6 沿环形路径平移的动画
    • CSS 选择器

      • 伪类
      • 属性选择器
      • 数学函数
      • 树结构伪类
      • 选择器优先级
      • 选择符
      • 逻辑选择器
    • 奇妙属性

      • @property
      • mask
  • JavaScript

    • 基础

      • 2 script标签
      • 3-1 语法
      • 3-3 var let const
      • 3-4-1 typeof 和 instanceof
      • 3-4-2 Undefined 类型
      • 3-4-3 Null 类型
      • 3-4-4 Boolean 类型
      • 3-4-5 Number 类型
      • 3-4-6 String 类型
      • 3-4-7 Symbol 类型
      • 3-4-8 Object 类型
      • 3-4-9 BigInt 类型
      • 3-5 操作符
      • 3-6 隐式转换
      • 4-1 原始值和引用值
      • 4-2 执行上下文与作用域
      • 4-3 垃圾回收
      • 5-1 Date
      • 5-2 RegExp
      • 5-3 原始值包装类型
      • 5-4-1 Global 单例内置对象
      • 5-4-2 Math 单例内置对象
      • 6-2 Array
      • 6-3 Map
      • 6-4 WeakMap
      • 6-5 Set
      • 6-6 WeakSet
      • 7-1 迭代器与生成器
      • 8-1-1 Object 属性
      • 8-1-2 Object 构造函数方法
      • 8-1-3 Object 语法增强和解构
      • 8-2 Object 创建
      • 8-3 原型和继承
      • 8-4 class 类
      • 8-5 this
      • 8-6 可选链和空值合并运算符
      • 9 proxy 代理与反射
      • 10-1 Function 函数
      • 10-2 apply call bind
      • 10-3 闭包
      • 10-4 私有变量
      • 10-5 扩展运算符和 rest
      • 11-1 event loop 事件循环机制
      • 11-2 Promise 期约
      • 11-3 async await
      • 12-1 BOM
      • 14-1-1 DOM 节点
      • 14-1-2 DOM Document 类型
      • 14-1-3 DOM Element 类型
      • 14-1-4 DOM Text 和 Comment 类型
      • 14-2 动态脚本和动态样式
      • 14-3 MutationObserver 监听节点变化
      • 14-4 Intersection Observer
      • 16-2-1 操控样式
      • 16-2-2 元素尺寸
      • 16-3 DOM 深度优先遍历
      • 17-1 事件流
      • 17-2 事件处理程序
      • 17-3 事件对象
      • 17-4-1 UI 和焦点事件
      • 17-4-2 鼠标和滚轮事件
      • 17-4-3 键盘与输入事件
      • 17-4-4 HTML5 事件
      • 17-4-5 设备和触摸事件
      • 17-6 模拟事件
      • 18-1 requestAnimationFrame&requestIdleCallback
      • 18-2 canvas
      • 19-1 表单基础
      • 19-2 文本框编程
      • 19-3 选择框编程
      • 19-5 富文本编辑器
      • 20-10 Perfromance API
      • 20-11-1 HTML 模板
      • 20-11-2 影子 DOM
      • 20-11-3 自定义元素
      • 20-14 MessageChannel和BroadcastChannel
      • 20-15 AbortController
      • 20-4 File API 与 Blob API
      • 20-7 Notifications API
      • 20-8 Page Visibility API
      • 20-9 Streams API
      • 23 JSON
      • 24-1 XMLHttpRequest
      • 24-5 Fetch API
      • 26-1 模块语法
      • 26-2 CommonJs 与 ES6 Module 的差异
    • 设计模式

      • 单例模式
      • 发布订阅模式
      • 工厂模式
      • 策略模式
      • 装饰器模式
      • 观察者模式
      • 适配器模式
    • 手写实现

      • apply call bind
      • flat
      • instanceof
      • JSONP
      • new
      • Promise API
      • trim
      • 数组去重
      • 柯里化
      • 深拷贝
      • 防抖和节流
    • 场景题

      • 映射 URL 参数
      • 模拟红绿灯
      • 请求-丢弃旧时序的请求
      • 请求-并发请求
      • 通过 value 找 key
      • 闭包题
    • 移动端

      • touch 事件
      • visualViewport
      • 像素
      • 移动端布局
      • 视口 Viewport
    • 实用

      • better-scroll 滚动组件
      • Proxy实现英文字母升降序
      • 区别数组和对象
      • 图片懒加载
      • 按首字母排序的列表
      • 控制粘贴板
    • PIXI

      • 1 Application
      • 2 Graphics
      • 3 loader
      • 4 Sprite
      • 5 Spine
      • 6 事件
      • 7 Renderer
  • Node

    • 基础

      • crypto 加密模块
      • fs 模块
      • http 模块
      • mysql 模块
      • redis 模块
    • 框架

      • express
      • koa2
    • 实用

      • @elasticelasticsearch
      • restful Mock 数据
      • 自定义 Mock 数据
    • 错误

      • pm2-watch报错 502
      • spawn 中文乱码
  • Jquery

    • 基础

      • 事件
      • 动画
      • 工具方法
      • 操作 dom
      • 获取元素
  • TypeScript

    • 环境

      • config
      • 环境
    • 基础

      • 01 原始类型和特殊类型
      • 02 字面量和类型拓宽
      • 03 interface 和 type
      • 04 数组和元组
      • 05 class(类)
      • 06 函数和重载
      • 07 联合类型和交叉类型
      • 08 泛型
      • 09 类型推断和类型断言
      • 10 匹配提取
      • 11 重新构造
      • 12 递归循环
      • 13数组长度做计数
      • 14 特殊特性
      • 15 内置高级类型
      • 16 inter extends
      • 17 协变和逆变
  • Vue

    • 环境

      • 安装
      • 自定义环境变量
    • 基础(2.x)

      • data
      • keep-alive
      • nextTick
      • props和sync
      • ref
      • v-for和v-if
      • 事件绑定
      • 动态组件
      • 动画
      • 循环渲染
      • 插槽 slot
      • 条件渲染
      • 样式绑定
      • 模板语法
      • 生命周期
      • 组件通讯
      • 自定义指令
      • 表单绑定
      • 计算属性和监听器
    • 基础(3.x)

      • Composition API
      • Script setup
      • Suspense
      • sync 语法糖
      • Teleport
      • vue3的升级点
      • 常用动画
      • 生命周期
    • router

      • vue-router 3
      • vue-router 4
    • vuex

      • pinia
      • vuex
      • vuex4
      • 刷新不丢失 vuex
    • 底层

      • MVVM
      • 双向绑定
      • 响应式
      • 模板编译
      • 渲染过程
      • 虚拟DOM和diff算法
    • 应用

      • 权限管理
  • React

    • 环境

      • 安装
      • 自定义环境变量
    • 基础

      • JSX
      • ref
      • SCU
      • 不可变数据 setState
      • 事件
      • 动画
      • 异步组件
      • 插槽
      • 条件
      • 生命周期
      • 组件公共逻辑抽离
      • 组件通讯
      • 表单
      • 逃离组件 portals
    • hooks

      • hooks
      • react-query
      • useClickOutSide —— 点击外面
      • useDebounce —— 防抖
      • useURLQueryParam —— 输入框值与search绑定
    • router

      • react-router-com 6
      • react-router-dom hooks
      • react-router-dom
    • redux

      • react-redux
      • redux 与 hooks
      • redux-persist
      • redux-thunk
      • redux-toolkit
      • redux
    • 底层

      • JSX本质
      • setState 原理
      • 合成事件
    • 实用

      • CSS-in-JS
      • 字体库
      • 数组优化为哈希表
      • 组件的子元素只能是规定的元素
  • Echarts

    • 基础

      • 仪表盘
      • 基础
      • 折线图
      • 散点图
      • 柱状图
      • 雷达图
      • 饼图
  • Electron

    • 环境

      • 基本安装
      • 集合 react
    • 基础

      • Dialog
      • 原生菜单
      • 右键菜单
      • 进程
  • 前端工程化

    • babel

      • babel7 实践
      • 工作原理
      • 生态
    • Browserslist

      • 基础
    • npm

      • npm模块安装机制
      • npm脚本
    • qiankun

      • 隔离原理
    • Vite

      • css
      • gzip-打包
      • vite config 常见配置
      • 环境
      • 静态资源
    • webpack

      • css环境
      • js&ts环境
      • loader
      • loader和plugin的区别及编写
      • plugin
      • splitChunks
      • webpack5--模块联邦
      • 代码压缩
      • 优化性能
      • 图片
      • 性能分析工具
      • 提高构建速度
      • 构建性能--并行
      • 构建性能--持久化缓存
      • 热更新
    • 代码提交规范

      • husky&lint-staged
  • Java

    • 环境

      • idea 创建 maven-archetype-webapp
      • spring boot web 的配置参考
      • spring 从初始配置
      • 与 git 集合
    • Web 基础

      • mybatis pager的应用
      • [] 和 List 和 Set
      • 全局 cors 跨域
      • 接口例子——登录
      • 文件接口——图片
      • 有效时间的唯一字符串
      • 自定义类
      • 通用的泛型服务端响应对象
  • Elastic

    • 基础

      • 分词规则
      • 基础概念
      • 基础语法
      • 环境搭建
    • 实用

      • canal——数据库准实时导入
      • logstash——数据库基于时间轴导入
  • Mysql

    • 基础

      • 字段操作
      • 数据库操作
      • 查询操作
      • 表操作
  • Python

    • 基础

      • 1-1-Number
      • 1-2-String
      • 1-3-list和tuple
      • 1-4-序列
      • 1-5-set
      • 1-6-dict
      • 2-1-运算符
      • 2-2-对象比较
      • 3-1-条件判断
      • 3-2-循环
      • 4-1-模块-包
      • 4-2-模块-__init__
      • 4-3-模块-内置变量
      • 4-4-模块-导入
      • 5-1-解包
      • 5-2-函数参数
      • 5-3-作用域
      • 5-4-高阶函数&三元表达式
      • 6-1-类
      • 6-2-类的继承
      • 6-3-枚举
      • 7-1-json
  • Flutter

    • 环境

      • 创建及运行项目
      • 第三方库
      • 运行环境
      • 静态资源
    • Dart

      • dynamic var object
      • List
      • Map
      • Number
      • Set
      • String
      • URI
      • 函数
      • 变量
      • 库
      • 异步
      • 流程控制语句
      • 类
      • 运算符
    • 基础

      • 1 Widget
      • 2 StatelessWidget & StatefulWidget
      • 3 State生命周期
      • 4 状态管理
      • 5 路由
    • 基础组件

      • Button
      • Text
      • 单选开关和复选框
      • 图片及ICON
      • 表单
      • 输入框
      • 进度指示器
    • 布局组件

      • 1 布局约束
      • 2 线性布局(Row & Column)
      • 3 弹性布局(Flex)
      • 4 流式布局(Wrap & Flow)
      • 5 层叠布局(Stack & Positioned)
      • 6 绝对定位(Align)
      • 7 LayoutBuilder & AfterLayout
    • 容器类组件

      • Container
      • Scaffold
      • 变换-Transform
      • 空间适配-FittedBox
      • 裁剪-Clip
      • 装饰-DecoratedBox
    • 可滚动组件

      • SingleChildScrollView
      • 通用属性
  • Git

    • 基础

      • Git commit message 规范
    • 实用

      • cherry-pick
      • stash
      • 分支&修改最近一次commit
      • 撤销
  • 算法

    • 基础

      • leetcode
      • 时间复杂度
    • 收录

      • 阿拉伯数字转中文
    • 数据结构

      • 哈希表、集合
      • 并查集
      • 数组、链表、跳表
      • 栈、队列
      • 树、二叉树、二叉搜索树
    • 算法

      • DFS 和 BFS
      • 二分查找
      • 二叉树路径问题题目
      • 动态规划2--基础题
      • 动态规划2--背包问题
      • 区间类题目
      • 岛屿类题目
      • 排列组合子集类题目
      • 排序算法
      • 摩尔投票
      • 递归
      • 链表
  • 部署

    • centos8

      • bitwarden_rs
      • ElasticSearch
      • git
      • https及http2
      • mac 向 centos 传输文件
      • nginx
      • nvm
    • 其他

      • docker
      • sitemap
      • 重装系统
  • 图形学

    • 基础

      • 1 三角函数
      • 2 斜率k
      • 3 向量
      • 4 矩阵
  • AI

    • langgraph

      • 1-01-图
      • 1-02-1-状态
      • 1-02-2-消息状态
      • 1-03-节点
      • 1-04-边
      • 1-05-Send
      • 1-06-Command
      • 1-07-配置-configurable
      • 1-08-1-内存持久性
      • 1-08-2-删除持久消息
      • 1-08-3-向量数据库
      • 1-08-4-持久上下文token优化
      • 1-09-1-工具
      • 1-09-2-大量工具优化
      • 1-10-1-人机交互-interrupt
      • 1-10-2-人机交互-interrupt_before
      • 1-11-1-流式输出
      • 1-11-2-自定义流式传输
      • 1-11-3-输出特定的流式消息
      • 1-12-1-子图
      • 1-13-1-ReAct
    • mcp

      • 服务器——python
      • 服务器——ts

动态规划2--基础题

最大子序和(53)

题目

  • 状态:dp[i] 表示以 nums[i] 结尾 的 连续 子数组的最大和
  • 方程
dp[i]={dp[i−1]+nums[i],if dp[i−1] > 0nums[i],if dp[i−1] ≤ 0

因为求最大值,所以可以直接取最大值

dp[i]=max{nums[i],dp[i−1]+nums[i]}
public int maxSubArray(int[] nums) {
	int len = nums.length;
	int[] dp = new int[len];
	dp[0] = nums[0];
	int res = nums[0];
	
	for (int i = 1; i < len; i++) {
		dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
		res = Math.max(res, dp[i]);
	}
	
	return res;
}

观察到只与 dp[i] 只与 dp[i - 1] 有关,顾可降维,优化空间复杂度

public int maxSubArray(int[] nums) {
	int dp = nums[0], res = nums[0];
	
	for (int i = 1; i < nums.length; i++) {
		dp = Math.max(nums[i], dp + nums[i]);
		res = Math.max(res, dp);
	}
	
	return res;
}

不同路径(62)

题目

  • 状态:dp[y][x] 表示走到坐标 (y, x) 的路径总数
  • 方程:走到坐标 (y, x) 可以从上方下来,也可以从左边过来,路径总数是二者之和
dp[y][x]=dp[y][x−1]+dp[y−1][x]
public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for (int y = 0; y < m; y++) dp[y][0] = 1;
    for (int x = 0; x < n; x++) dp[0][x] = 1;
    for (int y = 1; y < m; y++) {
        for (int x = 1; x < n; x++) {
            dp[y][x] = dp[y][x - 1] + dp[y - 1][x];
        }
    }
    return dp[m - 1][n - 1];
}

观察到左边第一行和上边第一行,肯定都为 1,因为 dp[y, x] 肯定与 dp[y - 1, x] 有关,可以用一维数组记录一行每列的dp 值,新一行继承上一行的值,再加上从左边过来的

public int uniquePaths(int m, int n) {
    int[] dp = new int[n];
    dp[0] = 1;
    for (int y = 0; y < m; y++) {
        for (int x = 1; x < n; x++) {
            dp[x] += dp[x - 1];
        }
    }
    return dp[n - 1];
}

不同路径 II(63)

题目

  • 状态:dp[y][x] 表示走到坐标 (y, x) 的路径总数
  • 方程
无障碍物有障碍物dp[y][x]={dp[y−1][x−1],(y,x) 无障碍物0,(y,x) 有障碍物
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length, n = obstacleGrid[0].length;
    int dp[][] = new int[m][n];
    for (int y = 0; y < m && obstacleGrid[y][0] == 0; y++) dp[y][0] = 1;
    for (int x = 0; x < n && obstacleGrid[0][x] == 0; x++) dp[0][x] = 1;
    for (int y = 1; y < m; y++) {
        for (int x = 1; x < n; x++) {
            if (obstacleGrid[y][x] == 0) {
                dp[y][x] = dp[y - 1][x] + dp[y][x - 1];
            }
        }
    }
    return dp[m - 1][n - 1];
}

降维,优化空间复杂度。dp[x] += dp[x - 1] 即从左边过来的,y++ 即从上边过来的

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length, n = obstacleGrid[0].length;
    int dp[] = new int[n];
    dp[0] = obstacleGrid[0][0] == 1 ? 0 : 1;
    for (int y = 0; y < m; y++) {
        for (int x = 0; x < n; x++) {
            if (obstacleGrid[y][x] == 1) { // 有障碍物,则不可能从上面或左边过来了
                dp[x] = 0; // 清除上一行缓存,即不可能从上面过来
                continue;  // 不走下面判断,即不可能从左边过来
            }
            if (x > 0) dp[x] += dp[x - 1];
        }
    }
    return dp[n - 1];
}

最小路径和(64)

题目

  • 状态:dp[y][x] 表示走到坐标 (y, x) 的最小路径和
  • 方程
dp[y][x]=max{dp[y−1][x],d[y][x−1]}+grid[y][x]
public int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];
    for (int y = 1; y < m; y++) dp[y][0] = dp[y - 1][0] + grid[y][0];
    for (int x = 1; x < n; x++) dp[0][x] = dp[0][x - 1] + grid[0][x];
    for (int y = 1; y < m; y++) {
        for (int x = 1; x < n; x++) {
            dp[y][x] = Math.min(dp[y - 1][x], dp[y][x - 1]) + grid[y][x];
        }
    }
    return dp[m - 1][n - 1];
}

这里降维需要多增加判断,即增加时间复杂度,所以就不降纬了

爬楼梯(70)

题目

  • 状态:dp[i] 表示爬到第 i 阶楼梯的方法数
  • 方程
dp[i]=dp[i−1]+dp[i−2]
public int climbStairs(int n) {
    int[] dp = new int[n + 1];
    dp[0] = dp[1] = 1;
    for (int i = 2; i < n + 1; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

降维,优化空间复杂度。

public int climbStairs(int n) {
    int a = 0, b = 1, c = 1;
    for (int i = 2; i < n + 1; i++) {
        a = b;
        b = c;
        c = a + b;
    }
    return c;
}

解码方法(91)

题目

  • 状态:dp[i] 表示到字符串第 i 位置时有多少种解码方法
  • 方程
dp[i]={dp[i−1],1 ≤ a ≤ 9dp[i−2],10 ≤ b ≤ 26dp[i−1]+dp[i−2],1 ≤ a ≤ 9, 10 ≤ b ≤ 26
public int numDecodings(String s) {
    char[] cs = s.toCharArray();
    if (cs[0] == '0') return 0;
    int len = cs.length;

    int[] dp = new int[len + 1];
    dp[0] = dp[1] = 1;
    for (int i = 1; i < len; i++) {
        int a = cs[i] - '0', b = (cs[i - 1] - '0') * 10 + a;
        if (a >= 1 && a <= 9) dp[i + 1] = dp[i];
        if (b >= 10 && b <= 26) dp[i + 1] += dp[i - 1];
    }
    return dp[len];
}

不同的二叉搜索树(96)

题目

  • 状态:dp[i] 代表有 i 个节点时,一共有多少种二叉搜索树
  • 当有 n 个节点时,以 m 作为根节点,一共有 f(m - 1) * f(n - m) 种二叉搜索树,则 dp[i] = f(1) + ... + f(i)
  • 而算 dp[i] 时,会依赖 dp[0] 到 dp[i - 1] 的结果(缓存下来,不用重复计算)

 public int numTrees(int n) {
    int[] dp = new int[n + 1];
    dp[0] = dp[1] = 1; // dp[0] 为 1,保证子树节点个数为 0 时,也能正常计算

    for (int i = 2; i < n + 1; i++) { // i 表示现在一共有多少个节点
        for (int j = 1; j <= i; j++) { // j 表示现在的根节点
            dp[i] += dp[j - 1] * dp[i - j];
        }
    }

    return dp[n];
}

三角形最小路径和(120)

题目

  • 状态:dp[y][x] 代表三角形中 (y, x) 位置的最小路径和
  • 方程
dp[y][x]=min{dp[y+1][x],dp[y+1][x+1]}+t[y][x]
public int minimumTotal(List<List<Integer>> triangle) {
    int n = triangle.size();
    int[][] dp = new int[n + 1][n + 1]; // 防止越界

    for (int y = n - 1; y >= 0; y--) { // 自底向上,从最后一行开始
        for (int x = 0; x <= y; x++) {
            dp[y][x] = Math.min(dp[y + 1][x], dp[y + 1][x + 1]) + triangle.get(y).get(x);
        }
    }

    return dp[0][0];
}

降维,减少空间复杂度。这一行只跟下一行有关,而且下一行的结果都保存在 dp 里

public int minimumTotal(List<List<Integer>> triangle) {
    int n = triangle.size();
    int[] dp = new int[n + 1]; // 防止越界

    for (int y = n - 1; y >= 0; y--) { // 自底向上,从最后一行开始
        for (int x = 0; x <= y; x++) {
            dp[x] = Math.min(dp[x], dp[x + 1]) + triangle.get(y).get(x);
        }
    }

    return dp[0];
}

单词拆分(139)

题目

  • 状态:dp[i]表示前 i 个字符是否可以在字典中找到
public boolean wordBreak(String s, List<String> wordDict) {
    int n = s.length();
    Set set = new HashSet(wordDict);
    boolean[] dp = new boolean[n + 1];
    dp[0] = true; // 空字符串

    for (int r = 1; r <= n; r++) { // 字串的右边界(不包括)
        for (int l = 0; l <= r - 1; l++) { // 字串的左边界
            // dp[1] 表示第一个字符是否可以在字典中找到
            // substring(1, r) 表示从第二个字符开始截取
            if (dp[l] && set.contains(s.substring(l, r))) {
                dp[r] = true;
                break;
            }
        }
    }

    return dp[n];
}

乘积最大子数组(152)

题目

  • 状态
    • maxDp[i] 表示以 i 结尾的子数组的最大乘积
    • minDp[i] 表示以 i 结尾的子数组的最小乘积
  • 方程:由于存在负数,那么会导致最大的变最小的,最小的变最大的
maxDp[i]=max{nums[i],maxDp[i−1]×nums[i],minDp[i−1]×nums[i]}minDp[i]=min{nums[i],maxDp[i−1]×nums[i],minDp[i−1]×nums[i]}
public int maxProduct(int[] nums) {
    int n = nums.length, max = nums[0];
    int[] maxDp = new int[n];
    int[] minDp = new int[n];
    maxDp[0] = nums[0]; minDp[0] = nums[0];

    for (int i = 1; i < n; i++) {
        maxDp[i] = Math.max(nums[i], Math.max(maxDp[i - 1] * nums[i], minDp[i - 1] * nums[i]));
        minDp[i] = Math.min(nums[i], Math.min(maxDp[i - 1] * nums[i], minDp[i - 1] * nums[i]));
        max = Math.max(max, maxDp[i]);
    }
    return max;
}

打家劫舍(198)

题目

  • 状态:dp[i] 代表前 i 个房子在满足条件下的能偷窃到的最高金额
  • 方程:不抢第 i 家,即为 dp[i - 1];抢第 i 家,即为 dp[i - 2] + nums[i]
dp[i]=max{dp[i−1],dp[i−2]+nums[i]}
public int rob(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n + 1];
    dp[0] = 0; dp[1] = nums[0];

    for (int i = 2; i < n + 1; i++) {
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
    }
    return dp[n];
}

空间优化,dp[i] 只和 dp[i - 1] 及 dp[i - 2] 有关,所以完全可以用两个变量完成

public int rob(int[] nums) {
     int a = 0, b = 0; // a: -2 家,b:-1 家
     for (int num : nums) { // num:第 i 家(i 从 0 开始)
         int dp = Math.max(b, a + num);
         a = b;
         b = dp;
     }
     return b;
 }

最大正方形(221)

题目

  • 状态:dp[y][x] 是以 matrix(y - 1, x - 1) 为 右下角 的正方形的最大边长
  • 方程:格子为 1 ,若要形成的正方形,则需要左上、左、上均为 1,想要形成更大的正方形,根据木桶短板原理,取最小一个
dp[y][x]=min{dp[y−1][x−1],dp[y][x−1],dp[y−1][x]}+1

public int maximalSquare(char[][] matrix) {
    int n = matrix.length, m = matrix[0].length, max = 0;
    int[][] dp = new int[n + 1][m + 1];

    for (int y = 0; y < n; y++) {
        for (int x = 0; x < m; x++) {
            if (matrix[y][x] == '1') {
                dp[y + 1][x + 1] = Math.min(dp[y][x], Math.min(dp[y][x + 1], dp[y + 1][x])) + 1;
                max = Math.max(max, dp[y + 1][x + 1]);
            }
        }
    }
    return max * max;
}

完全平方数(279)

题目

  • 状态:dp[i] 表示i的完全平方和的最少数量
  • 方程:dp[i] = min(dp[i], dp[i - j * j] + dp[j * j]),但 dp[j * j] 必定为 1,所以有以下简写
dp[i]=min{dp[i],dp[i−j∗j]+1}
public int numSquares(int n) {
    int[] dp = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        dp[i] = i; // 最差情况,1 + 1 + 1 = 3
        for (int j = 1; i - j * j >= 0; j++) { // 找一个完全平方数(j * j)
            dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
        }
    }
    return dp[n];
}

最长递增子序列(300)

题目

  • dp[i] 代表 nums 以 nums[i] 结尾的最长子序列长度
  • 方程
    • 以第 i 个数字为结尾,即要求 nums[i] 必须被选取,则初始长度至少为 1
    • 如果当前的数 nums[i] 大于之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的 LIS 。把前面的 i 个数都看了, LIS[i] 就是它们的最大值加 1。即比当前数要小的那些里头,找最大的,然后加 1。
dp[i]=max{dp[i],dp[j]+1}if  j < i  &  nums[i] > nums[j] 
public int lengthOfLIS(int[] nums) {
    int n = nums.length, max = 1;
    int[] dp = new int[n];
    Arrays.fill(dp, 1); // 自己本身就是一个子序列

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        max = Math.max(max, dp[i]);
    }

    return max;
}

整数拆分(343)

题目

  • 状态:dp[i] 表示数字 i 能够被拆分的最大乘积
  • 方程:将 i 挨个可能拆了,找到最大值
dp[i]=max{dp[i],j∗(i−j),j∗dp[i−j]}
public int integerBreak(int n) {
    int[] dp = new int[n + 1];
    dp[1] = 1;

    for (int i = 2; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
        }
    }

    return dp[n];
}

dp[1] = 1 当 i = 2,j = 1 时用到了 dp[i],所以需要初始化

Math.max(j * (i - j), j * dp[i - j]) 存在情况,j * (i - j) 大于 j * dp[i - j]

  • 如 2 = 1 + 1,dp[2] = 1*1 = 1,而当 3 拆成 1+2 时,dp[3] 是等于 1*2 的,而不是 1*dp[2]

摆动序列(376)

题目

  • 状态
    • up[i] 表示 nums[0:i] 中最后两个数字递增的最长摆动序列长度
    • down[i] 表示 nums[0:i] 中最后两个数字递减的最长摆动序列长度
  • 方程:情况为上升时,则 up 在 down 上加一,为下降时,则 down 在 up 上加一,其他时候往上继承
up[i]={down[i−1]+1,nums[i - 1] < nums[i]up[i],nums[i - 1] ≥ nums[i]down[i]={down[i−1],nums[i - 1] ≤ nums[i]up[i−1]+,nums[i - 1] < nums[i]
public int wiggleMaxLength(int[] nums) {
    int n = nums.length;
    int[] up = new int[n], down = new int[n];
    up[0] = 1; down[0] = 1; // 虽然一个数不形成摆动序列,但数量为一。当形成摆动序列时,这个数也想算上的

    for (int i = 1; i < n; i++) {
        up[i] = up[i - 1];
        down[i] = down[i - 1];
        if (nums[i - 1] < nums[i]) { // 上升,则在下降序列基础上加一
            up[i] = down[i - 1] + 1;
        } else if (nums[i - 1] > nums[i]) { // 下降,则在上升序列基础上加一
            down[i] = up[i - 1] + 1;
        }
    }

    return Math.max(up[n - 1], down[n - 1]);
}

可观察到只与前一个变量有关,故可降维,降低空间复杂度

public int wiggleMaxLength(int[] nums) {
   int up = 1, down = 1;
   for (int i = 1; i < nums.length; i++) {
       if (nums[i - 1] < nums[i]) {
           up = down + 1;
       } else if (nums[i - 1] > nums[i]) {
           down = up + 1;
       }
   }

   return Math.max(up, down);
}

组合总和 Ⅳ(377)

题目

  • 状态:dp[i] 表示凑成总和为 i 的组合总和
  • 方程
dp[i]=∑j=0n−1∫dp[i−nums[j]]ifi≥nums[j]

以 nums = [1, 2, 3] , target = 4 为例

  • dp[0] = 1
  • dp[1] = dp[0](选择1)= 1
  • dp[2] = dp[0](选择2)+ dp[1](选择1)= 2
  • dp[3] = dp[0](选择3)+ dp[1](选择2)+ dp[2](选择1)= 4
  • dp[4] = dp[1](选择3)+ dp[2](选择2)+ dp[3](选择1)= 7
public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target + 1];
    dp[0] = 1; // 没有放任何物体进背包也算一种

    for (int i = 1; i <= target; i++) { // 确定背包容量
        for (int num : nums) { // 尝试所有可能
            if (i - num >= 0) dp[i] += dp[i - num];
        }
    }

    return dp[target];
}

等差数列划分(413)

题目

  • 状态:dp[i] 表示以 nums[i] 结尾的、且长度大于等于 3 的连续等差数列的个数
    • 必需以 nums[i] 结尾,nums[i] 必需被选取
    • 长度大于等于 3
    • 记录的是个数,就是题目要我们找的长度大于等于 3 的连续子数组(且是等差数列)的个数
  • 方程:如果 nums[i] 能够接在 nums[i - 1] 之后形成一个长度更长的(在原数组上连续的)等差数列,那么 dp[i] = dp[i - 1] + 1
dp[i]=dp[i−1]+1ifnums[i]−nums[i−1]==nums[i−1]−nums[i−2]
public int numberOfArithmeticSlices(int[] nums) {
    int n = nums.length, res = 0;
    int[] dp = new int[n];

    for (int i = 2; i < n; i++) {
        if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
            dp[i] = dp[i - 1] + 1;
            res += dp[i];
        }
    }

    return res;
}

斐波拉契数列(509)

题目

  • 状态:dp[i] 表示第 i 个斐波拉契数值
  • 方程
dp[i]=dp[i−1]+dp[i−2]
public int fib(int n) {
    if (n < 2) return n;
    int[] dp = new int[n + 1];
    dp[1] = 1;

    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    return dp[n];
}

使用最小花费爬楼梯(746)

题目

  • 状态:dp[i] 代表登上第 i 级台阶的最小花费
  • 方程
    • 先踏上第i-2级台阶(最小总花费dp[i-2]),再直接迈两步踏上第i级台阶(花费cost[i]),最小总花费dp[i-2] + cost[i]
    • 先踏上第i-1级台阶(最小总花费dp[i-1]),再迈一步踏上第i级台阶(花费cost[i]),最小总花费dp[i-1] + cost[i]
dp[i]=min(dp[i−2],dp[i−1])+cost[i]
public int minCostClimbingStairs(int[] cost) {
    int n = cost.length;
    int[] dp = new int[n];
    dp[0] = cost[0]; dp[1] = cost[1]; // 初始时的最小花费

    for (int i = 2; i < n; i++) {
        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
    }

    return Math.min(dp[n - 2], dp[n - 1]); // 寻找从后两层最小的花费
}

环形子数组的最大和(918)

题目

  • 前提:需要掌握 **53.最大子序和 ** 空间优化后的解法

最大和有两种情况:

  1. 最大子序和就在数组中
  2. 最大自序和的子数组一部分在首,一部分在尾,则可以转换为第一种

即 res = max(最大子数组和,数组总和-最小子数组和)

证明第二种情况

前缀数组后缀数组数组总和子数组数组总和子数组数组总和子数组max(前缀数组+后缀数组)=max(数组总和−子数组)=数组总和+max(−子数组)=数组总和−min(子数组)

注意极端情况,数组全部为负值,则第一种情况,结果为最大的负数,第二种情况,结果为 0,如果直接返回二者的最大值,则肯定为 0,但示例 5 告诉我们,这种极端情况要返回最大的负数,所以需要特判一下

total为数组的总和,max为最大子数组和,min为最小子数组和,maxdp为以当前值结尾的最大子数组和,mindp为当前值结尾的最小子数组和

public int maxSubarraySumCircular(int[] nums) {
    // 考虑到存在全为负值,max 和 min 初始不能为 0
    int total = 0, max = nums[0], maxdp = 0, min = nums[0], mindp = 0;

    for (int num : nums) {
        maxdp = Math.max(maxdp + num, num);
        max = Math.max(max, maxdp);
        mindp = Math.min(mindp + num, num);
        min = Math.min(min, mindp);
        total += num;
    }

    return max > 0 ? Math.max(max, total - min) : max;
}

最低票价(983)

题目

  • 状态:dp[i] 表示到下标为 i 的这一天,旅行所需要的最低消费
  • 方程:
不在中在中dp[i]={dp[i−1],i 不在 days 中min{dp[i−1]+costs[0],dp[i−7]+costs[1],dp[i−30]+costs[2]},i 在 days 中
public int mincostTickets(int[] days, int[] costs) {
    int lastDay = days[days.length - 1];
    int[] dp = new int[lastDay + 1];
    int index = 0; // 检测 i 是不是在 days 中

    for (int i = 1; i <= lastDay; i++) {
        if (i == days[index]) {
            // 比较三个的最小值
            int cost = Integer.MAX_VALUE;
            cost = Math.min(cost, dp[i - 1] + costs[0]);
            cost = Math.min(cost, dp[Math.max(i - 7, 0)] + costs[1]);
            cost = Math.min(cost, dp[Math.max(i - 30, 0)] + costs[2]);
            dp[i] = cost;

            index++; // 寻找下一个 i
        } else {
            dp[i] = dp[i - 1];
        }
    }
    return dp[lastDay];
}
最近更新:: 2022/8/16 00:06
Contributors: kingmusi
Prev
二叉树路径问题题目
Next
动态规划2--背包问题