字母迷宫

字母迷宫(单词搜索)

图 0

思路

刚开始看到这题,知道用回溯算法,所以回忆了回溯算法三部曲

  • 1、确定参数
  • 2、确定终止条件
  • 3、单层搜索逻辑

但是看了半天之确定了参数,终止条件和单层逻辑都理出来。看了讲解知道了原因。

第一个字母的匹配逻辑和后续字母匹配逻辑不一致。

  • 第一个字母就在矩阵中挨个遍历,哪个元素等于target[0],就可以开始下一个字母。

  • 而非第一个字的匹配,是要在第一个匹配上的字母的上下左右寻找。

前者按照行列遍历,后者按照方向遍历。因此有必要分成两部分,先找到第一个字母吗,然后后续字母遵照一样的逻辑处理。

思路详解

  • 1、参数:i,j,k分别表示矩阵的行、矩阵的列以及当前遍历到target字符的哪一位字符
  • 2、确定终止条件
    • 如果当前位置的字符不等于 target[k],则当前路径不可能构成目标单词,返回 false。
    • 如果 k == target.length - 1 且当前字符匹配,说明已经成功找到目标单词,返回 true。
    • 注意,还应该有一个终止条件是检查 (i, j) 是否越界或者该位置已经被访问过,以避免重复访问。
  • 3、单层搜索逻辑:
    • 对于矩阵中的每一个元素,如果它是 target[0],则从这个元素开始,尝试所有可能的搜索路径。
    • 一旦找到匹配的第一个字母,后续的搜索应该限制在当前元素的上下左右四个方向上。
    • 搜索过程中,需要标记当前路径已访问过的位置,以避免循环访问。搜索后,需要撤销标记(回溯),以便其他搜索路径可以使用这些位置。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var wordPuzzle = function(grid, target) {
const h = grid.length,
w = grid[0].length;
const direction = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0]
];
const visited = Array.from({ length: h }, () => Array(w).fill(false));

const check = (i, j, k) => {
if (grid[i][j] != target[k]) return false;
else if (k == target.length - 1) return true; // 修正比较操作

visited[i][j] = true;
//向四分方向寻找
for (let [dx, dy] of direction) { // 添加let声明
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w && !visited[newi][newj]) { // 修正条件判断
if (check(newi, newj, k + 1)) {
return true; // 找到匹配后立即返回true
}
}
}
visited[i][j] = false;
return false;
};
// 第一个字母的判断
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
if (check(i, j, 0)) {
return true;
}
}
}
return false;
}

copilot 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var wordPuzzle = function(grid, target) {
const h = grid.length,
w = grid[0].length;
const directions = [
[0, 1], // 右
[0, -1], // 左
[1, 0], // 下
[-1, 0] // 上
];

const visited = Array.from({ length: h }, () => Array(w).fill(false));

const dfs = (i, j, k) => {
if (i < 0 || i >= h || j < 0 || j >= w || visited[i][j] || grid[i][j] !== target[k]) {
return false;
}
if (k === target.length - 1) {
return true;
}
visited[i][j] = true;
for (const [di, dj] of directions) {
if (dfs(i + di, j + dj, k + 1)) {
return true;
}
}
visited[i][j] = false; // 回溯
return false;
};

for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
if (grid[i][j] === target[0] && dfs(i, j, 0)) {
return true;
}
}
}
return false;
};

基于cesium和高德地图API的路径规划

基于cesium和高德地图API的路径规划

cesium

  • 作为一个开源的、世界级的、展现3D全球地图的JavaScript类库,Cesium毫无疑问已然成为WebGIS开发中三维地球框架的首选,(https://cesium.com/)

  • cesium的组成和功能,如下表所示

Component Description
Viewer 提供3D、2D、2.5D的地图视图
DataSource 支持多种数据源,包括地形、影像、矢量数据等
Entity 用于在地图上展示点、线、面等几何图形
Widget 提供了一系列UI组件,如指南针、缩放控件等
Scene 管理和渲染3D场景
Camera 控制视角和视图
CesiumWidget 不带UI控件的Cesium Viewer的简化版本
Primitives 用于渲染几何图形和图像
ImageryLayer 管理地图影像图层
TerrainProvider 提供地形数据

高德地图api

  • 高德地图api在js中的应用,有两大类一种是已经封装好的web服务,一种是js API
类别 描述 使用场景
Web服务 高德地图提供的一系列服务器端API,可以通过HTTP请求访问。这些服务包括地理编码、路径规划、静态地图等。 当你需要在服务器端处理地图数据,或者在客户端进行简单的HTTP请求以获取地图数据时使用。
JavaScript API 一套在网页中直接嵌入和操作地图的接口。提供了丰富的功能,如地图显示、交互、覆盖物添加等。 当你需要在网页中嵌入交互式地图,实现复杂的地图操作和数据可视化时使用。

前者已经封装好api,直接访问即可返回数据,而后者是需要在前端页面中嵌入的地图功能。

输入提示(JS API)

安装

1
npm install @amap/amap-jsapi-loader

引入

ES Module:

1
2
3
4
5
6
7
8
9
10
11
12
import AMapLoader from '@amap/amap-jsapi-loader'
// 初始化地图时要配置key,和密钥
// 因为我没配置密钥导致我的提示一直不出来,搞了好久才成功
AMapLoader.load({
"key": "你的高德地图API密钥", // 请替换成你的高德地图API密钥
"version": "2.0", // API版本号
"plugins": [] // 需要使用的插件列表
}).then((AMap) => {
// 高德地图API已成功加载,现在可以通过AMap对象进行地图开发了
}).catch(e => {
console.log(e); // 加载错误
});
1
2
3
4
5
6
7
8
// 在index.html中通过CDN的方式引入
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode:'安全密钥',
}
</script>
<script src="https://webapi.amap.com/maps?v=2.0&key=我的key"></script>
<script src="https://webapi.amap.com/ui/1.1/main.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//使得id为inputId的input元素可以实现搜索提示
const initAutocomplete = (inputId) => {
// 延迟执行,确保DOM元素已经渲染
nextTick(() => {
AMap.plugin('AMap.AutoComplete', () => {
const autoOptions = {
input: inputId
};
const autoComplete = new AMap.Autocomplete(autoOptions);
//选中某个poi之后的操作,获取poi的经纬度用于后续路径规划
autoComplete.on('select', (e) => {
console.log('select', e)
if (inputId === 'tipinput1') {
startPoint.value = e.poi.name;
startCoor.value.lng = e.poi.location.lng;
startCoor.value.lat = e.poi.location.lat;
// startCity.value = e.poi.sdcode;
} else {
endPoint.value = e.poi.name;
endCoor.value.lng = e.poi.location.lng;
endCoor.value.lat = e.poi.location.lat;
// endCity.value = e.poi.adcode ;
}
});
});
});
};

