學習筆記
https://www.bilibili.com/video/BV1Y64y1U7sK
記憶化遞回
fib
一般實作
const fib = n => {
if (n === 1 || n === 2) return 1
return fib(n - 1) + fib(n - 2)
}
console.log(fib(6))
console.log(fib(7))
console.log(fib(8))
console.log(fib(50)) // 卡住了

這樣的遞回會在每次都計算一次, 造成多次呼叫多次
優化
我們考慮一下如何優化這個程序
考慮一個簡化版的模型, 我們的觀察一個這樣的函式
當我們遞回兩次的時候
所以我們之前的fib時間復雜度是
這真是太糟糕了
帶有記憶的遍歷就是dp
// memoization
// js obj, keys: arg, value returns
// 修改1 設定memo和初始值
const fib = (n, memo = {}) => {
// 修改2 檢查是否有記憶
if (n in memo) return memo[n]
if (n === 1 || n === 2) return 1
// 修改3 遞回的時候帶上我們的參考
memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
return memo[n]
}
console.log(fib(6))
console.log(fib(7))
console.log(fib(8))
console.log(fib(50)) // 很快就出結果了
旅行者gridTraveler
我們從極簡形式開始分析
其實這也是一種邊界情況
簡單情況
每移動一步, 問題將會簡化
所以我們可以這樣想這個問題

具象化的理解就是

遞回版
const gridTraveler = (m, n) => {
if (m === 1 && n === 1) return 1
if (m === 0 || n === 0) return 0
return gridTraveler(m - 1, n) + gridTraveler(m, n - 1)
}
console.log(gridTraveler(1,2))
console.log(gridTraveler(3,2))
console.log(gridTraveler(3,3))
console.log(gridTraveler(18,18))
dp版
const gridTraveler = (m, n, memo = {}) => {
const key = `${m}+${n}`
if (key in memo) return memo[key]
if (m === 1 && n === 1) return 1
if (m === 0 || n === 0) return 0
memo[key] = gridTraveler(m - 1, n, memo) + gridTraveler(m, n - 1, memo)
return memo[key]
}
console.log(gridTraveler(1, 2))
console.log(gridTraveler(3, 2))
console.log(gridTraveler(3, 3))
console.log(gridTraveler(18, 18))
這類問題的總結
成功最小結果和失敗最小結果
canSum
逆向思維: 求和到定值->使用定值遍歷陣列減到0
遞回
我的解法
const canSum = (targetSum, numbers) => {
if (targetSum === 0) return true
if (targetSum < 0) return false
let remainder
for (let num of numbers) {
remainder = remainder || canSum(targetSum - num, numbers)
}
return remainder
}
console.log(canSum(7, [3, 2]))
console.log(canSum(7, [4, 2]))
console.log(canSum(7, [5, 6, 2]))
console.log(canSum(300, [7, 14]))
視頻解法
const canSum = (targetSum, numbers) => {
if (targetSum === 0) return true
if (targetSum < 0) return false
for (let num of numbers) {
if (canSum(targetSum - num, numbers)) return true
}
return false
}
視頻解法遞回次數更少
dp

const canSum = (targetSum, numbers, memo = {}) => {
if (targetSum in memo) return memo[targetSum]
if (targetSum === 0) return true
if (targetSum < 0) return false
for (const num of numbers) {
memo[targetSum] = canSum(targetSum - num, numbers, memo)
if (memo[targetSum]) return true
}
return false
}
console.log(canSum(7, [3, 2]))
console.log(canSum(7, [4, 2]))
console.log(canSum(7, [5, 6, 2]))
console.log(canSum(300, [7, 14]))

howSum


