数据结构与算法笔记:基础篇 - 初始动态规划:如何巧妙解决“双十一”购物时的凑单问题?

概述

淘宝的 “双十一” 购物节有各种促销活动,比如 “满 200 元减 50元”。假设你女朋友购物车中有 n 个(n > 100)想买的商品,它希望从里面选几个,在凑够满减条件的前提下,让选出来的商品价格总和最长成都接近满减条件(200 元),这样就可以极大限度地 “薅羊毛”。作为程序员的你,能不能编个代码来帮她搞定?

要想高效地解决这个问题,就要用到本章讲的动态规划(Dynamic Programming)。


动态规划学习线路

动态规划比较适合用来求解最优问题,比如求最大值、最小值等等。它可以非常显著地降低时间复杂度,提高代码的执行效率。不过,它也是出了名的难学。它的主要学习难点跟递归类似,那就是求解问题的过程不太符合人类常规的思维方式。对于新手来说,想要入门确实不容易。不过,等你掌握了之后,你会发现,实际上并没有想象中那么难。

为了让你更容易理解动态规划,我分了三章进行讲解。这三章分别是,初始动态规划、动态规划理论、动态规划实战。

第一章,我会通过两个非常经典的动态规划问题模型,向你展示我们为什么需要动态规划,以及动态规划解题方法是如何演化出来的。实际上,你只要掌握了这两个例子的解决思路,对于其他很多动态规划问题,你都可以套用类似的思路来解决。

第二章,我会总结动态规划适合解决的问题的特征,以及动态规划解题思路。此外,还会将贪心、分治、回溯、动态规划这四种算法思想放在一起,对比分析它们各种的特点以及适用的场景。

第三章,我会教你应用第二节讲的动态规划理论知识,实战解决三个非常经典的动态规划问题,加深你对理论的理解。弄懂了这三章中的例子,对于动态规划这个知识点,你就算是入门了。

0-1 背包问题

再讲贪心算法、回溯算法时,多次讲到背包问题。本章,依旧拿这个问题来举例。

对于一组不同重量、不可分割的物品,我们需要选择一些装入背包,在满足背包最大重量限制的前提下,背包中物品总重量的最大值是多少?

关于这个问题,上篇文章讲了回溯的解决方法,也就是穷举搜索所有可能得装法,然后找出满足条件的最大值。不过,回溯算法的复杂度比较高,是指数级别。有没有什么规律,可以有效降低时间复杂度呢?我们一起来看看。

    // 回溯算法实现
    private int maxW = Integer.MIN_VALUE; // 结果放到 maxW 中
    private int[] weight = {2,2,4,6,3}; // 物品重量
    private int n = 5; // 物品个数
    private int w = 9; // 背包承受的最大重量
    public void f(int i, int cw) { // 调用f(0,0)
        if (cw == w || i == n) { // cw == w表示装满了;i==n表示物品都考察完了
            if (cw > maxW) maxW = cw;
            return;
        }
        f(i+1, cw); // 选择不装第i个物品
        if (cw + weight[i] <= w) {
            f(i+1, cw + weight[i]); // 选择装第i个物品
        }
    }

规律是不是不好找?那我们就举个例子、画个图看看。我们假设背包的最大承重是 9。我们有 5 个不同的物品,每个物品的重量分别是 2,2,4,6,3。如果我们把这个例子的回溯求解过程,用递归树画出来,就是下面这个样子:

在这里插入图片描述

递归树种的每个结点表示一种状态,我们用 (i, cw) 来表示。其中,i 表示将要决策第几个物品是否装入背包,cw 表示当前背包中物品的总重量。比如,(2, 2) 表示我们将要决策第 2 个物品是否装入背包,在决策前,背包中的总总量是 2。

从递归树中,你应该会发现,有些子问题的求解是重复的,比如 f(2, 2) 和 f(3,4) 都被重复计算了两次。我们可以借助递归那一节将的 “备忘录” 的解决方式,记录已经计算好的 f(i,cw),当再次计算到重复的 f(i,cw) 时,可以直接从备忘录中取出来用,就不用再递归计算了,这样就可以比避免冗余计算。

    // 回溯算法实现
    private int maxW = Integer.MIN_VALUE; // 结果放到 maxW 中
    private int[] weight = {2,2,4,6,3}; // 物品重量
    private int n = 5; // 物品个数
    private int w = 9; // 背包承受的最大重量
    private boolean[][] mem = new boolean[5][10]; //备忘录,默认为false
    public void f(int i, int cw) { // 调用f(0,0)
        if (cw == w || i == n) { // cw == w表示装满了;i==n表示物品都考察完了
            if (cw > maxW) maxW = cw;
            return;
        }
        if (mem[i][cw]) return; // 重复状态
        mem[i][cw] = true;
        f(i+1, cw); // 选择不装第i个物品
        if (cw + weight[i] <= w) {
            f(i+1, cw + weight[i]); // 选择装第i个物品
        }
    }