路径规划(web 服务)

只需处理数据,把输入数据根据api要求拼接成想要url即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//封装好的axios
import instance from "./outServeRequest.js"
// 访问高德地图api,获取数据;v3有polyline,v5没有
const url = '/amap/v3/direction/'
const key = '我的key'
export function getRoute(routeOpt,originCoor,destinationCoor,) {
let routeUrl = ''
if (routeOpt === 'integrated') {
//后面两个city的值是城市的adcode
routeUrl = `${url}/transit/${routeOpt}?&key=${key}&origin=${originCoor.lng},${originCoor.lat}&destination=${destinationCoor.lng},${destinationCoor.lat}&city=110000&cityd=110000`
} else {
routeUrl = `${url}${routeOpt}?&key=${key}&origin=${originCoor.lng},${originCoor.lat}&destination=${destinationCoor.lng},${destinationCoor.lat}`
}
return routeUrl
}
// 发起get请求
export function getRouteData(routeOpt,originCoor,destinationCoor) {
const routeUrl = getRoute(routeOpt,originCoor,destinationCoor)
return instance.get(routeUrl)
}

使用api时常看控制台的网络服务,看看请求是否发送,响应是什么。如果发生错误可以根据响应代码对照高德地图api提供的错误代码查看错误类型()

前后端的跨域问题

后端跨域问题配置,只能解决后端提供的api

而第三方提供的api修改后端跨域无法解决,由于我用的打包工具时vite,在解决第三方跨域问题时需要配置vite.config.js,设置代理为'/amap'。配置之后前端api访问时的地址也需要更改需要使用代理,'/amap/v3/direction'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vite.config.js
export default defineConfig({
server: {
proxy: {
// 代理所有到 /amap 的请求到 https://restapi.amap.com
'/amap': {
//第三方地址,这里用amap代理了,那么请求的地址就应该时amap开头
target: 'https://restapi.amap.com', // 目标地址
changeOrigin: true, // 必须设置为true,以便代理服务器发送请求时更改Origin头信息
rewrite: (path) => path.replace(/^\/amap/, ''), // 将请求地址中的 /amap 替换为空,因为实际的API地址中不包含 /amap
secure: false,
xfwd: true,
}
}
}
})
1
2
3
4
5
6
前端使用代理
const url = `/amap/v3/assistant/coordinate/convert?key=${key}&locations=${locations}&coordsys=gps`

不使用代理前的url为
'https://restapi.amap.com/v3/assistant/coordinate/...'

坐标系转换