遞回版
const howSum = (targetSum, numbers) => {
if (targetSum === 0) return []
if (targetSum < 0) return null
for (const num of numbers) {
const remainder = targetSum - num
const remainderResult = howSum(remainder, numbers)
if (remainderResult !== null) return [...remainderResult, num]
}
return null
}
console.log(howSum(7, [3, 2]))
console.log(howSum(7, [4, 2]))
console.log(howSum(7, [5, 6, 2]))
console.log(howSum(300, [7, 14]))
dp版
const howSum = (targetSum, numbers, memo = {}, path = []) => {
if (targetSum in memo) return memo[targetSum]
if (targetSum === 0) return []
if (targetSum < 0) return null
for (const num of numbers) {
const remainder = targetSum - num
const remainderResult = howSum(remainder, numbers, memo)
if (remainderResult !== null) {
memo[targetSum] = [...remainderResult, num]
return memo[targetSum]
}
}
memo[targetSum] = null // 不可達也需要記錄
return memo[targetSum]
}
console.log(howSum(7, [3, 2]))
console.log(howSum(7, [4, 2]))
console.log(howSum(7, [4, 3, 2]))
console.log(howSum(7, [5, 6, 2]))
console.log(howSum(300, [7, 14]))
bestSum

tips: 使用遞回的思路
- 想好出口, 邊界條件, 失敗成功條件
- 呼叫遞回函式的時候要假設遞回函式能獲取到你想要的結果
遞回版
const bestSum = (targetSum, numbers, lastBest) => {
if (targetSum === 0) return []
if (targetSum < 0) return null
let shortestCombination = null
for (const num of numbers) {
const remainder = targetSum - num
const remainderCombination = bestSum(remainder, numbers)
if (remainderCombination !== null) {
const combination = [...remainderCombination, num]
if (
shortestCombination === null ||
combination.length < shortestCombination.length
)
shortestCombination = combination
}
}
return shortestCombination
}
console.log(bestSum(7, [1, 3, 2, 7])) // [7]
console.log(bestSum(7, [1, 4, 2])) // [2,4,1]
console.log(bestSum(7, [1, 4, 3, 2])) // [3,4]
console.log(bestSum(7, [1, 5, 6, 2])) // [6, 1]
console.log(bestSum(100, [1, 2, 3, 14]))
dp版
const bestSum = (targetSum, numbers, memo = {}) => {
if (targetSum in memo) return memo[targetSum]
if (targetSum === 0) return []
if (targetSum < 0) return null
let shortestCombination = null
for (const num of numbers) {
const remainder = targetSum - num
const remainderCombination = bestSum(remainder, numbers, memo)
if (remainderCombination !== null) {
const combination = [...remainderCombination, num]
if (
shortestCombination === null ||
combination.length < shortestCombination.length
)
shortestCombination = combination
}
}
memo[targetSum] = shortestCombination
return memo[targetSum]
}
console.log(bestSum(7, [1, 3, 2, 7])) // [7]
console.log(bestSum(7, [1, 4, 2])) // [2,4,1]
console.log(bestSum(7, [1, 4, 3, 2])) // [3,4]
console.log(bestSum(7, [1, 5, 6, 2])) // [6, 1]
console.log(bestSum(100, [1, 2, 3, 5, 10, 40])) //[ 40, 40, 10, 10 ]

這三個問題的總結
canConstruct

