js算法初窥05(算法模式02-动态规划与贪心算法)

简介:   在前面的文章中(js算法初窥02(排序算法02-归并、快速以及堆排)我们学习了如何用分治法来实现归并排序,那么动态规划跟分治法有点类似,但是分治法是把问题分解成互相独立的子问题,最后组合它们的结果,而动态规划则是把问题分解成互相依赖的子问题。

  在前面的文章中(js算法初窥02(排序算法02-归并、快速以及堆排)我们学习了如何用分治法来实现归并排序,那么动态规划跟分治法有点类似,但是分治法是把问题分解成互相独立的子问题,最后组合它们的结果,而动态规划则是把问题分解成互相依赖的子问题。

  那么我还有一个疑问,前面讲了递归,那么递归呢?分治法和动态规划像是一种手段或者方法,而递归则是具体的做操作的工具或执行者。无论是分治法还是动态规划或者其他什么有趣的方法,都可以使用递归这种工具来“执行”代码。

  用动态规划来解决问题主要分为三个步骤:1、定义子问题,2、实现要反复执行来解决子问题的部分(比如用递归来“反复”),3、识别并求解出边界条件。这么说有点懵逼....那么我们试试用动态规划来解决一些经典的问题。

一、最少硬币找零问题

  最少硬币找零问题是硬币找零问题的一个变种。硬币找零问题是给出要找零的钱数,以及可用的硬币面额以及对应的数量,找出有多少种找零的方法。最少硬币找零问题则是要找出其中所需最少数量的硬币。比如我们有1,5,10,25面额的硬币,如果要找36面额的钱,要如何找零呢?答案是一个25,一个10,一个1。这就是答案。那么如何把上面的问题转换成算法来解决呢?毕竟有了计算机很快速简单的就可以得到结果,不用我们再费力地用人脑去解决问题了,下面我们就来看一下代码:

//最少硬币找零
function MinCoinChange(coins) {
    // coins是有多少种面额的钱币。
    // 这里我们直接把构造函数传进来的参数用私有变量存储一下。
    var coins = coins;
    // 缓存结果集的变量对象
    var cache = {};
    // 定义一个构造函数的私有方法,
    this.makeChange = function (amount) {
        // 这里的this指向的就是this.makeChange私有函数本身,把它赋值给一个变量是为了不用在每次调用的时候都要计算(个人见解)
        var me = this;
        // amount就是我们要找零的钱数,如果为非正数,直接返回空数组,因为你找零的钱数不应该为负数。
        if(!amount) {
            return [];
        };
        
        // cache[amount]的判断是为了在重复计算前面已经计算过的结果时可以直接返回结果
        // 避免重复计算所造成的时间浪费
        if(cache[amount]) {
            return cache[amount];
        };
        // min用来存储最终结果的数组,newMin和newAmount分别是在逻辑的执行过程中,用于存储当前的符合条件的找零数组和找零钱数的。
        var min = [],newMin,newAmount;
        // 我们循环coins的长度。通过循环,我们为每一个conis数组中的面额都进行下面的逻辑操作。(主要是为当前coin做递归)
        for(var i = 0; i < coins.length; i++) {
            // 选择coins中的当前面额。
            var coin = coins[i];
            // 我们用要找零的钱数减去当前要找零的面额。并存储为newAmount变量。
            newAmount = amount - coin;
            // 在当前循环的递归中,如果newAmount是不小于0的值,也就是合法的找零的钱数,我们同样为该数调用找零方法。
            // 这里就是有点类似分而治之的那种策略了,递归求解。
            if(newAmount >= 0) {
                newMin = me.makeChange(newAmount);
            };
            // 在前面符合条件的newAmount递归后会进入下一个值得逻辑执行,然后就会到这里的逻辑判断
            // 下面的if判断主要是判断是否是当前的最优解,如果是,那么就放入我们最终的数组内。
            console.log(!min.length,min.length)
            if(newAmount >= 0 && (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount)) {
                min = [coin].concat(newMin);
                //console.log('new Min' + min + 'for' + amount);
            }
        };
        //cache存储了1到amount之间的所有结果
        //console.log(cache)
        return (cache[amount] = min);
    };
};

var minCoinChange = new MinCoinChange([1,5,10,25]);
console.log(minCoinChange.makeChange(36))

  这是用动态规划的方法来解决最少硬币找零问题,那么我们再来看看如何用贪心算法求解最少硬币找零的问题。那么什么是贪心算法呢?贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。

  我们还是来看下代码:

function MinCoinChange(coins) {
    var coins = coins;
    this.makeChange = function (amount) {
        var change = [],total = 0;
        for(var i = coins.length; i >= 0; i--) {
            var coin = coins[i];
            while(total + coin <= amount) {
                change.push(coin);
                total += coin;
            }
        }
        return change;
    };
}

var minCoinChange = new MinCoinChange([1,5,10,25]);
console.log(minCoinChange.makeChange(36))

  我们看上面的代码,主要逻辑跟动态规划十分相似,只是代码本身要简单了不少。贪心算法从我们的硬币中最大的开始拿,直到拿不了了再去拿下一个,直到返回最终结果。那么我们看看两种解决方法有什么不通过。动态规划会通过cache来缓存之前的计算结果,在当前的计算结果中与之前的对比,选择两者之间的最优解。而贪心算法则只是选择了当前的最优解,不会回退,也不会去存储记录之前的解决方案。

 

二、背包问题

  背包问题其实是一个组合优化问题,问题是这样的,给定一个固定大小,能携带重量为W的背包,以及一组有价值和重量的物品,找出一个最佳解决方案,使得装入背包的物品总重量不超过W,且总价值是最大的。这个问题有两个版本,一个是0-1背包问题,该版本只允许背包里装入完整的物品,不能拆分。还有另外一个是可以装入分数物品。我们后面会用贪心算法来解决分数背包问题。

  我们来看代码:

//背包问题
function knapSack(capacity,weights,values,n) {
    var i,w,a,b,kS = [];

    for (var i = 0; i <= n; i++) {
        kS[i] = [];
    }

    for(i = 0; i <= n; i++) {
        for(w = 0; w <= capacity; w++) {
            if(i == 0 || w == 0) {
                kS[i][w] = 0;
            } else if(weights[i - 1] <= w) {
                a = values[i - 1] + kS[i - 1][w - weights[i - 1]];
                b = kS[i - 1][w];
                kS[i][w] = (a > b) ? a : b;
            } else {
                kS[i][w] = kS[i - 1][w];
            }
        }
    }
    findValues(n,capacity,kS,weights,values);
    return kS[n][capacity];
};

function findValues(n,capacity,kS,weights,values) {
    var i = n,k = capacity;
    console.log('解决方案包括以下物品:');
    while(i > 0 && k > 0) {
        if(kS[i][k] !== kS[i - 1][k]) {
            console.log('物品' + i + ',重量:' + weights[i- 1] + ',价值:' + values[i - 1]);
            i--;
            k = k - kS[i][k];
        } else {
            i--;
        }
    }
}

var values = [3,4,5],weights = [2,3,4],capacity = 5,n = values.length;
console.log(knapSack(capacity,weights,values,n))

  上面的代码中,我们最开始初始化一个矩阵,用来存放各种解决方案,而且要注意装入背包的物品i必须小于capacity,也就是小于背包可容纳的重量,才可以成为装入背包的一部分,不然你一个物品就超过了背包可容纳的重量,这是不允许的。并且当有两个物品重量相同的时候,我们选择价值较大的哪一个。

  其实上面的算法还可以继续优化,这里不做多讲,大家有兴趣可以深入学习。

贪心算法的分数背包问题:

  分数背包问题和0-1背包问题类似,只是我们可以在分数背包中加入部分的物品。代码并不难,大家自己写一下就明白了。

function knapSack(capacity,values,weights) {
    var n = values.length,load = 0,i = 0,val = 0;

    for(i = 0; i < n && load < capacity; i++) {
        if(weights[i] <= (capacity - load)) {
            val += values[i];
            load += weights[i];
        } else {
            var r = (capacity - load) / weights[i];
            val += r * values[i];
            load += weights[i];
        }
    }
    return val;
}
var values = [3,4,5],weights = [2,3,4],capacity = 6;

console.log(knapSack(capacity,values,weights))

三、最长公共子序列问题

  该问题是这样的,找出两个字符串序列中的最长子序列的长度。最长子序列是指,在两个字符串序列中以相同的顺序出现,但不要求一定是连续的字符串序列。

//最长公共子序列LCS
function lcs(wordX,wordY) {
    var m = wordX.length,n = wordY.length,l = [],i,j,a,b;
    var solution = [];

    for (i = 0; i <= m; ++i) {
        l[i] = [];
        solution[i] = [];
        for(j = 0; j <= n; ++j) {
            l[i][j] = 0;
            solution[i][j] = '0';
        }
    }

    for(i = 0; i <= m; i++) {
        for(j = 0; j <= n; j++) {
            if(i == 0 || j == 0) {
                l[i][j] = 0;
            } else if(wordX[i - 1] == wordY[j - 1]) {
                l[i][j] = l[i - 1][j - 1] + 1;
                solution[i][j] = 'diagonal';
            } else {
                a = l[i - 1][j];
                b = l[i][j - 1];
                l[i][j] = (a > b) ? a : b;
                solution[i][j] = (l[i][j] == l[i - 1][j]) ? 'top' : 'left';
            }
        }
    }
    printSolution(solution,l,wordX,wordY,m,n);
    return l[m][n];
}

function printSolution(solution,l,wordX,wordY,m,n) {
    var a = m,b = n,i,j,
    x = solution[a][b],
    answer = '';

    while(x !== '0') {
        if(solution[a][b] === 'diagonal') {
            answer = wordX[a - 1] + answer;
            a--;
            b--;
        } else if(solution[a][b] === 'left') {
            b--;
        } else if(solution[a][b] === 'top') {
            a--;
        }
        x = solution[a][b];
    }
    console.log('lcs:' + answer);
}

lcs("acbaed","abcadf");

   

四、矩阵链相乘

  该问题是要找出一组矩阵相乘的最佳方式(顺序),在开始之前,有必要给大家简单讲解一下矩阵相乘,简单来说就是,加入一个n行m列的矩阵A和m行p列的矩阵B相乘,会得到一个n行p列的矩阵C。要注意,只有一个矩阵的行与另一个矩阵的列相同两个矩阵才可以想乘。

  那么如果我想有A,B,C,D四个矩阵相乘,由于乘法满足结合律(小学数学知识点)。所以我们可以这样(A(B(CD))),或者这样((AB)(CD))等五种相乘的方法,但是要注意的是,每种相乘的顺序不一样,我们的计算量也是不一样的。所以,我们来构建一个函数,找出计算量最少的相乘方法。这就是矩阵链相乘问题了。

//矩阵链相乘
function matrixChainOrder(p,n) {
    var i,j,k,l,q,m = [];


    //辅助矩阵s
    var s = [];
    for(i = 0; i <= n; i++) {
        s[i] = [];
        for(j = 0; j <= n; j++) {
            s[i][j] = 0;
        }
    }

    for(i = 0; i <= n; i++) {
        m[i] = [];
        m[i][i] = 0;
    };

    for(l = 2; l < n; l++) {
        for(i = 1; i <= n - l + 1; i++) {
            j = i + l - 1;
            m[i][j] = Number.MAX_SAFE_INTEGER;
            for(k = i; k <= j - 1; k++) {
                q = m[i][k] + m[k + 1][j] + p[i - 1]*p[k]*p[j];
                if(q < m[i][j]) {
                    m[i][j] = q;
                    s[i][j] = k;//辅助矩阵
                }
            }
        }
    }
    printOptimalParenthesis(s,1,n - 1);
    return m[1][n - 1];
}

function printOptimalParenthesis(s,i,j) {
    if(i == j) {
        console.log("A[" + i + "]");
    } else {
        console.log("(");
        printOptimalParenthesis(s,i,s[i][j]);
        printOptimalParenthesis(s,s[i][j] + 1,j);
        console.log(")");
    }
}

var p = [10,100,5,50,1,100];
n = p.length;
console.log(matrixChainOrder(p,n));

 

  最后,由于本人水平有限,能力与大神仍相差甚远,若有错误或不明之处,还望大家不吝赐教指正。非常感谢!

一只想要飞得更高的小菜鸟
目录
相关文章
|
24天前
|
存储 安全 JavaScript
云计算浪潮中的网络安全之舵探索Node.js中的异步编程模式
【8月更文挑战第27天】在数字化时代的风帆下,云计算如同一片广阔的海洋,承载着企业与个人的数据梦想。然而,这片海洋并非总是风平浪静。随着网络攻击的波涛汹涌,如何确保航行的安全成为了每一个船员必须面对的挑战。本文将探索云计算环境下的网络安全策略,从云服务的本质出发,深入信息安全的核心,揭示如何在云海中找到安全的灯塔。
|
22天前
|
JavaScript 算法 前端开发
JS算法必备之String常用操作方法
这篇文章详细介绍了JavaScript中字符串的基本操作,包括创建字符串、访问特定字符、字符串的拼接、位置查找、大小写转换、模式匹配、以及字符串的迭代和格式化等方法。
JS算法必备之String常用操作方法
|
22天前
|
JavaScript 算法 前端开发
JS算法必备之Array常用操作方法
这篇文章详细介绍了JavaScript中数组的创建、检测、转换、排序、操作方法以及迭代方法等,提供了数组操作的全面指南。
JS算法必备之Array常用操作方法
|
4天前
|
JavaScript 前端开发 开发者
探索Node.js中的异步编程模式
【9月更文挑战第15天】在Node.js的世界中,“一切皆异步”不仅是一句口号,更是其设计哲学的核心。本文将带你深入理解Node.js中异步编程的几种主要模式,包括经典的回调函数、强大的Promise对象、以及简洁的async/await结构。我们将通过实例代码来展示每种模式的使用方式和优缺点,帮助你更好地掌握Node.js异步编程的精髓。无论你是Node.js新手还是有一定经验的开发者,这篇文章都能给你带来新的启示和思考。让我们一起开启Node.js异步编程的探索之旅吧!
|
6天前
|
JavaScript 前端开发 中间件
深入浅出Node.js中间件模式
【9月更文挑战第13天】本文将带你领略Node.js中间件模式的魅力,从概念到实战,一步步揭示如何利用这一强大工具简化和增强你的Web应用。我们将通过实际代码示例,展示中间件如何在不修改原有代码的情况下,为请求处理流程添加功能层。无论你是前端还是后端开发者,这篇文章都将为你打开一扇通往更高效、更可维护代码的大门。
|
1月前
|
设计模式 JavaScript 前端开发
Vue.js组件设计模式:构建可复用组件库
在Vue.js中,构建可复用组件库是提升代码质量和维护性的核心策略。采用单文件组件(SFC),定义props及默认值,利用自定义事件和插槽进行灵活通信,结合Vuex或Pinia的状态管理,以及高阶组件技术,可以增强组件的功能性和灵活性。通过合理的抽象封装、考虑组件的可配置性和扩展性,并辅以详尽的文档和充分的测试,能够打造出既高效又可靠的组件库。此外,采用懒加载、按需导入技术优化性能,制定设计系统和风格指南确保一致性,配合版本控制、CI/CD流程和代码审查机制,最终形成一个高品质、易维护且具有良好社区支持的组件库。
49 7
|
29天前
|
设计模式 JavaScript 前端开发
Vue.js 组件设计模式:在前端热潮中找到归属感,打造可复用组件库,开启高效开发之旅!
【8月更文挑战第22天】Vue.js 以其高效构建单页应用著称,更可通过精良的组件设计打造可复用组件库。组件应职责单一、边界清晰,如一个显示文本并触发事件的按钮组件,通过 props 传递标签文本,利用插槽增强灵活性,允许父组件注入动态内容。结合 CSS 预处理器管理和封装独立模块,配以详尽文档,有效提升开发效率及代码可维护性。合理设计模式下,组件库既灵活又强大,持续实践可优化项目工作流。
40 1
|
1月前
|
JavaScript 算法 前端开发
"揭秘Vue.js的高效渲染秘诀:深度解析Diff算法如何让前端开发快人一步"
【8月更文挑战第20天】Vue.js是一款备受欢迎的前端框架,以其声明式的响应式数据绑定和组件化开发著称。在Vue中,Diff算法是核心之一,它高效计算虚拟DOM更新时所需的最小实际DOM变更,确保界面快速准确更新。算法通过比较新旧虚拟DOM树的同层级节点,递归检查子节点,并利用`key`属性优化列表更新。虽然存在局限性,如难以处理跨层级节点移动,但Diff算法仍是Vue高效更新机制的关键,帮助开发者构建高性能Web应用。
39 1
|
29天前
|
JavaScript 前端开发 安全
TypeScript:解锁JavaScript的超级英雄模式!类型系统如何化身守护神,拯救你的代码免于崩溃与混乱,戏剧性变革开发体验!
【8月更文挑战第22天】TypeScript作为JavaScript的超集,引入了强大的类型系统,提升了编程的安全性和效率。本文通过案例展示TypeScript如何增强JavaScript:1) 显式类型声明确保函数参数与返回值的准确性;2) 接口和类加强类型检查,保证对象结构符合预期;3) 泛型编程提高代码复用性和灵活性。这些特性共同推动了前端开发的标准化和规模化。
48 0
|
15天前
|
算法 BI Serverless
基于鱼群算法的散热片形状优化matlab仿真
本研究利用浴盆曲线模拟空隙外形,并通过鱼群算法(FSA)优化浴盆曲线参数,以获得最佳孔隙度值及对应的R值。FSA通过模拟鱼群的聚群、避障和觅食行为,实现高效全局搜索。具体步骤包括初始化鱼群、计算适应度值、更新位置及判断终止条件。最终确定散热片的最佳形状参数。仿真结果显示该方法能显著提高优化效率。相关代码使用MATLAB 2022a实现。