高德地图的坐标系和cesium的坐标系是不同的,当然不同数据的坐标系都有差异,我们需要进行坐标系的转换。
可以借助现有的库,我用的是gcoord(https://github.com/hujiulong/gcoord)

安装

npm 安装

1
npm install gcoord --save

或者直接在页面中通过 script 标签引入:

1
<script src="https://unpkg.com/gcoord/dist/gcoord.global.prod.js"></script>

引入

CommonJS:

1
const gcoord = require('gcoord');

ES Module:

1
import gcoord from 'gcoord';

通过 script 标签引入可以直接使用全局变量 gcoord 或 window.gcoord

transform API

transform(input, from, to)

进行坐标转换

参数

  • input GeoJSON | string | Array GeoJSON对象,或GeoJSON字符串,或经纬度数组
  • from CRS 当前坐标系
  • to CRS 目标坐标系

返回值

GeoJSON | Array

示例

1
2
3
// 将GCJ02坐标转换为WGS84坐标
var result = gcoord.transform([123, 45], gcoord.GCJ02, gcoord.WGS84);
console.log(result); // [122.99395597, 44.99804071]
1
2
3
4
5
6
7
// 转换GeoJSON坐标
var geojson = {
"type": "Point",
"coordinates": [123, 45]
}
gcoord.transform(geojson, gcoord.GCJ02, gcoord.WGS84);
console.log(geojson.coordinates); // [122.99395597, 44.99804071]

返回数组或GeoJSON对象(由输入决定),注意:当输入为GeoJSON时,transform会改变输入对象

前一种输入为数组,后一种为一个geojson,geojson的coordinates可以是二维数组

CRS为坐标系,目标支持以下几种坐标系

CRS 坐标格式 说明
gcoord.WGS84 [lng,lat] WGS-84坐标系,GPS设备获取的经纬度坐标
gcoord.GCJ02 [lng,lat] GCJ-02坐标系,google中国地图、soso地图、aliyun地图、mapabc地图和高德地图所用的经纬度坐标
gcoord.BD09 [lng,lat] BD-09坐标系,百度地图采用的经纬度坐标
gcoord.BD09LL [lng,lat] 同BD09
gcoord.BD09MC [x,y] BD-09米制坐标,百度地图采用的米制坐标,单位:米
gcoord.BD09Meter [x,y] 同BD09MC
gcoord.Baidu [lng,lat] 百度坐标系,BD-09坐标系别名,同BD-09
gcoord.BMap [lng,lat] 百度地图,BD-09坐标系别名,同BD-09
gcoord.AMap [lng,lat] 高德地图,同GCJ-02
gcoord.WebMercator [x,y] Web Mercator投影,墨卡托投影,同EPSG3857,单位:米
gcoord.WGS1984 [lng,lat] WGS-84坐标系别名,同WGS-84
gcoord.EPSG4326 [lng,lat] WGS-84坐标系别名,同WGS-84
gcoord.EPSG3857 [x,y] Web Mercator投影,同WebMercator,单位:米
gcoord.EPSG900913 [x,y] Web Mercator投影,同WebMercator,单位:米

具体实现

踩坑总结

二叉树

二叉树

二叉树分类

二叉树遍历

  • 前序:根左右
  • 中序:左根右
  • 后序:左右根

递归法

递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!

确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
前序方法1
var preorderTraversal = function(root) {
let res = []
const dfs = ()=>{
if (root === null) return
res.push(root.val)
dfs(root.left)
dfs(root.right)
}
//调用
dfs(root)
return res
};
前序方法2:
var preorderTraversal = function (root) {
return root
? [
root.val,
...preorderTraversal(root.left),
...preorderTraversal(root.right)
]
: []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
中序方法1:
var inorderTraversal = function(root) {
let res=[]
const dfs = (r)=>{
if (r===null) return
dfs(r.left)
res.push(r.val)
dfs(r.right)
}
dfs(root)
return res
};
中序方法2:
var inorderTraversal = function (root) {
return root
? [
...inorderTraversal(root.left),
root.val,
...inorderTraversal(root.right)
]
: []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
后序方法1
var postorderTraversal = function(root) {
let res = []
const dfs = (r)=>{
if (r===null) return
dfs(r.left)
dfs(r.right)
res.push(r.val)
}
dfs(root)
return res
};
后序方法2
var postorderTraversal = function (root) {
return root
? [
...postorderTraversal(root.left),
...postorderTraversal(root.right),
root.val
]
: []
}

迭代法–栈

图 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
前序
// 入栈 右 -> 左
// 出栈 中 -> 左 -> 右
var preorderTraversal = function (root) {
let res = []
if (!root) return res
let queue = [root]
while(queue.length){
cur = queue.pop()
res.push(cur.val)
cur.right && queue.push(cur.right)
cur.left && queue.push(cur.left)
}
return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 入栈 左 -> 右
// 出栈 中 -> 右 -> 左 结果翻转
后序
var postorderTraversal = function (root) {
let res = []
if (!root) return res
let stack = [root]
while(stack.length){
cur = stack.pop()
res.push(cur.val)
cur.left && stack.push(cur.left)
cur.right && stack.push(cur.right)
}
return res.reverse()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
中序
var inorderTraversal = function (root) {
// 入栈:左->右
// 出栈:左 中 右
let res = []
if (!root) return res
const stack = []
cur = root
while (cur || stack.length){
if (cur){
stack.push(cur)
cur = cur.left
}
else{
cur = stack.pop()
res.push(cur.val)
cur = cur.right
}
}
return res
}

观察上述代码发现,前后序可以通过调整代码顺序,实现.但中序却喝前后序的方法不同,那有没有统一的迭代方法呢?

我们以中序遍历为例,在二叉树:听说递归能做的,栈也能做! (opens new window)中提到说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。

那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。

如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var inorderTraversal = function (root) {
// 入栈:左->右
// 出栈:左 中 右
let res = []
const stack = []
if (root) stack.push(root)
while (stack.length) {
const node = stack.pop()
if (!node) {
res.push(stack.pop().val)
continue
}
if (node.right) stack.push(node.right) // 右
stack.push(node) // 中
stack.push(null)
if (node.left) stack.push(node.left) // 左
}
return res
}

回文子串

回文子串

https://leetcode.cn/problems/palindromic-substrings/description/

图 0

思路

  • 1、dp[i][j] 表示以i开始,j结尾的字符串是否是回文子串
  • 2、递推公式:
    • if (s[i]==s[j])
      • if (i==j) return true
      • if (j-i==1) return true
      • if (j-i>1) return dp[i+1][j-1]
    • else dp[i][j] = false
  • 3、初始化:统统false
  • 4、遍历:i倒序,j正序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
超时了,129/132
var countSubstrings = function(s) {
let num = 0
function isHuiwen(t) {
let p = 0;q = t.length-1
while (p<q){
if (t[p] != t[q]){
return false
}
p++;
q--
}
return true
}
for (let i=0;i<s.length;i++){
for (let j=i+1;j<=s.length;j++){
console.log(isHuiwen(s.substring(i, j)))
if (isHuiwen(s.substring(i,j))) num ++
}
}
return num
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
动态规划
var countSubstrings = function(s) {
let num = 0
let n = s.length
let dp = Array.from({length:n},()=>Array(n).fill(false))
for(let i=n-1;i>=0;i--){
//j是尾巴,尾巴j大于等于头i
for(let j=i;j<n;j++){
if (s[i]==s[j]){
if (j-i>1) {
dp[i][j] = dp[i+1][j-1]
}
else dp[i][j] = true
}
if (dp[i][j] === true) num++
}
}
console.log(dp)
return num
};

最长回文子序列

https://leetcode.cn/problems/longest-palindromic-subsequence/description/

图 1

思路

  • 1、dp[i][j]表示 以i开头j结尾的字符串中最长回文子序列的长度
  • 2、递推公式:
    • if (s[i]==s[j]) dp[i][j] = dp[i+1][j-1]+2
    • else max(dp[i+1][j],dp[i][j-1]),分别把s[j]和s[i]加入后的最长回文子序列的长度。
  • 3、初始化:
  • 遍历顺序,i倒序,j正序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var longestPalindromeSubseq = function(s) {
let n = s.length
let dp = Array.from({length:n},()=>Array(n).fill(0))
for (let i=0;i<n;i++){
dp[i][i] =1
}
for(let i=n-1;i>=0;i--){
for(let j=i+1;j<n;j++){
if (i==j) dp[i][j] = 1
if (s[i]==s[j]) dp[i][j] = dp[i+1][j-1] + 2
else dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1])
}
}
return dp[0][n-1]
};

编辑距离

编辑距离类问题

判断子序列

https://leetcode.cn/problems/is-subsequence/description/

图 6

思路
这题和之前的最长公共子序列有什么关系呢?
关系在于,这题的最长公共子序列长度和S1的长度相等说明有关系。

这也是编辑距离的入门题目,下面是编辑距离的基本思路

  • 1、二维dp数组,dp[i][j],以i-1结尾的s1和以j-1结尾的s2的最长公共子序列。
  • 2、递推公式:
    • if (s1[i-1]==s2[i-2]) dp[i-1][j-1]+1;
    • else dp[i][j] = max(dp[i-1][j],dp[i][j-1])
  • 3、初始化:均为0
  • 4、遍历顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
var isSubsequence = function(s, t) {
let n = s.length
let m = t.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if(s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1] + 1
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
}
}
if (dp[n][m] == n) return true
return false
};

不同的子序列

https://leetcode.cn/problems/distinct-subsequences/description/

图 3

思路
这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。

这道题目相对于72. 编辑距离,简单了不少,因为本题相当于只有删除操作,不用考虑替换增加之类的。

这道题可以理解为怎么删除元素可以由s变为t;

  • 1、二维dp数组,dp[i][j]以i-1为结尾的s中以j-1为结尾的t的个数
  • 2、递推公式:
    • if (s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1] +dp[i-1][j],使用s[i-1]这个元素+不使用s[i-1]这个元素
    • else dp[i][j] = dp[i-1][j],模拟删除当前元素,也就是不考录当前的元素。
  • 3、初始化:
    • dp[i][0] = 1(); t是空字符串的话,把s中元素全部删除可以得到t
    • dp[0][j] = 0; s为空字符串,t不可能出现在s中,所以未0
    • dp[0][0]= 1; s和t都为空字符串的话,只有一种可能
  • 4、遍历顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var numDistinct = function(s, t) {
let n = s.length
let m = t.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=0;i<=n;i++) {dp[i][0]=1}
for (let i=1;i<=n;i++){
for (let j=1;j<=m;j++){
//相等,可以选择使用s[i-1]或者不使用s[i-1]
if (s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
else dp[i][j] = dp[i-1][j] //不相等说明s[i-1]用不了得删掉。
}
}
return dp[n][m]
};

两个字符串的删除操作

https://leetcode.cn/problems/delete-operation-for-two-strings/description/

图 4

思路1
求出最长公共子序列,然后用n+m-2x,n,m,s分别表示字符串1、字符串2和最长公共子字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
12
var minDistance = function(word1, word2) {
let n = word1.length
let m = word2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
}
}
return n+m-2*dp[n][m]
};