很顯然, 這和canSum是一類問題
尋找這個問題的邊界條件, 也就是遞回終止條件, 不斷減少字符的長度, 直到為空即可, 失敗就是剩余的字符的子字符不在wordbank里面
問題來了1. 如何存盤已經匹配的字符? 如何判斷當前字符已經不能再被匹配了?
遞回版
我的實作(錯誤版)
每次成功匹配后, 就分割字串, 依次查詢取結果的和運算結果, 當字串是空為成功結果, 回圈完了沒有符合條件, 有一個分割后的子串不能滿足情況的是失敗結果
const canConstruct = (target, wordBank) => {
if (target === '') return true
for (const word of wordBank) {
if (target.indexOf(word) !== -1) {
return target
.split(word, 2)
.reduce(
(pre, targetStr) => pre && canConstruct(targetStr, wordBank),
true
)
}
}
return false
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'Dog', 'Vs']))
仔細想一下, 這個有一個很大的問題, 就是程式匹配到第一個分割點后直接回傳, 沒有檢查第二個分割點是否還能滿足條件
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))// 本該為true, 輸出false
所以作出這樣的修改
const canConstruct = (target, wordBank) => {
if (target === '') return true
return wordBank.reduce((pre, word) => {
if (target.indexOf(word) !== -1) {
return (
pre ||
target
.split(word, 2)
.reduce(
(pre, targetStr) => pre && canConstruct(targetStr, wordBank),
true
)
)
}
return pre
}, false)
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
這樣就可以解決那個問題了, 但是這樣又有一個不太好的地方就是, 不能見好就收, 找到pre是true的時候就可停下來了, 所以, 我們可以使用some來代替, some 在回傳true的時候會停止回圈, 類似的every將會在回傳false的時候跳出回圈.
當然還可以使用throw+trycatch完成終止回圈, 但是那樣太奇怪了, 很反模式, 不過我還是實作了一下
some/every優化版
const canConstruct = (target, wordBank) => {
if (target === '') return true
console.log(target, wordBank)
return wordBank.some(
word =>
target.indexOf(word) !== -1 &&
target.split(word, 2).every(subStr => canConstruct(subStr, wordBank))
)
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
try-catch版
看著就惡心
const canConstruct = (target, wordBank) => {
if (target === '') return true
try {
return wordBank.reduce((pre, word) => {
if (pre === true) throw new Error(true)
if (target.indexOf(word) !== -1) {
return (
pre ||
target
.split(word, 2)
.reduce(
(pre, targetStr) => pre && canConstruct(targetStr, wordBank),
true
)
)
}
return pre
}, false)
} catch (e) {
// console.log(typeof e.message)
// 注意這里會把boolean轉成string, 直接return ture 就好了
return true
}
return false
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
視頻的實作
我們可以從左到右依次檢查是否是子串, 這樣就可以省很多事情, 而且遞回的時候可以不需要檢查兩邊的是否都滿足
這體現了一種轉換的思路
const canConstruct = (target, wordBank) => {
if (target === '') return true
for (const word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
if (canConstruct(suffix, wordBank) === true) return true
}
}
return false
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
之前忘了壓力測驗的用例了, 不用想, 肯定都跑不完
console.log(
canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', [
'e',
'ee',
'eee',
'eeee'
])
)
我的實作(正確版)
啊這, 過壓力測驗用例的時候, 發現: 使用split將會把每個e都分割掉, 所以會得到['','']的結果, 所以會錯
所以需要實作一個只分割一次的函式
const canConstruct = (target, wordBank) => {
if (target === '') return true
return wordBank.some(
word =>
target.indexOf(word) !== -1 &&
splitOnce(target, word).every(subStr => canConstruct(subStr, wordBank))
)
}
// 只分割一次的函式
const splitOnce = (str, sign) => {
const index = str.indexOf(sign)
if (index === -1) return [str]
return [str.slice(0, index), str.slice(index + sign.length)]
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
console.log(
canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', [
'ef',
'eeeeeeeeeee'
])
)
console.log(
canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', [
'e',
'ee',
'eee',
'eeeeeeeeeee'
])
)
dp版
我的實作
const canConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return true
memo[target] = wordBank.some(
word =>
target.indexOf(word) !== -1 &&
splitOnce(target, word).every(subStr =>
canConstruct(subStr, wordBank, memo)
)
)
return memo[target]
}
const splitOnce = (str, sign) => {
const index = str.indexOf(sign)
if (index === -1) return [str]
return [str.slice(0, index), str.slice(index + sign.length)]
}
視頻實作
const canConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return true
memo[target] = false
for (const word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
if (canConstruct(suffix, wordBank, memo) === true){
memo[target] = true
return true
}
}
}
return memo[target]
}

countConstruct