这种解决方法非常好。实际上,它已经跟动态规划的执行效率基本上没有差别。但是,多一种方法就多一种解决思路,我们现在来看看动态规划是怎么做的。

我们把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个物品的决策(放入或不放入背包)完之后,背包中的重量会有多种情况,也就是说,会达到多种不同的状态,对应到递归树中,就是有很多不同的节点。

我们把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。我们可以通过合并每一层的重复状态,这样就保证每一层不同状态的个数斗不过超过 w 个(w 表示背包的承载重量),也就是例子中的 9。于是,我们就成功避免了每层状态个数的指数级增长。

我们用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。

第 0 个(下标从 0 开始编号)物品的重量是 2,要么装入背包,要么不装入背包,决策完之后,会对应背包的两种状态,背包中物品的总重量是 0 或者 2。我们用 state[0][0]=truestate[0][2]=true 来表示这两种状态。

第 1 个物品的重量是 2,基于之前的背包状态,在这个物品决策完之后,不同的状态有 3 个,背包中物品总重量分别是 0(0+0),2(0+2 or 2+0),4(2+2)。我们用 state[1][0]=true, state[1][2]=true, state[1][4]=true 来表示这三种状态。

依次类推,直到考察完所有的物品后,整个 states 状态数组就计算好了。我把整个计算的过程画了出来,你可以看看。图中 0 表示 false,1 表示 true。我们只需要在最后一层,找一个职位 true 的最近接 w(这里是 9)的值,就是背包中物品总重量的最大值。

在这里插入图片描述
在这里插入图片描述

文字描述可能不够清楚。我把上面的过程,翻译成代码,你可以结合着一款看下。

    // weight:物品重量,n:物品个数,w:背包重量
    public int knapsack(int[] weight, int n, int w) {
        boolean[][] states = new boolean[n][w+1]; // 默认值fasle
        states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
        if (weight[0] <= w) {
            states[0][weight[0]] = true;
        }
        for (int i = 1; i < n; i++) { // 动态规划状态转移
            for (int j = 0; j <= w; j++) { // 不把第i个物品放入背包
                if (states[i-1][j] == true) states[i][j] = true;
            }
            for (int j = 0; j <= w - weight[i]; j++) { // 把第i个物品放入背包
                if (states[i-1][j] == true) states[i][j+weight[i]] = true;
            }
        }
        for (int i = w; i >= 0; i--) { // 输出结果
            if (states[n-1][i] == true) return i;
        }
        return 0;
    }

实际上,这就是一种用动态规划解决问题的思路。我们把问题分解为多个阶段,每个阶段对应一个决策。我们记录每一个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,来推导下一阶段的状态集合,动态地往前推进。这也是动态规划这个名字的由来,你可以自己体会一下,是不是还挺形象的?

前面我们讲到,用回溯算法解决这个问题的时间复杂度是 O ( 2 n ) O(2^n) O(2n),是指数级的。那动态规划解决方案的时间复杂度是多少呢?

这个代码的时间复杂度非常好分析,耗时最多的部分就是代码中的两层 for 循环,所以时间复杂度是 O ( n ∗ w ) O(n*w) O(nw)。n 表示物品个数,w 表示背包可以承受的总重量。

从理论上讲,指数级的时间复杂度肯定要比 O ( n ∗ w ) O(n*w) O(nw) 高很多。为了让你有更加深刻的感受,我来举个例子给你比较一下。

我们假设有 10000 个物品,重量分布在 1 到 15000 之间,背包可以承载的总重量是 30000。如果我们用回溯法解决,用具体的数值表示出时间复杂度是 2 10000 2^{10000} 210000,这是一个相当大的数字。如果我们用动态规划解决,用具体的数值表示出时间复杂度,就是 10000*30000。虽然看起来也很大,但是和 2 10000 2^{10000} 210000 比起来,要小太多。