思路2

  • 1、二维dp数组,dp[i][j]以i-1为结尾的word1和以j-1为结尾的word2相同需要的最少操作。
  • 2、递推公式:始终记得dp数组的含义
    • if (word1[i-1] == word2[i-1]) dp[i][j] = dp[i-1][j-1],如果相等说明不需要操作。
    • else dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+2)
    • 由于dp[i][j-1]= dp[i-1][j-1]+1
  • 3、初始化:dp[i][0] = i,dp[0][j] = j,dp[0][0] = 0
  • 遍历顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var minDistance = function(word1, word2) {
let n = word1.length
let m = word2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
//这两个for循环用于初始化dp数组
for (let i=0;i<=n;i++){
dp[i][0] = i
}
for (let j=0;j<=m;j++){
dp[0][j] = j
}
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1]
else dp[i][j] = Math.min(dp[i-1][j]+1,dp[i][j-1]+1)
}
}
return dp[n][m]
};

编辑距离

https://leetcode.cn/problems/edit-distance/description/

图 5

思路
看起来好复杂哦。

题目要求讲word1转为word2的最少操作次数,但实际上我们可以从word2转为word3,或者word1和word2同时操作都可以

  • 1、dp[i][j]: 以i-1为结尾的word1和j-1为结尾的word2的最少的操作次数

  • 2、递推公式:

    • if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1],相同不需要操作

    • else dp[i][j] = min(dp[i][j-1] + 1,dp[i-1][j]+1,dp[i-1][j-1]+1)

      • 上操作了
      • 增:word2[i-1]增加一个字符, dp[i][j] = dp[i][j-1] + 1
      • 删:删掉word1[i-1] ,dp[i][j] = dp[i-1][j]+1
      • 替换:dp[i][j] = dp[i-1][j-1]+1
        eg: word1 = ‘ab’ , word2 = ‘a’ , 可以是ab删掉b,也可以a增加b。dp数组如下图所示意的:
      a d
      1
      2
      3
      4
      5
      6
      7
        +-----+-----+             +-----+-----+-----+
      | 0 | 1 | | 0 | 1 | 2 |
      +-----+-----+ ===> +-----+-----+-----+
      a | 1 | 0 | a | 1 | 0 | 1 |
      +-----+-----+ +-----+-----+-----+
      d | 2 | 1 |
      +-----+-----+
  • 3、初始化:

    • dp[i][0] = i
    • dp[0][j] = j
  • 4、遍历顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var minDistance = function(word1, word2) {
let n= word1.length
let m = word2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=0;i<=n;i++){
dp[i][0] = i
}
for (let j=0;j<=m;j++){
dp[0][j] = j
}
for (let i=1;i<=n;i++){
for (let j=1;j<=m;j++){
if (word1[i-1]==word2[j-1]) dp[i][j] = dp[i-1][j-1]
else dp[i][j] = Math.min(dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1)
}
}
return dp[n][m]
};

博客域名配置

博客域名配置

域名购买

我在阿里云买了域名10元一个,因为我已经不是新用户了,呜呜呜呜

1、注册并登录

2、查询域名,挑一个喜欢的实惠的买下来。

直接这样操作下来就会发现付款的时候有提示说,没有实名认证。但也可以付款只不过要在付款后进行实名认证,认证成功后完成过户才正常使用。

  • 认证的话跟着指引一步步来就行
  • 过户的操作:进入我的域名控制台–>域名列表–>管理–>域名持有者信息修改(过户)–>提交申请,通过后会有短信提醒。
    图 0
    图 1
    图 2

3、域名可以正常使用之后要取配置github pages和域名的解析

3.1、github pages:

打开仓库–>点击新建一个文件–>新建一个名为CNAME的为文件,文件内容为刚申请的域名–>保存修改后,打开设置–>修改名字(个人觉得这步可以去掉,只要后面解析的时候保证名字一样即可)–>点击左边导航栏的Pages–>在Custom domain中输入自己的域名。
不过这里肯定解析不出来啦,因为还没配置域名解析呢(见3.2)!
图 4
图 5
图 3
图 6
图 8

3.2、配置域名解析

去阿里云平台,找到域名控制台的域名列表,点击解析;配置图见下面图2,图3的名字和图2中配置保持一致。然后再到pages页面稍等片刻就会发现DNS check成功了,说明可以直接通过域名访问博客了。
图 9
图 10
图 11
图 12
图 13