遞回版
我的實作
const countConstruct = (target, wordBank, counter = 0) => {
if (target === '') return counter + 1
for (const word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
counter = countConstruct(suffix, wordBank, counter)
}
}
return counter
}
視頻實作
const countConstruct = (target, wordBank) => {
if (target === '') return 1
let counter = 0
for (const word of wordBank)
if (target.indexOf(word) === 0)
counter += countConstruct(target.slice(word.length), wordBank)
return counter
}
dp版
const countConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return 1
let counter = 0
for (const word of wordBank)
if (target.indexOf(word) === 0)
counter += countConstruct(target.slice(word.length), wordBank, memo)
memo[target] = counter
return memo[target]
}
我覺得我已經挺熟練了

allConstruct

遞回版
我的實作
const allConstruct = (target, wordBank) => {
const path = []
helper(target, wordBank, [], path)
return path
}
const helper = (target, wordBank, currentPath = [], path = []) => {
if (target === '' && currentPath.length !== 0) {
path.push([...currentPath])
}
for (const word of wordBank) {
if (target.indexOf(word) === 0) {
const preCur = [...currentPath] // key: 保存之前的狀態, 每次獲取子元素的子路徑后還回去
currentPath.push(word)
helper(target.slice(word.length), wordBank, currentPath, path)
currentPath = preCur
}
}
}
說句實話我也不知道我在寫啥
視頻實作
const allConstruct = (target, wordBank) => {
if (target === '') return [[]]
const result = []
for (const word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
const suffixWays = allConstruct(suffix, wordBank)
const targetWays = suffixWays.map(way => [word, ...way])
result.push(...targetWays)
}
}
return result
}
dp版
const allConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return [[]]
const result = []
for (const word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
const suffixWays = allConstruct(suffix, wordBank, memo)
const targetWays = suffixWays.map(way => [word, ...way])
result.push(...targetWays)
}
}
memo[target] = result
return result
}
串列化tabulation
取消遞回, 使用陣列記錄, 研究每個之前情況對之后情況的影響
fib(nth)
const fib = n => {
const table = Array(n + 1).fill(0) // 初始化
table[1] = 1 // 開始, 人工賦值
for (let i = 0; i < n; i++) {
// 每個格子會影響后面的兩個格子
table[i + 1] += table[i]
table[i + 2] += table[i]
}
return table[n]
}
console.log(fib(6))
console.log(fib(7))
console.log(fib(8))
console.log(fib(50)) // 很快就出結果了
gridTraveler
const gridTraveler = (m, n) => {
const table = Array(m + 1)
.fill() //undefined 不能map
.map(() => Array(n + 1).fill(0)) //直接full會指向相同的參考
table[1][1] = 1
for (let i = 0; i <= m; i++) {
for (let j = 0; j <= n; j++) {
if (i + 1 <= m) table[i + 1][j] += table[i][j] // 二維陣列左值邊界檢查
if (j + 1 <= n) table[i][j + 1] += table[i][j]
}
}
return table[m][n]
}
console.log(gridTraveler(3, 2))
這類問題的總結
- 規劃你的table記錄什么
- 找出你的table的size , 維度
- 初始化table的值是多少
- 找到更新table的初值種子 (尋找那個和決定/隨機/資源沒有關系的情況 一般是0/1)
- 迭代更新table
- 考察每個格子對未來的格子的影響

canSum

target是0的時候, 一定是true
const canSum = (targetSum, numbers) => {
const table = Array(targetSum + 1).fill(false)
table[0] = true
for (let i = 0; i <= targetSum; i++) {
if (table[i] === true)
numbers.forEach(number => {
table[number + i] = true
})
}
return table[targetSum]
}
console.log(canSum(7, [3, 2]))
console.log(canSum(7, [4, 2]))
console.log(canSum(7, [5, 6, 2]))
console.log(canSum(300, [7, 14]))
小哥陷入無限回圈的問題: 不要時刻判斷length,這樣不好