尽管动态规划的执行效率比较高,但是就刚刚的代码来说,我们需要额外申请一个 n 乘以 m+1 的二维数组,对空间的消耗比较多。所以,有时候,我们会说,动态规划是一种空间换时间的解决思路。你可能要问了,有什么办法可以降低空间消耗吗?

实际上,我们只需要一个大小为 w+1 的一维数组就可以解决这个问题。动态规划状态转移的过程都可以基于一个一维数组来操作。具体的代码实现如下所示,你可以仔细看下。

    public int knapsack2(int[] weight, int n, int w) {
        boolean[] states = new boolean[w+1]; // 默认值fasle
        states[0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
        if (weight[0] <= w) {
            states[weight[0]] = true;
        }
        for (int i = 1; i < n; i++) { // 动态规划状态转移
            // 不把第i个物品放入背包与i-1的结果一致,所以不用再处理
            for (int j = w - weight[i]; j >= 0; j--) { // 把第i个物品放入背包
                if (states[j] == true) states[j+weight[i]] = true;
            }
        }
        for (int j = w; j >= 0; j--) { // 输出结果
            if (states[j] == true) return j;
        }
        return 0;
    }

我强调一下代码中的第 9 行,j 需要从大到小来处理。如果我们按照 j 从小到大处理的话,会出现 for 循环重复计算的问题。

0-1 背包问题升级版

我们继续升级难度。我改造了一下刚刚的背包问题。你看下这个问题又该如何用动态规划解决呢?

刚刚讲的背包问题,只涉及背包重量和物品重量。我们现在引入物品价值这一变量。对于一组不同重量、不同价值、不可分割的物品,我们选择将某些物品放入背包,在满足背包最大重量限制的前提下,背包中装入物品的总结之最大是多少呢?

这个问题依旧可以用回溯算法来解决。这个问题并不复杂,所以具体的实现思路,就不用文字描述了,直接给你看代码。

    // 回溯算法实现
    private int maxV = Integer.MIN_VALUE; // 结果放到 maxW 中
    private int[] weight = {2,2,4,6,3}; // 物品重量
    private int[] value = {3,4,8,9,6}; // 物品价值
    private int n = 5; // 物品个数
    private int w = 9; // 背包承受的最大重量
    public void f(int i, int cw, int cv) { // 调用f(0,0,0)
        if (cw == w || i == n) { // cw == w表示装满了;i==n表示物品都考察完了
            if (cw > maxV) maxV = cw;
            return;
        }
        f(i+1, cw, cv); // 选择不装第i个物品
        if (cw + weight[i] <= w) {
            f(i+1, cw + weight[i], cv + value[i]); // 选择装第i个物品
        }
    }

针对上面的代码,我们还是照例画出递归树。在递归树中,每个节点表示一个状态。现在我们需要 3 个变量 (i,cw,cv) 来表示一个状态。其中,i 表示即将要决策的第 i 个物品是否装入背包,cw 表示当前背包中物品的总重量,cv 表示当前背包中物品的总价值。

在这里插入图片描述

我们发现,在递归树中,有几个节点 i 和 cw 是相同的,比如 f(2,2,4) 和 f(2,2,3)。在背包中物品总重量是一样的情况下,f(2,2,4) 这种状态对应的物品总价值更大,我们可以舍弃 f(2,2,3) 这种状态,只需要沿着 f(2,2,4) 这条决策路线继续往下决策就可以。

也就是说,对于 (i,cw) 相同的不同状态,我们只需要保留 cv 值最大的那个,继续递归处理,其他状态不予考虑。

思路说完了,但是代码如何实现呢?如果用回溯算法,这个问题就没法再用 “备忘录” 解决了。所以,我们就需要换种思路,看看动态规划是不是更容易地解决这个问题?

我们还是把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就会达到多种不同的状态。

我们用一个二维数组 states[n][w+1],来记录每层可达到的不同状态。不过这里数组存储的值不再试 boolean,而是当前状态对应地最大总价值。我们把每一层中 (i,cw) 重复的状态(节点)合并,只记录 cv 值最大的那个状态,然后基于这些状态来推导下一层的状态。

我们把这个动态规划的过程翻译成代码,就是下面这个样子。

    // weight:物品重量,value:物品价值,n:物品个数,w:背包重量
    public int knapsack3(int[] weight, int[] value, int n, int w) {
        int[][] states = new int[n][w+1];
        for (int i = 1; i < n; i++) { // 初始化states
            for (int j = 0; j <= w; j++) {
                states[i][j] = -1;
            }
        }
        states[0][0] = 0; // 第一行的数据要特殊处理,可以利用哨兵优化
        if (weight[0] <= w) {
            states[0][weight[0]] = value[0];
        }

        for (int i = 1; i < n; i++) { // 动态规划状态转移
            for (int j = 0; j <= w; j++) { // 不把第i个物品放入背包
                if (states[i-1][j] >= 0) states[i][j] = states[i-1][j];
            }
            for (int j = 0; j <= w - weight[i]; j++) { // 把第i个物品放入背包
                if (states[i-1][j] >= 0) {
                    int v = states[i-1][j] + value[i];
                    if (v > states[i][j + weight[i]]) {
                        states[i][j + weight[i]] = v;
                    }
                }
            }
        }
        // 找出最大值
        int maxValue = -1;
        for (int j = w; j >= 0; j--) { // 输出结果
            if (states[n-1][j] > maxValue) maxValue = states[n-1][j];
        }
        return maxValue;
    }

关于这个问题的时间、空间复杂度的分析,跟上一个例子大同小异,所以就不赘述了。我直接给出答案,时间复杂度是 O ( n ∗ w ) O(n*w) O(nw),空间复杂度也是 O ( n ∗ w ) O(n*w) O(nw)。跟上一个例子类似,空间复杂度也是可以优化的,你可以自己写一下。

如何巧妙解决“双十一”购物时的凑单问题?

对于这个问题,你当然可以利用回溯算法,穷举所有的排列组合,看大于等于 200 并且最接近 200 的组合是哪一个?但是,这样效率太低了点,时间复杂度非常高,是指数级的。当 n 很大时,可能 “双十一” 已经结束了,你的代码还没有运行处结果,这显然会让你在女朋友心中的形象大大减分。

实际上,它跟第一个例子中讲的 0-1 背包问题很像,只不过是把 “重量” 换成了价格。购物车中有 n 个商品。我们针对每个商品都决策是否否买。每次决策之后,对应不同的状态集合。我们还是用一个二维数组 states[n][x],来记录每次决策之后所有可达的状态。不过,这里的 x 值是多少呢?

0-1 背包问题中,我们找的是小于等于 w 的最大值,x 就是背包的最大承载重量 w+1。对于这个问题来说,我们要找的是大于等于 200(满减条件)的值中最小的,所以就不能设置 200 加 1了。就这个实际的问题而言,如果要购买的物品的总价格超过 200 太多,比如 1000,那这个羊毛 “薅” 得就没有太大意义了。所以,我们可以限定 x 值为 1001。

不过这个问题不仅要求大于等于 200 的总价格中的最小的,我们还要找出这个最小总价对应都该买哪些商品。实际上,我们可以利用 states 数组,倒推出这个被选择的商品序列。我先把代码写出来,待会再照着代码给你解释。

    // items:商品价格;n:商品个数;w:满减条件,比如200
    public void double11advance(int[] items, int n, int w) {
        boolean[][] states = new boolean[n][3*w+1]; // 超过3被就没有薅羊毛的价值了
        states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
        if (items[0] <= 3*w) {
            states[0][items[0]] = true;
        }
        for (int i = 1; i < n; i++) { // 动态规划状态转移
            for (int j = 0; j < 3*w; j++) { // 不买第i个商品
                if (states[i-1][j] == true) states[i][j] = states[i-1][j];
            }
            for (int j = 3*w - items[i]; j >= 0; j--) { // 买第i个商品
                if (states[i-1][j] == true) states[i][j+items[i]] = true;
            }
        }
        int j;
        for (j = w; j < 3*w+1; j++) {
            if (states[n-1][j] == true) break; // 输出结果大于等于w的最小值
        }
        if (j == 3*w+1) return; // 没有可行解
        for (int i = n-1; i >= 0; i--) {
            if (j - items[i] >= 0 && states[i-1][j-items[i]] == true) {
                System.out.print(items[i] + " "); // 购买这个商品
                j = j - items[i];
            } // else 没有购买这个商品,j不变
        }
        if (j != 0) System.out.print(items[0]);
    }

代码的前半部分跟 0-1 背包问题没有什么不同,我们着重看下后半部分,看它是如何打印出选择购买哪些商品的。

状态 (i,j) 只有可能从 (i-1,j) 或者 (i-1, j-items[i]) 这两个状态推到过来。所以,我们就检查这两个状态是否是可达的,也就是 states[i-1][j]states[i-1][j-items[i]] 是否为 true。

如果 states[i-1][j] 可达,就说明我们没有购买第 i 个上篇,如果 states[i-1][j-items[i]] 可达,那就说明我们选择购买了第 i 个商品。我们从中选择一个可达的状态(如果两个都可达,就随意选择一个),然后,继续迭代地考察其他商品是否有选择购买。

小结

动态规划的第一章到此就讲完了。内容比较多,你可能要多花一点时间来消化。

本章的内容不涉及动态规划的理论,我通过两个例子,给你展示了动态规划是如何解决问题的,并且一点一点详细给你讲解了动态规划解决问题的思路。这两个例子都是非常经典的动态规划问题,只要你真正搞懂了这两个问题,基本上动态规划就已经入门一半了。所以,你要多花点时间,真正弄懂这两个问题。

从例子中,你应该能发现,大部分动态规划能解决的问题,都可以通过回溯法来解决,只不过回溯算法解决起来比较低效,时间复杂度是指数级的。动态规划算法,在执行效率方面,要高很多。尽管执行效率提高了,但是动态规划的空间复杂度也提高了,所以,很多时候,我们会说,动态规划是一种空间换时间的算法思想。

前面也说了,本章的内容不涉及理论知识。这两个例子的分析过程,我并没有涉及任何高深的理论方面的东西。而且,我个人觉得,贪心、分治、回溯、动态规划,这四个算法有关的理论知识,大部分都是 “后验性” 的,也就是说,在解决问题的过程中,我们往往是先想到如何用某个算法思想解决问题,然后才用算法理论知识,去验证这个算法思想解决问题的正确性。所以,你大可不必过于急于寻求动态规划的理论知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/735038.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

汉语拼音字母表 (声母表和韵母表)

汉语拼音字母表 [声母表和韵母表] 1. 汉语拼音声母表2. 汉语拼音韵母表References 1. 汉语拼音声母表 声母是韵母前的辅音&#xff0c;与韵母一起构成一个完整的音节。 辅音是发声时&#xff0c;气流在口腔中受到各种阻碍所产生的声音&#xff0c;发音的过程即是气流受阻和克…

Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!

代码仓库 会同步代码到 GitHub https://github.com/turbo-duck/flink-demo 当前章节 继续上一节的内容&#xff1a;https://blog.csdn.net/w776341482/article/details/139875037 上一节中&#xff0c;我们需要使用 nc 或者 telnet 等工具来模拟 Socket 流。这节我们写一个 …

【python】linux下安装chromedriver

首先&#xff0c;安装selenium模块 pip3 install selenium查看系统内chrome版本&#xff1a; google-chrome --version 根据谷歌浏览器版本下载对应的浏览器驱动版本&#xff1a; wget https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.114/linux64/ch…

2024年6月大众点评成都餐饮店铺POI分析22万家

2024年6月大众点评成都餐饮店铺POI共有221002家 店铺POI点位示例&#xff1a; 店铺id CACuqlcUQApLA7Ki 店铺名称 峨眉山豆腐脑(百吉街店) 十分制服务评分 7.3 十分制环境评分 7.5 十分制划算评分 7.1 人均价格 18 评价数量 38 店铺地址 百吉街86号1层 大类 美食 中类…

Day7 —— 大数据技术之Hive

Hive快速入门系列 Hive的概述什么是Hive&#xff1f;使用Hive的原因 Hive架构Hive安装Hive配置文件修改启动Hive以命令行方式启动&#xff08;在$HIVE_HOME/bin目录下&#xff09;以JDBC连接启动&#xff08;beeline方式连接&#xff09; Hive基本操作Hive数据库操作Hive表操作…

天气冷电脑不能启动找不到硬盘

https://diy.zol.com.cn/2004/0611/101994.shtml

为什么 JakeWharton 建议:App 只要用到一个 Activity ?

我们来看看这条回答都提到了哪些内容&#xff0c;对 Activity 和 Fragment 之间的爱恨情仇有何独到的见解&#xff0c;凭什么能得到 JakeWharton 本尊的青睐有加。 因为 Activity 是一个程序入口。你可以将其视为 app 的一个 main 函数。站在用户的立场上&#xff0c;通常你进入…

ARM功耗管理软件之WFIWFE

安全之安全(security)博客目录导读 思考&#xff1a;功耗管理软件栈及示例&#xff1f;WFI&WFE&#xff1f;时钟&电源树&#xff1f;DVFS&AVS&#xff1f; ARM功耗管理精讲与实战汇总参见&#xff1a;Arm功耗管理精讲与实战

IO模型详解

阻塞IO模型 假设应用程序的进程发起IO调用&#xff0c;但是如果内核的数据还没准备好的话&#xff0c;那应用程序进程就一直在阻塞等待&#xff0c;一直等到内核数据准备好了&#xff0c;从内核拷贝到用户空间&#xff0c;才返回成功提示&#xff0c;此次IO操作&#xff0c;称…

OkHttp框架源码深度剖析【Android热门框架分析第一弹】

OkHttp介绍 OkHttp是当下Android使用最频繁的网络请求框架&#xff0c;由Square公司开源。Google在Android4.4以后开始将源码中的HttpURLConnection底层实现替换为OKHttp&#xff0c;同时现在流行的Retrofit框架底层同样是使用OKHttp的。 源码传送门 优点: 支持Http1、Http…

基于Java的农机电招平台系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果你对农机电招平台系统感兴趣或有相关开发需求&#xff0c;可以私信联系我。 开发语言 Java 数据库 MySQL 技术 B/S结构&#xff0c;SpringBoot框架 工具 Eclipse&#xff0c;Navicat&#xff0c;Tomcat8.0 系…

24年下半年各省自考报名时间汇总

24年下半年各省自考报名时间汇总

C语言 | Leetcode C语言题解之第174题地下城游戏

题目&#xff1a; 题解&#xff1a; int calculateMinimumHP(int** dungeon, int dungeonSize, int* dungeonColSize) {int n dungeonSize, m dungeonColSize[0];int dp[n 1][m 1];memset(dp, 0x3f, sizeof(dp));dp[n][m - 1] dp[n - 1][m] 1;for (int i n - 1; i >…

利用JAVA语言调用GLM-4接口实战指南

一、什么是API接口 API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;是一种软件接口&#xff0c;它定义了不同应用程序之间如何相互通信、交互。API接口分为很多种&#xff0c;常见的有Web API&#xff0c;数据库API&#xff0c;操…

【非常实验】如何在移动设备上运行 Docker?

本章就从在 DevOps 中最基本但也是最强大的工具 Docker 开始。最近,我在尝试更多Termux的可能性,于是就想着试试Docker适不适合arm架构。 我用的是天玑9000芯片,而不是高通,所以显示不出来 Qualcomm。所以我决定从在手机上运行 docker 开始,但这可能吗?让我们一起来看看吧…

高性能并行计算华为云实验三:蒙特卡罗算法实验

目录 一、实验目的 二、实验说明 三、实验过程 3.1 创建蒙特卡罗算法源码 3.2 Makefile的创建与编译 3.3 主机文件配置与运行监测​​​​​​​ 四、实验结果与分析 4.1 原教程对应的实验结果 4.2 改进后的实验结果 五、实验思考与总结 5.1 实验思考 5.2 实验总结…

从零实现GPT【1】——BPE

文章目录 Embedding 的原理训练特殊 token 处理和保存编码解码完整代码 BPE&#xff0c;字节对编码 Embedding 的原理 简单来说就是查表 # 解释embedding from torch.nn import Embedding import torch# 标准的正态分布初始化 也可以用均匀分布初始化 emb Embedding(10, 32) …

探索Agent AI智能体的未来

随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;Agent AI智能体正成为一种改变世界的新力量。这些智能体不仅在当前的技术领域中发挥着重要作用&#xff0c;而且在未来将以更深远的影响改变我们的生活、工作和社会结构。本文将探讨Agent AI智能体的现状、潜…

回顾今年的618大战:除了卷低价,还有别的出路吗?

今年的618刚刚落下帷幕&#xff0c;大促期间&#xff0c;一些电商平台纷纷备足马力、迎接挑战&#xff0c;反倒是一向领跑的淘宝京东公开表示&#xff0c;今年取消了618预售制。 互联网电商20年来&#xff0c;每年618、双11轮流登场&#xff0c;“低价大战”愈演愈烈&#xff0…

【C++】类和对象2.0

俺来写笔记了&#xff0c;哈哈哈&#xff0c;浅浅介绍类和对象的知识点&#xff01; 1.类的6个默认成员函数 俺们定义一个空类&#xff1a; class N {}; 似乎这个类N里面什么都没有&#xff0c;其实不是这样子的。这个空类有6个默认的成员函数 。 默认成员函数&#xff1a…