注意
为了防止每次hexo d都需要去github pages重新配置域名。需要在本地项目的source文件夹下新建一个CNAME去配置你的域名。

图 14

子序列

子序列与动态规划问题

最长递增子序列

https://leetcode.cn/problems/longest-increasing-subsequence/description/

图 0

思路
本题也是代码随想录中子序列问题的第一题,如果没接触过这种题目的话,本题还是很难的,甚至想暴力去搜索也不知道怎么搜。 子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下表j的子序列长度有关系,那又是什么样的关系呢。

  • 1、dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
    为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在做递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾 和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么 如何算递增呢。
  • 2、状态转移方程
    • if (nums[i]>nums[j]) dp[i] = max(dp[i],dp[j]+1)这里的含义是,取dp[j]+1的最大值。
  • 3、初始化,子序列的长度最短为1
  • 4、遍历顺序,外层遍历i(1 ~ n),内层遍历j(0 ~ i-1)
  • 还要注意并不是dp[n-1]才是最终的值,最大值应该是dp数组中的最大值
1
2
3
4
5
6
7
8
9
10
11
12
var lengthOfLIS = function(nums) {
let n = nums.length
let dp = Array(n).fill(1)
let result = 1
for(let i=1;i<n;i++){
for(let j=0;j<i;j++){
if (nums[i]>nums[j]) dp[i] = Math.max(dp[i],dp[j]+1)
}
if (dp[i]>result) result = dp[i]
}
return result
};

最长递增子序列

https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/

图 1

思路
这里的序列要连续,因此当前的状态要由前一个决定

  • 1、dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]
  • 2、if(nums[i]>nums[i-1]) dp[i] = dp[i-1]+1
  • 3、初始化dp[i]都等于1
  • 4、循环,只需要一层循环遍历i即可
  • 当然同样注意最终答案不是dp[n-1],而是dp数组中的最大值
1
2
3
4
5
6
7
8
9
10
var findLengthOfLCIS = function(nums) {
let n = nums.length
let dp = Array(n).fill(1)
let result =1
for(let i=1;i<n;i++){
if(nums[i]>nums[i-1]) dp[i] = dp[i-1]+1
if (dp[i]>result) result = dp[i]
}
return result
};

最长重复子数组

https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/

图 2

难点
动态数组怎么表示,又是什么含义

思路
如果要暴力破解的话,把每个数组的子数组遍历出来,然后两两比较,得出最长重复子数组

  • 1、二维dp[i][j],以i-1为结尾的nums[1]和以j-1为结尾的nums[2],的最长重复子数组的长度;本质上也是两两比较了,但是由于我记录了前一个状态的值,在进行下一个状态比较时,子需要对比一个数即可结合前一个状态得出现在状态的值。

    • 为什么i,j表示下标为i-1和j-1呢?因为有dp[i-1],dp[j-1],因此i和j的最小值应该为1。dp数组遍历时候从1开始,但是nums数组要从0号元素开始比较。
  • 2、递推公式:if(nums[i-1]==nums[j-1]) dp[i][j] = dp[i-1][j-1] +1;
    图 4

  • 3、初始化:dp[i][0] = 0,dp[0][j] = 0;因为i和j遍历时都从1开始

  • 4、遍历顺序:

  • 这题也是结果在过程中,而不是遍历完之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
var findLength = function(nums1, nums2) {
let n = nums1.length
let m = nums2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
let result = 0
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if(nums1[i-1] == nums2[j-1]) dp[i][j]=dp[i-1][j-1]+1
if (dp[i][j] > result) result = dp[i][j]
}
}
return result
};

最长公共子序列

https://leetcode.cn/problems/longest-common-subsequence/description/

图 3

思路
这道题就是可以不连续,可以不连续前面一维数组做过,不过现在dp需要时二维数组,因此这题主要突破点再递推公式上

  • 1、dp[i][j]表示以i-1为结尾的第一个字符串和j-1结尾的第二个字符串的最长公共子序列长度

  • 2、递推公式:主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同。

    • if (text1[i-1]==text2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
    • else dp[i][j] = max(dp[i-1][j],dp[i][j-1])
  • 3、初始化,均为0即可,

  • 4、遍历顺序,那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
    图 5

1
2
3
4
5
6
7
8
9
10
11
12
var longestCommonSubsequence = function(text1, text2) {
let n = text1.length
let m = text2.length
let dp = Array.from({length:n+1},()=> Array(m+1).fill(0))
for(let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if (text1[i-1]==text2[j-1]) dp[i][j] = dp[i-1][j-1]+1
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
}
}
return dp[n][m]
};

不相交的线

https://leetcode.cn/problems/uncrossed-lines/description/

图 2

思路
其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)

这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

1
2
3
4
5
6
7
8
9
10
11
12
var maxUncrossedLines = function(nums1, nums2) {
let n = nums1.length
let m = nums2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for(let i = 1;i<=n;i++){
for(let j = 1;j<=m;j++){
if (nums1[i-1]==nums2[j-1]) dp[i][j] = dp[i-1][j-1]+1
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
}
}
return dp[n][m]
};

买卖股票

买卖股票

买卖股票最佳时机(121)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

图 0

思路
股票只能买卖一次

  • 1、dp[i][0],dp[i][1] 分别表示持有股票和不持有股票的现金最大值
  • 2、递推公式:
    • dp[i][0] = max(dp[i-1][0],-prices[i]) 维持前一天持有股票的状态或者买入今天的股票,这是因为只能买一次买一次所以是-prices[i]
    • dp[i][1] = max(dp[i-1][1],dp[i-1][0]+prices[i]) 维持前一天不持有股票的状态,或者在今天卖出股票
  • 3、初始化,dp[0][0] = -prices[0]
  • 4、dp[i]的状态依赖dp[i-1]的状态因此从前往后遍历
    可以把二维dp数组压缩为一维如下代码所示
1
2
3
4
5
6
7
8
9
10
11
var maxProfit = function(prices) {
let dp = Array(2).fill(0)
dp[0] = -prices[0]
let n = prices.length
for(let i=1;i<n;i++ ){
let val0 = Math.max(dp[0],-prices[i])
let val1 = Math.max(dp[0]+prices[i],dp[1])
dp = [val0,val1]
}
return dp[1]
};