howSum
const howSum = (targetSum, numbers) => {
const table = Array(targetSum + 1).fill(null)
table[0] = []
for (let i = 0; i <= targetSum; i++) {
if (table[i] !== null)
numbers.forEach(number => {
table[number + i] = [...table[i], number]
})
}
return table[targetSum]
}
console.log(howSum(7, [3, 2]))
console.log(howSum(7, [4, 2]))
console.log(howSum(7, [5, 6, 2]))
console.log(howSum(300, [7, 14]))
bestSum
const bestSum = (targetSum, numbers) => {
const table = Array(targetSum + 1).fill(null)
table[0] = []
for (let i = 0; i <= targetSum; i++) {
if (table[i] !== null)
numbers.forEach(number => {
if (!table[number + i] || table[number + i].length > table[i].length)
// 如果是null需要給予初值
table[number + i] = [...table[i], number]
})
}
return table[targetSum]
}
console.log(bestSum(7, [3, 2]))
console.log(bestSum(7, [4, 2]))
console.log(bestSum(7, [5, 6, 2]))
console.log(bestSum(300, [7, 14]))
canConstruct

const canConstruct = (target, wordBank) => {
const table = Array(target.length + 1).fill(false)
table[0] = true
for (let i = 0; i <= target.length; i++) {
if (table[i] === true)
wordBank.forEach(word => {
if (target.slice(i, i + word.length) === word)
table[i + word.length] = true
})
}
return table[target.length]
}
console.log(canConstruct('', ['cat']))
console.log(canConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(canConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
console.log(canConstruct('CatVsDog', ['Cat', 'V', 's', 'Dog']))
console.log(
canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', [
'ef',
'eeeeeeeeeee'
])
)
console.log(
canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', [
'e',
'ee',
'eee',
'eeeeeeeeeee'
])
)
countConstruct
const countSum = (target, wordBank) => {
const table = Array(target.length + 1).fill(0)
table[0] = 1
for (let i = 0; i <= target.length; i++) {
if (table[i] !== 0)
wordBank.forEach(word => {
if (target.slice(i, i + word.length) === word)
table[i + word.length] += table[i]
})
}
return table[target.length]
}
console.log(countSum('', ['cat']))
console.log(countSum('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(countSum('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(countSum('CatVsDog', ['Cat', 's', 'Do']))
console.log(countSum('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
console.log(countSum('CatVsDog', ['Cat', 'V', 's', 'Vs', 'Dog']))
allConstruct

const allConstruct = (target, wordBank) => {
const table = Array(target.length + 1)
.fill()
.map(() => [])
table[0] = [[]]
for (let i = 0; i < target.length; i++) {
wordBank.forEach(word => {
if (target.slice(i, i + word.length) === word) {
// 對于當前格子的每個情況都需要進行后續單詞的檢查
const newCombinations = table[i].map(subArr => [...subArr, word])
// 增加而不是覆寫
table[i + word.length].push(...newCombinations)
}
})
}
return table[target.length]
}
console.log(allConstruct('', ['cat']))
console.log(allConstruct('CatVsDog', ['Cat', 'Vs', 'Dog']))
console.log(allConstruct('CatVsDog', ['at', 'Vs', 'Dog']))
console.log(allConstruct('CatVsDog', ['Cat', 's', 'Do']))
console.log(allConstruct('CatVsDog', ['Cat', 'VsD', 'Vs', 'Dog']))
console.log(allConstruct('CatVsDog', ['Cat', 'V', 's', 'Vs', 'Dog']))
總結
遇見dp問題:
- 注意到重疊的子問題
- 決定什么是最小的輸入
- 想一下記憶化遞回
- 想一下串列化問題
- 畫一個策略, 樹或者陣列

Keep curious, keep learning
【Jeff 在寫代碼】有關代碼的一切的一切
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/337906.html
標籤:其他
下一篇:【滲透測驗筆記】之【內網滲透——應用層隧道(DNS隧道:dnscat2的使用(域名連接、直連、powershell連接))】
