算法设计
2020-09-22 21:40:59 0 举报
AI智能生成
清华大学算法设计课程
作者其他创作
大纲/内容
分治<br>divide and conquer
概念
基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题<b><font color="#16884a">相互独立且与原问题性质相同。<br></font>求出子问题的解,<font color="#16884a">合并</font>得到原问题的解。</b><br>
三个基本步骤
分
治
合(关键)
常见用法
<ul><li>Break up problem of size n into two equal parts of size 1⁄2 n.</li><li><b>Solve two parts recursively.</b></li><li><b>Combine two solutions</b> into overall solution in linear time.</li></ul>
效果
Brute force: n2.<br><br>Divide-and-conquer: n log n.(以2为底)<br>
归并排序
n Divide array into two halves.<br>n Recursively sort each half.<br>n Merge two halves to make sorted whole.<br>
merge efficiently
<font color="#c41230">双指针法</font>
计算逆序数
<br>
最近点对
<br>
<br>
动态规划<br>
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。<br>还是把问题划分为子问题,但是<b><font color="#16884a">子问题相互关联(与分治法的不同之处)。</font></b><br><br>把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。<br>
基本步骤/一般套路:<br><br>1.列出动态规划基本方程<br>(注意有些并没有类似“加权区间安排”问题那样的较简单清晰的基本方程,<br>有些问题可能并不好归纳出方程,直接通过文字与代码来表达更简单)<br><br>2.不少问题需要用到递归<br> (1)要缓存子问题的结果<br><br> (2)从底向上比从上往底效率高<br> 可以从底向上计算M[i]/M[i][j] 的值<br> 从底向上其实就是用的尾递归,尾递归可以大大缩减使用的栈空间<br>
加权区间安排
<font color="#0076b3">Greedy algorithm works if all weights are 1.</font>
动态规划基本方程
p(j) = largest index i < j such that job i is compatible with j.
<br>
Finding a Solution<br><br>目前理解应该是<br>找到使结果最优的元素搭配<br>
这里 Find-Solution 是一个函数名字。<br><br>实现中也用到了递归。
从底向上
<br>
分段最小二乘
<br>
这里对 n3 的来源不懂, eij 和M[i-1] 不是已经算好了只要取值的么
背包问题
<br>
<br>
这里 <b><font color="#16884a">M[i,w] 中的 i 代表只考虑序号 <= i 的物品,w 代表此时的容量。</font></b><br>
const items = [<br> { w: 1, v: 3 },<br> { w: 2, v: 7 },<br> { w: 3, v: 12 },<br>];<br>const W = 5; //背包的最大容量<br>console.log(knapsack(items, W));<br>
function knapsack(items, W) {<br> let M = [],<br> len = items.length;<br><br> // 初始化二维数组<br> for (let i = 0; i < len; i++) {<br> M[i] = new Array(W + 1).fill(0);<br> }<br> // 初始化只考虑序号 <= 0 的物品<br> for (let i = 0; i < W + 1; i++) {<br> M[0][i] = items[0].w <= i ? items[0].v : 0;<br> }<br> console.table(M);<br> for (let i = 1; i < len; i++) {<br> for (let w = 0; w <= W; w++) {<br> let cW = items[i].w,<br> cV = items[i].v;<br><b><font color="#16884a"> if (cW > w) {<br> M[i][w] = M[i - 1][w];<br> } else {<br> M[i][w] = Math.max(M[i - 1][w - cW] + cV, M[i - 1][w]);<br> }</font></b><br> }<br> }<br> console.table(M);<br> let maxV = M[len - 1][W];<br><font color="#924517"> // 如何得到最大价值的物品组合呢??</font><br> let arr = M[len - 1];<br> let combinedItemIndex = getCombinedItemIndex(arr, items);<br> return { maxV, combinedItemIndex };<br>}
二维数组的构成
代码中生成的二维数组
function getCombinedItemIndex(arr, items) {<br>// 以后再和懂的人讨论讨论其他实现方式<br><font color="#924517"> // 从最大值往前推,如果 M[len-1][W]-M[len-1][wi] 存在于 items 中,<br> //就 push 此 item 入结果数组,<br> // 然后再从 M[len-1][wi] 按照上面步骤往前推算</font><br> let combinedItems = [],<br> ind = arr.length - 1;<br><br><font color="#16884a"> // 如果设置成 ind>=0,当 ind==0时,此 while 循环会一直空转<br><b> while (ind > 0) {</b><br><b> for (let i = ind - 1; i >= 0; i--) {</b><br> let matchedItem = existValueItem(arr[ind] - arr[i], items);<br> if (matchedItem) {<br> combinedItems.push(matchedItem);<br><b>// 这个算法里 i 总会到0的</b><br><b> ind = i;</b><br> break;<br> }<br> }<br> }</font><br> return combinedItems;<br><br> function existValueItem(v, items) {<br> for (let i = 0, len = items.length; i < len; i++) {<br> if (items[i].v === v) return i;<br> }<br> return false;<br> }<br>}<br>
课后练习
走台阶
There is a stairs with a height of 10 steps. You can climb stairs only by two or three steps. So you can go by _____ different kinds of ways.
function steps(n) {<br> //第几个元素(元素下标+1)代表有多少个台阶,元素值代表此台阶级数(第几个元素)下有多少种走法<br><b><font color="#c41230">//将结果缓存在对象中会比数组好理解,以后用对象来存结果</font></b><br> let M = [0, 1, 1];<br> for (let i = 3; i < n; i++) {<br> M[i] = M[i - 2] + M[i - 3];<br> }<br> return M[n - 1];<br>}<br>console.log(steps(10));<br>
多项式最大值
Using dynamic programming to solve the following integer linear programming, the objective value is _____
integerLinear();<br>function integerLinear() {<br> let Max=10;//此题 x1 x2 x3 都为0时的结果<br><br>//循环的初始条件用到了动态规划<br> for (let x1 = 0; x1 <= 4; x1++) {<br> for (let x2 = 4 - x1; x2 >= 0; x2--) {<br> for (let x3 = 4 - x1 - x2; x3 >= 0; x3--) {<br> let value =<br> 5 * x1 * x1 +<br> 3 * x2 * x2 +<br> x3 * x3 -<br> 10 * x1 -<br> 3 * x2 +<br> 2 * x3 +<br> 10;<br> if(value>Max){<br> Max=value<br> }<br> }<br> }<br> }<br> console.log(Max)<br> return Max<br>}<br>
其他题目
<b style="color: rgb(196, 18, 48);">注意在很多涉及到递归的动态规划算法里其核心函数需要接收一些<br>在递归过程中不断变化的参数(一般是数组)</b><font color="#5c5c5c">(这倒也呼应了“动态规划”这个术语),<br>如下面两题的 result 参数。<br>这时候调用递归函数时不能改变原来的参数值(状态)。</font><br><font color="#c41230">当参数为引用类型时需要好好注意!!!</font><br><b style="color: rgb(196, 18, 48);">对于数组类型的参数,用 concat , slice 等不改变原数组的方法返回新数组当参数,<br>不要用 push 或者 splice 这些会改变原数组的变异方法。</b><br>对于对象类型的参数,也不能改变原对象,要保持原对象不变,生成新的对象当参数。
全排列
下面的方式是我 2019.12.18 闭卷手写的,用了尾递归的方案<br><b><font color="#c41230">注意递归调用时不能改变原 result 与 source<br></font></b>
// 全排列 2019.12.18 闭卷手写成功 开心^3^<br>// [1,2,3]=><br>// [1,2,3] [1,3,2]<br>// [2,1,3] [2,3,1]<br>// [3,1,2] [3,2,1]<br>function allSequence(arr) {<br><b><font color="#c41230"> // 思路是 result 参数是用来携带排列结果的数组<br> // 循环 source 中的项目,从 source 参数中选一项,push 到递归函数的 result 参数里, <br> // 同时递归函数的 source 参数要减少这一项</font></b><br> const arrangeOneFromSource = <b><font color="#c41230">function(result, source)</font></b> {<br> if (source.length === 0) {<br> // 递归出口 一个排列完成了<br> // 处理排列结果 这里就打印好了<br> console.log(result);<br> return result;<br> }<br> <br><b><font color="#c41230"> // 这下面算是动态规划的基本思路</font></b><br> for (let i = 0; i < source.length; i++) {<br> const ele = source[i];<br><br><b><font color="#c41230">// 注意递归调用时不能改变原 result 与 source 入参,<br> // 不然一次排列完成后,原 result 参数已经是第一种排列的结果,source 参数也成了空数组, <br> // source.length===0,这个循环体也不会再执行了<br> // 要生成新的数组当递归函数的入参<br> // 用 concat 与 slice 来生成新的数组<br> // 不要用 push 或 splice 来改变原数组<br> <br> // 这个函数算尾递归</font></b><br> arrangeOneFromSource(<br> <b> result.concat([ele]),<br> source.slice(0, i).concat(source.slice(i + 1))</b><br> );<br> }<br> };<br>// 初始调用<br><b> arrangeOneFromSource([], arr);</b><br>}<br>allSequence([1, 2]);<br>
八皇后
// 八皇后 12.20日闭卷手写<br>// 8*8棋盘的里面的位置描述:<br>// 2020/01/24<br><b>// 其实就用一个一维的数组就可以表示,下标表示行,下标对应的值表示这行的棋子放哪列</b><br><br>eightQueen();<br><br>function eightQueen() {<br> //这里的 count 一定要定义在调用 processLine 方法的前面<br> let count = 0;<br> <br> processLine(0, []);<br> <br> function <b><font color="#c41230">processLine(n, result)</font></b> {<br> if (n === 8) {<br> //出口,处理结果<br> console.log(result);<br> console.log(++count);<br> return result;<br> }<br><br>// 下面算是动态规划的基本思路<br> // 尝试每一列的位置<br><b><font color="#c41230"> for (let j = 0; j < 8; j++) {<br> let curPos = j;<br> // 判断是否和之前的项目不冲突<br> if (!isConflict(result, curPos)) {<br> // 不冲突就进入下一行<br> // 递归调用函数时 result 参数合并上此位置<br> // 这里不能用 push 改原 result 参数数组,<br> // 不然在同一行遍历下一个位置的时候,result 参数已经被塞过上一轮的数值了<br> processLine(n + 1, result.concat([curPos]));<br> }<br> }</font></b><br> }<br> function isConflict(arr, item) {<br> let arrL = arr.length;<br> for (let i = 0; i < arrL; i++) {<br> const ele = arr[i];<br> if (isConflictWithItem([i, ele], [arrL, item])) {<br> return true;<br> }<br> }<br> return false;<br> }<br><br> //[r1,c1],[r1,c1]<br> function isConflictWithItem([r1, c1], [r2, c2]) {<br> if (r1 === r2 || c1 === c2) return true;<br> let rowDis = r1 - r2,<br> colDis = c1 - c2;<br> if (rowDis === colDis || rowDis === -colDis) return true;<br> return false;<br> }<br>}<br>
二维数组环形遍历
// 二维数组环形遍历<br>const arr = [<br> [1, 2, 3, 31],<br> [4, 5, 6, 61],<br> [7, 8, 9, 91],<br> [10, 11, 12, 13]<br>];<br>const arr1 = [<br> [1, 2, 3, 31],<br> [4, 5, 6, 61],<br> [7, 8, 9, 91]<br>];<br>const arr2 = [<br> [1, 2, 3],<br> [4, 5, 6],<br> [7, 8, 9],<br> [10, 11, 12]<br>];<br><br>circleTraverse(arr2);<br>
function circleTraverse(arr) {<br> let width = arr[0].length;<br> let height = arr.length;<br><b> let level = Math.ceil(Math.max(width, height) / 2);</b><br> <br> // 遍历第几层<br><b> // 最外层的为 0 层,从 arr[0][0] 开始<br> // 里面一层为 1 层,从 arr[1][1] 开始</b><br> function traverseLevel(n) {<br> let colEnd = width - n - 1,<br> colStart = n;<br> let rowEnd = height - n - 1,<br> rowStart = n;<br> // up<br> for (let i = colStart; i <= colEnd; i++) {<br> console.log(arr[rowStart][i]);<br> }<br> // right<br> for (let i = rowStart + 1; i <= rowEnd; i++) {<br> console.log(arr[i][colEnd]);<br> }<br> // bottom<br><b><font color="#c41230"> if (rowEnd != rowStart)</font></b> {<br> for (let i = colEnd - 1; i >= colStart; i--) {<br> console.log(arr[rowEnd][i]);<br> }<br> }<br> // left<br><b><font color="#c41230"> if (colEnd != colStart)</font> </b>{<br> for (let i = rowEnd - 1; i >= rowStart + 1; i--) {<br> console.log(arr[i][colStart]);<br> }<br> }<br><b><font color="#c41230"> if (++n <= level) {<br> traverseLevel(n);<br> }</font></b><br> }<br> traverseLevel(0);<br>}<br>
最长递增子序列
最长递增子序列意思是在一组数字中,找出最长一串递增的数字,比如<br><br>0, 3, 4, 17, 2, 8, 6, 10<br><br>对于以上这串数字来说,最长递增子序列就是 0, 3, 4, 8, 10,可以通过以下表格更清晰的理解
通过以上表格可以很清晰的发现一个规律,<br><font color="#16884a">找出刚好比当前数字小的数,并且在小的数组成的长度基础上加一。</font><br>
这个问题的动态思路解法很简单,直接上代码<br><br>function lis(n) {<br> if (n.length === 0) return 0<br> // 创建一个和参数相同大小的数组,并填充值为 1<br> let array = new Array(n.length).fill(1)<br> // 从索引 1 开始遍历,因为数组已经所有都填充为 1 了<br> for (let i = 1; i < n.length; i++) {<br> // 从索引 0 遍历到 i<br> // 判断索引 i 上的值是否大于之前的值<br> for (let j = 0; j < i; j++) {<br> <font color="#16884a">if (n[i] > n[j]) {<br> array[i] = Math.max(array[i], 1 + array[j])<br> }</font><br> }<br> }<br> let res = 1<br> for (let i = 0; i < array.length; i++) {<br> res = Math.max(res, array[i])<br> }<br> return res<br>
参考
file:///Users/lucy/books/FE/前端面试之道/32-常考算法题解析.htm
数钱<br>猿辅导网上搜集<br>面试题<br>
给出一个实际金额,比如40<br><br>以及一个优惠券面额列表比如[30, 50, 100],每种优惠券数量不限<br><br>要求返回能组合成实际金额的最大值,<br>比如:实际金额40 -> 返回30;<br>80 -> 80=30+50;<br>110 -> 110=30+30+50<br>
测试
console.log('10:',maxMoney(10));<br>console.log('30:',maxMoney(30));<br>console.log("40:", maxMoney(40));<br>console.log("80:", maxMoney(80));<br>console.log("180:", maxMoney(180));<br>
代码
function maxMoney(targetValue) {<br><b> const Memo = {}; // 缓存</b><br> const List = [30, 50, 100]; //<b>从小到大排序</b><br><br> return getMaxMoney(targetValue);<br><br> function getMaxMoney(targetValue) {<br> if (Memo[targetValue]) {<br> console.log("Meno has targetVal: " + targetValue);<br> return Memo[targetValue];<br> }<br> console.log("call maxMoney(" + targetValue + ")");<br><br> let max = 0;<br> // <b>基本方程</b><br> for (let i = 0, len = List.length; i < len; i++) {<br> if (List[i] < targetValue) {<br> <b><font color="#0076b3"> max = Math.max(<br> List[i] + getMaxMoney(targetValue - List[i]),<br> max<br> );</font></b><br> } else if (List[i] === targetValue) {<br> // return List[i]<br> max = targetValue;<br> } else {<br> // List[i] > targetValue 时<br> // return 0 <br> //<b> 这里不能直接 return 0,<br> </b>// <b>因为在之前的循环中 max 可能已经有了大于 0 的值</b><br> <b><font color="#0076b3"> max = Math.max(0, max);</font></b><br> }<br> }<br> Memo[targetValue] = max;<br> return max;<br> }<br>}<br>
图
连通性与最短路径
广度优先搜索
可以用 queue 实现,在 while 循环中动态 push 与 shift,直到 queue 清空<br>其他实现方式待会查
贪婪算法<br>都要先将项目根据一个希望对其贪婪的指标排序
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。<br>也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。<br><br>贪心算法<b>不是对所有问题都能得到整体最优解</b>。<br><br>贪心算法在某些情况下可能都得不到结果。<br>
换硬币问题
<br>
类似换硬币的问题中贪婪算法不一定是最优解。<br><br>可能使用贪婪算法会得不到解,如下<br>目标:140<br>面值:100 70<br>
美国邮政系统
区间安排<br>interval scheduling
[Earliest start time] Consider jobs in ascending order of start time sj.<br>
[Earliest finish time]<br> Consider jobs in ascending order of finish time fj<br>
唯一可能是最优的算法
<br>
<br>
一般套路:<br><br><b>1.按照某个指标将原始数据排序</b><br>2.用 for 循环<br>2.1 在循环体中找到与结果集兼容的项目,添加到结果集中,动态修改了结果集<br>之后的项目在运行循环体时就需要与新的结果集兼容了<br>
不知道项目数量的用 while 循环,<br>知道的用 for 循环
区间安排问题中贪婪算法是最优解
[Shortest interval]<br> Consider jobs in ascending order of interval length fj - sj.<br>
[Fewest conflicts] <br>For each job, count the number of conflicting jobs cj. Schedule in ascending order of conflicts cj.<br>
区间划分(如给若干讲座分配最少教室)<br>interval partioning
最早开始时间算法
<br>
区间划分中贪婪算法是最优解
最小延迟
[Shortest processing time first] Consider jobs in ascending order<br>of processing time tj.<br>
[Earliest deadline first] Consider jobs in ascending order of deadline dj.<br>
<br>
[Smallest slack] Consider jobs in ascending order of slack dj - tj.
优化缓存
剔除未来最远的元素(Farthest-in-future)
是最优算法,证明其最优内容比较多,以后有需要再看
最短路径
dijkstra 算法
子主题
<br>
最小扩展树
<br>
应用
network design
telephone, electrical, hydraulic, TV cable, computer, road<br>
approximation algorithms for NP-hard problems(这里不懂,以后需要了再了解)
traveling salesperson problem, Steiner tree
Cluster analysis.(同上,以后需要了再了解)
算法
Kruskal's algorithm(目前感觉这个算法思路与实现更简单)
Start with T = []. Consider edges in ascending order of cost. Insert edge e in T unless doing so would create a cycle.
Prim's algorithm
Start with some root node s and greedily grow a tree T from s outward. At each step, add the cheapest edge e to T that has exactly one endpoint in T.<br>
聚类
k-clustering
Divide objects into k non-empty groups. Find a k-clustering of maximum spacing
Single-link k-clustering algorithm
Form a graph on the vertex set U, corresponding to n clusters.(n 个点就初始化为 n 个聚类)<br><br>Find the closest pair of objects such that each object is in a<br>different cluster, and add an edge between them.(找到并连接最近的聚类,聚类数量-1)<br><br>Repeat n-k times until there are exactly k clusters.<br>
找到最近的聚类
This procedure is precisely Kruskal's algorithm (except we stop when there are k connected components)
回溯
回溯算法<b><font color="#16884a">实际上是一个类似枚举的搜索尝试过程,</font>主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。</b><br>回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。<br>但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,<br>而满足回溯条件的某个状态的点称为“回溯点”。<br><br><b>许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。</b><br>
基本思想
从一条路往前走,能进则进,不能进则退回来,换一条路再试。
回溯算法说白了就是穷举法。<br>不过回溯算法使用剪枝函数,剪去一些不可能到达最终状态(即答案状态)的节点,从而减少状态空间树节点的生成。<br>
典型问题
八皇后
回溯算法求解八皇后问题的原则是:有冲突解决冲突,没有冲突往前走,无路可走往回退,走到最后是答案。
代码
0 条评论
下一页