买卖股票的最佳时机(122)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

图 1

思路
股票只能买卖多次

  • 1、dp[i][0],dp[i][1] 分别表示持有股票和不持有股票的现金最大值
  • 2、递推公式:
    • dp[i][0] = max(dp[i-1][0],dp[i-1][1]-prices[i]) 维持前一天持有股票的状态或者买入今天的股票,(今日买入手中的现金=前一天不持有的利益-今日股票价格)
    • dp[i][1] = max(dp[i-1][1],dp[i-1][0]+prices[i]) 维持前一天不持有股票的状态,或者在今天卖出股票
  • 3、初始化,dp[0][0] = -prices[0]
  • 4、dp[i]的状态依赖dp[i-1]的状态因此从前往后遍历
    可以把二维dp数组压缩为一维如下代码所示
1
2
3
4
5
6
7
8
9
10
11
12
13
var maxProfit = function(prices) {
let n = prices.length
let dp = Array(2).fill(0)
dp[0] = -prices[0]
for (let i = 1;i<n;i++){

let val0 = Math.max(dp[0],dp[1]-prices[i])
let val1 = Math.max(dp[0]+prices[i],dp[1])
dp = [val0,val1]
console.log(dp)
}
return dp[1]
};

买卖股票的最佳时机(123)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/

图 2

思路
关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖

  • 1、dp数组状态:dp[i][0],dp[i][1],dp[i][2],dp[i][3],dp[i][4]
    • 0:没有操作
    • 1:第一天持有股票
    • 2:第一天不持有股票
    • 3:第二次持有股票
    • 4:第二次不持有股票
  • 2、递推公式
    • dp[i][0] = dp[i-1][0]
    • dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]) 保持前一天持有状态,今天刚刚买入。
    • max(dp[i-1][1] + prices[i],dp[i-1][2]) 延续前一天不持有的状态或者,前一天持有今天买了
    • max(dp[i-1][3],dp[i-1][2]-prices[i]),延续前一天买入第二次股票的状态,前一天第一次不持有减去今天股票价格
    • max(dp[i-1][4],dp[i-1][3]+prices[i])
  • 3、初始化:
    • dp[0][0] = 0
    • dp[0][1] = -prices[0]
    • dp[0][2] = 0,同一天买卖,手里现金增加0
    • dp[0][3] = -prices[0],第一天买入,卖出,又买入
    • dp[0][4] = 0
  • 4、遍历:从前往后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var maxProfit = function(prices) {
let n = prices.length
let dp = Array(5).fill(0)
dp[1] = -prices[0]
dp[3] = -prices[0]
for(let i=1;i<n;i++){
let val0 = dp[0]
let val1 = Math.max(dp[0] - prices[i],dp[1])
let val2 = Math.max(dp[1]+prices[i],dp[2])
let val3 = Math.max(dp[3],dp[2]-prices[i])
let val4 = Math.max(dp[4],dp[3]+prices[i])
dp = [val0,val1,val2,val3,val4]
}
return dp[4]
};

买卖股票最佳时机(188)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/

图 3

思路
至多可以买卖k
从前面一道题可以看出规律,dp[i][k]的状态由dp[i-1][k]dp[i-1][k-1]决定

  • 1、dp[i][j],(0<i<n;0<j<2*k),表示第i天的2k种状态,
  • 2、递推公式,dp[i][j]
    • j是奇数,持有状态:max(dp[i-1][j],dp[i-1][j-1]-prices[i])
    • j是偶数,不持有状态:max(dp[i-1][j],dp[i-1[j]+prices[i]])
  • 3、初始化,索引为奇数的元素全部初始化为-prices[0],其余全为0
  • 4、遍历,从前往后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var maxProfit = function(k, prices) {
let n = prices.length
let dp = Array(2*k+1).fill(0)
for (let i=1;i<2*k;i+=2){
dp[i] = -prices[0]
}
for(let i=1;i<n;i++){
let arr =[0]
for (let j=1;j<=2*k;j++){
if(j%2 === 1) arr.push(Math.max(dp[j],dp[j-1]-prices[i]))
else arr.push(Math.max(dp[j], dp[j - 1] + prices[i]))
}
dp = arr
}
return dp[2*k]
};

买卖股票的最佳时机含冷冻期(309)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/

图 4

思路

  • 1、dp[j]表示当天股票状态,
    • dp[0] 表示持有股票
    • dp[1] 表示保持卖出股票的状态
    • dp[2] 表示当天卖出股票的操作
    • dp[3] 冷冻的期
  • 2、递推公式
    • dp[0] = max(dp[0],dp[3]-prices[i],dp[1]-prices[i])
    • dp[1] = max(dp[1],dp[3])
    • dp[2] = dp[0]+prices[i]
    • dp[3] = dp[2]
  • 3、初始化
    • dp[0] = -prices[i]
    • dp[1] = 0
    • dp[2] = 0
    • dp[3] = 0
  • 4、遍历顺序
    结果取最后一天状态1,2,3的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
var maxProfit = function(prices) {
let n = prices.length
let dp = Array(4).fill(0)
dp[0] = -prices[0]
for(let i=0;i<n;i++){
let val0 = Math.max(dp[0],dp[3]-prices[i],dp[1]-prices[i])
let val1 = Math.max(dp[1],dp[3])
let val2 = dp[0]+prices[i]
let val3 = dp[2]
dp = [val0,val1,val2,val3]
}
return Math.max(dp[1],dp[2],dp[3])
};

买卖股票的最佳时机含手续费(714)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/

图 5

思路

  • 1、dp[j]表示每天的状态
    • dp[0] 持有
    • dp[1] 不持有
  • 2、递推公式
    • dp[0] = max(dp[0],dp[1]-price[i]-fee) ,保持前一天的持有状态,或者在前一天不持有的状态下买入
    • dp[1] = max(dp[1],dp[0]+prices[i]),保持前一天不持有的状态,或者在昨天持有股票的基础上卖出股票
  • 3、初始化
    • dp[0] = -prices[0]-fee
    • dp[1] = 0
  • 4、遍历
1
2
3
4
5
6
7
8
9
10
11
12
var maxProfit = function(prices, fee) {
let n = prices.length
let dp = Array(2).fill(0)
dp[0] = -prices[0]- fee
dp[1] = 0
for(let i=0;i<n;i++){
let val0 = Math.max(dp[0],dp[1]-prices[i]-fee)
let val1 = Math.max(dp[1],dp[0]+prices[i])
dp = [val0,val1]
}
return dp[1]
};

打家劫舍系列

打家劫舍系列

打家劫舍(198)

https://leetcode.cn/problems/house-robber/description/

图 0

思路
五部曲

  • 1、dp[i] 考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。

  • 2、递推公式:
    偷:dp[i] = dp[i-2] + num[i],不偷:dp[i] = dp[i-1],因此dp[i] = max(dp[i-2] + num[i],dp[i] = dp[i-1])

  • 3、初始化:
    从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

    从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);

  • 4、遍历:i从2到最后一个房间

1
2
3
4
5
6
7
8
9
10
var rob = function(nums) {
let n = nums.length
let dp = Array(n+1).fill(0)
dp[0] = nums[0]
dp[1] = Math.max(dp[0],nums[1])
for (let i=2;i<n;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1])
}
return dp[n-1]
};

打家劫舍(213)

https://leetcode.cn/problems/house-robber-ii/description/

图 1

思路
对于一个数组,成环的话主要有如下三种情况:

  • 1、首尾均不考虑
  • 2、考虑首,不考虑尾
  • 3、考虑尾,不考虑首
    情况2和3包含了情况1,因此只需取情况2和3的最大值即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var rob = function(nums) {
let n = nums.length
if (n===1) result = nums[0]
else{
result1 = robRange(nums,1,n-1)
result2 = robRange(nums,0,n-2)
result = Math.max(result1,result2)
}
function robRange(nums,start,end){
let dp = Array(end-start+2).fill(0)
dp[start] = nums[start]
dp[start+1] = Math.max(nums[start],nums[start+1])
console.log(start,end)
for (let i=start+2;i<=end;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1])
}
console.log(dp)
return dp[end]
}
return result
};

打家劫舍(337)

https://leetcode.cn/problems/house-robber-iii/description/

图 2

思路
数据结构变成个二叉树了,投了父节点就不能考虑子节点了,而是要考虑孙子节点

  • 每个节点有一个一维dp数组,数组长度为2,dp[0]表示不偷所获金钱的最大值,dp[1]表示偷所获金钱的最大值。

  • 递归三部曲

    • 确定参数,robtree(root)返回dp数组
    • 终止条件,if(cur == null) return [0,0]
    • 遍历顺序是后序遍历,因为父的值需要依靠孩子的值决定
  • 递归五部曲

    • 1、dp[0]表示不偷所获金钱的最大值,dp[1]表示偷所获金钱的最大值。
    • 2、递推公式dp[0] = max(leftdp[0],leftdp[1]) + max(rightdp[0],rightdp[1]) ,dp[1] = cur.val + leftdp[0] + rightdp[0]
    • 3、初始化均为0
    • 4、遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var rob = function(root) {
//递归后续遍历二叉树
const robtree = (rootnode)=>{
let cur = rootnode
if(cur===null) return [0,0]
let leftdp = robtree(cur.left)
let rightdp = robtree(cur.right)
let val0 = Math.max(leftdp[0],leftdp[1]) + Math.max(rightdp[0],rightdp[1])
let val1 = cur.val + leftdp[0] + rightdp[0]
return [val0,val1]
}
//这里解构的时候容易出错
let [result0,result1] = robtree(root)
// console.log(result0,result1)
return Math.max(result0,result1)
};

完全背包

完全背包

什么是完全背包

  • 01背包,每个物品只能使用一次
  • 完全背包:每个物品可以使用无数次

图 0

01背包引出完全背包

1
2
3
4
5
for (let i=0;i<goodsNum;i++){
for (let j=weight[i];j<=bagWeight;j--){
dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
}
}
  • 01背包使用一维滚动数组时,背包容量要倒序遍历,这样保证每个物品只是用一次。
  • 完全背包,物品可以使用无数次,因此将背包容量遍历的时候改为正序遍历,即可实现

图 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//ACM模式
let readline = require('readline')
const rl = readline.createInterface({
input:process.stdin,
output:process.stdout
});
let inputArr = []
rl.on('line',function(line){
inputArr.push(line.split(' '))
}).on('close',function(){
let n = parseInt(inputArr[0][0])
let v = parseInt(inputArr[0][1])
let weight = []
let value = []
for (let i = 1;i<=n;i++){
weight.push(parseInt(inputArr[i][0]))
value.push(parseInt(inputArr[i][1]))
}
let dp = Array(v+1).fill(0)
for (let i=0;i<=n;i++){
//这边这个循环要注意,总写错j的起始值
for(let j=weight[i];j<=v;j++){
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
}
}
console.log(dp[v])
})

题目

零钱兑换

https://leetcode.cn/problems/coin-change-ii/description/

图 2

思路
这是一个统一方法数的题目,和之前价值不一样的递推公式。

  • bn找价值最高:max(dp[j],dp[j]+dp[j-weight[i]]+value[i])
  • 而找方法数:dp[j]+=dp[j-weight[i]]
    就比如硬币找零:找总额5元,有1,2,5三种硬币,那么dp[5] = dp[4] + dp[3] + dp[0]

五部曲
1、dp[j] 表示总和为j的找零方式;
2、递推公式 dp[j] += dp[j-coins[i]];
3、初始化,dp[0] = 1,其他索引对应的值均为0,因为要加等于,如果都是零加起来也都是零;
4、遍历,两个都是正序,因为完全背包,每种面额的钱有无数张。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
因为则何种情况下物品的遍历顺序是固定的,有{1,2}就没有被{2,1}

1
2
3
4
5
6
7
8
9
10
11
12
var change = function(amount, coins) {
let n = amount
let m = coins.length
let dp = Array(n+1).fill(0)
dp[0] = 1
for (let i=0;i<m;i++){
for(let j = coins[i];j<=n;j++){
dp[j] += dp[j-coins[i]]
}
}
return dp[n]
};

组合总和IV

https://leetcode.cn/problems/combination-sum-iv/description/

图 3

题目实际上是一个排列问题,元素有顺序的要求了,所以遍历顺序是解题关键

动规五部曲
1、dp[j]表示目标为j时的排列数量
2、递推公式:算方法数dp[j] += dp[j-nums[i]]
3、初始化,dp[0]=1,其他均为零
4、遍历,先遍历背包后遍历物品这样物品就有顺序了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var combinationSum4 = function(nums, target) {
let n = target
let m = nums.length
let dp = Array(n+1).fill(0)
dp[0] = 1
// 背包
for (let i=0;i<=n;i++){
// 物品
for (let j=0;j<m;j++){
if (i-nums[j] >=0) dp[i] += dp[i - nums[j]]
}
}
return dp[n]
};

爬楼梯(进阶版)

https://kamacoder.com/problempage.php?pid=1067

图 4

思路
使用动规五部曲分析

  • 1、dp[i]表示爬到第i级台阶的方法数
  • 2、递推公式:dp[i] = dp[i-1] + dp[i-2] + … +dp[i-m]
  • 3、初始化:
    需要初始化dp[0]到dp[m],但是m是变化的,不能直接赋值需要一步步递推。
    以m为5的情况为例:
    dp[1] = 1
    dp[2] = dp[1] + 1
    dp[3] = dp[2] + dp[1] + 1
    dp[4] = dp[3] + dp[2] + dp[1] +1
    dp[5] = dp[4] + dp[3] +dp[2] + dp[1] +1
    根据上述规律不难想出,让dp[0] = 1
    此外还可以看出这里的规律和递推公式是如出一辙
    dp[j] = dp[j-1] + dp[j-2] + … dp[j-m]
  • 4、遍历顺序:外层遍历爬上的太结束,内层遍历m
    综上得出如下面的代码。

总结
看了代码随想录的文档,惊觉这竟然是一个完全背包问题。其中要爬最终台阶个数为背包容量;而能爬的台阶个数1,2,… m是物品数量。

每次爬的台阶数可以重复也就是物品可以重复放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const readline = require('readline')
const rl = readline.createInterface({
input:process.stdin,
output:process.stdout
});

rl.on('line',function(line){
var tokens = line.split(' ')
let n = parseInt(tokens[0])
let m = parseInt(tokens[1])
let dp = Array(n+1).fill(0)
dp[0] = 1
for (let i=0;i<=n;i++){
for (let j=1;j<=m && i-j>=0;j++){
dp[i] += dp[i-j]
}
}
console.log(dp[n])
})

零钱兑换(322)

https://leetcode.cn/problems/coin-change/description/

图 5

思路
*1、dp[i] 表示兑换总额为i的金钱需要的最少硬币数
*2、递推公式 dp[j] = min(dp[j],dp[j-coins[i]]+1)
*3、初始化 dp[0] = 0,其余均为一个较大的正整数
*4、遍历顺序:先遍历物品后遍历背包,背包容量从小到。

记错模块
dp数组及其下标的含义、递推公式以及初始化都是没有问题的,但是在遍历顺序上我搞反了,导致错误。组合问题还是要先遍历物品,这样物品有顺序。排列问题才需要物品顺序的变化,要先遍历背包。

图 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var coinChange = function(coins, amount) {
let n = amount
let m = coins.length
let dp = Array(n+1).fill(Number.MAX_SAFE_INTEGER)
dp[0] = 0
for(let i=0;i<m;i++){
console.log(dp)
for(let j=coins[i];j<=n;j++){
dp[j] = Math.min(dp[j],dp[j-coins[i]]+1)

}
}
if (dp[n] == Number.MAX_SAFE_INTEGER) return -1
else return dp[n]
};

完全平方数

https://leetcode.cn/problems/perfect-squares/description/

图 7

思路
感觉这个和前一道题也没啥区别,只不过需要自己简历完全平方数的数组而已。物品是一个个的完全平方数,背包就是和。
*1、dp[i],表示组成和为i的完全平方数的个数
*2、递推公式,dp[j] = min(dp[j],dp[j-nums[i]]+1)
*3、初始化,dp[0] = 0,其他值为最大正整数
*4、遍历顺序,先遍历物品,后遍历背包,背包正序遍历。

1
2
3
4
5
6
7
8
9
10
11
12
var numSquares = function(n) {
let m = Math.floor(Math.sqrt(n))
const num = Array.from({ length: m }, (_, i) => (i + 1) ** 2)
let dp = Array(n+1).fill(Number.MAX_SAFE_INTEGER)
dp[0] = 0
for(let i=0;i<m;i++){
for(let j = num[i];j<=n;j++){
dp[j] = Math.min(dp[j],dp[j-num[i]]+1)
}
}
return dp[n]
};

单词拆分

https://leetcode.cn/problems/word-break/description/

图 8

思考
难点:背包是什么,背包装满怎么表示?
背包是指字符串s,s能否用数组中的单词表示就是能否装满的意思。

*1、dp[i]表示字符串s[0:i]能否由数组组成
*2、递推公式:i表示背包,j表示物品索引
q = i-wordDict(j)
dp[i] = (dp[q] && s[q,i] === wordDictus[j])
*3、初始化,dp[0] = true,其他为false
*4、这本质上是一个求排列的问题,所以要先遍历背包i后后遍历物品j。

记错模块
直接在内循环中使用dp[i] = dp[q] && s.substring(q, i) === wordDict[j]
则会么写的问题在于忽略了三个问题
*首先i-wordDict[j]的大小是否大于0
*每遍历一个单词dp[i]都要被修改,如果已经找到了单词满足条件,继续遍历,答案势必会错
*substring的用法,应该是小括号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var wordBreak = function(s, wordDict) {
let n = s.length
let m = wordDict.length
let dp = Array(n+1).fill(false)
dp[0] = true
console.log(dp)
for(let i=0;i<=n;i++){
for(let j = 0;j<m;j++){
//这里忘记打.length了
let q = i-wordDict[j].length
if (q >= 0 && dp[q] && s.substring(q, i) === wordDict[j]) {
dp[i] = true;
break; // 找到匹配,无需继续检查其他单词
}
console.log(dp)
}
}
return dp[n]
};