动态规划(一) —— 从背包系列问题看DP

news2024/12/24 8:47:17

前言

        动态规划可以算是算法初学者的噩梦哈哈,这段时间荔枝在持续学习Java后端的同时也没有忘记刷题嘿嘿嘿,总算把代码随想录上给出的有关动态规划的题目刷完了。接下来的几篇文章荔枝将会对于刷过的动态规划问题做出总结并给出相应的个人体会和理解。在本篇文章中荔枝首先来总结一下动态规划中的背包问题,主要包括:0-1背包问题、完全背包问题和多重背包问题,希望能给正在学习的小伙伴们带来一些帮助~~~


文章目录

前言

一、背包问题

二、0-1背包问题

2.1 dp数组的定义及其推导式

2.1.1 二维数组

2.1.2 滚动数组

2.2 0-1背包的经典例题

2.2.1 Leecode416.分割等和子集

2.2.2 Leecode494.目标和

2.2.3 Leecode474.一和零

三、完全背包问题

3.1 两种遍历顺序示例 

3.2 完全背包问题典例

3.2.1 Leecode518.零钱兑换||

3.2.2 Leecode组合总和IV

3.2.3 Leecode70.爬楼梯 

3.2.4 Leecode322.零钱兑换

3.2.5 Leecode139.单词拆分

四、多重背包问题

总结


一、背包问题

        背包问题,顾名思义就是拿背包来装物品,物品的数量可以是一个也可以是无数个,物品的种类可以是一种也可以是多种。背包有一个具体的最大容量v,而每一个物品都有其对应的价值value和体积weight,题目常常要求我们求出背包能装下物品的最大价值或者求解装满这个背包一共有多少种方法。其实很多动态规划的问题都可以转化成求解背包问题,因此学好背包问题尤为重要。在面试中常考的背包问题主要有三类:0-1背包问题、完全背包问题和多重背包问题,具体题目要求区别如下:

  • 0-1背包问题:给定物品的价值数组和体积数组,并且所有种类物品的数量只有一个
  • 完全背包问题:给定物品的价值数组和体积数组,所有种类的物品数量不限
  • 多重背包问题:给定物品的价值数组和体积数组,不同种类的物品数量也不相同

从上面为我们可以看出其实这三种背包问题最大的区别就是在于物品的种类和数量上的差异,不同的条件对于我们dp递推公式的推导和遍历顺序都是不一样的。很多题目最难的就是确定dp数组的含义,dp推导式如何推导以及初始化的值如何设定,这些我们需要拿一些题目来体会一下这个确定的过程。


二、0-1背包问题

        首先我们来看看0-1背包问题,在前面荔枝已经写出三种背包问题的区别——物品的数量和种类。标准的0-1背包问题是这么描述的:有n件物品和一个最多能背重量为w的背包,第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。接下来我们主要从dp数组的定义方式、遍历顺序和几道经典的0-1背包问题来掌握解题的思路和方法。

2.1 dp数组的定义及其推导式

        对于0-1背包问题求解时的dp数组我们一般有两种定义方式:二维数组和滚动数组。在实际的刷题中我们习惯使用滚动数组的定义方式,因为写起来代码更为简便~~~下面我们接着上文中的标准0-1背包问题来写出对应的dp数组的定义及其推导式。

2.1.1 二维数组

dp[i][j]表示从下标为[0-i]的物品里任意取物品,放进容量为j的背包的最大价值总和。 

//相应的dp推导式如下:
dp[i][j] = max(dp[i-1][j-weight[i]]+value[i],dp[i-1][j]);
//max中逗号左右两边分别代表取到i物品和不取i物品

得出相应的dp推导式后就需要将确定初始化条件了,当背包的容量为0时价值必然为0,因此dp[i][0]=0,但是dp[0][j]=value[0],具体的初始化过程如下

vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
//之所以要初始化dp数组的大小为bagweight+1是因为我们需要遍历到j==bagweight的情况

确定完初始化条件我们需要确定遍历顺序了,在二维数组中,先遍历数组和先遍历物品都可以,都是正序遍历。

for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    }
}

2.1.2 滚动数组

        滚动数组字面上有点抽象,其实滚动数组的方式是通过一个以为数组来实现上述二维数组的功能的,虽然不太好理解dp的含义但是却极大简洁了代码实现。简单来说其实滚动数组就是对每一维度的数组元素进行拷贝并将其数据进行覆盖,前面维度的数据都会加到当前维度上,以此来模拟二维数组的遍历过程。

dp[j]表示的含义是:装满容量为j的背包的最大价值总和。

//从二维数组转化到滚动数组其实很简单,只需要将i那个维度去掉就行
dp[j] = max(dp[j-weight[i]]+value[i],dp[j-1]);

那么滚动数组又应该如何来初始化呢?

首先我们确定了dp[i]的定义是容量为j的背包的最大价值总和,所以dp[0]自然就为0了。从递推公式中我们看出其余的dp数组元素都是由dp[0]推导出来的,所以索性都初始化为0。 

需要注意的是滚动数组的遍历顺序

在一维dp数组的遍历过程中,一定要先遍历物品再遍历背包,同时注意背包的遍历顺序是倒叙遍历的。倒叙遍历是为了每个物品只被添加过一次,这里之所以要先遍历物品再遍历背包是为了防止出现一个背包只放一个物品的情况。


vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

为什么在二维数组中正序和倒序遍历均可?

这是因为在一维滚动数组中我们每一层的数据跟上一层是有关系的,而在二维数组中前后两层数据是隔离开来的,因此这时候需要倒序遍历。

为什么先遍历物品再遍历背包?

这是因为一维数组在dp前后两层之间有联系,如果将遍历背包放在外层循环的话,由于是倒序遍历我们背包中存放就只有一个物品的价值了。

2.2 0-1背包的经典例题

2.2.1 Leecode416.分割等和子集

题目描述:

        给你一个只包含正整数非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

输入样例:

        nums = [1,5,11,5]

输出样例:

        true

        这道题目其实比较容易想到转化成背包问题,题目要求划分成等和的两个子集,那么其实就是拿整个集合容量的一半容量大小的背包,看看物品是否能够装满它。荔枝在前面的介绍中提到了0-1背包问题中是会给出物品的weight数组和value数组,这道题目其实就是将整个集合中的元素按照大小关系划分为两个等和子集,这里我们将物品的values和weight等价于同一数值既可以将原问题转化成01背包问题。需要注意的是dp[i]的含义:它指的是容量为i的背包中可以放的数字总和的最大值,只有当容量为sum/2时候,dp[sum/2]=sum/2时可以将原来的数组分割成相同的两个子集。那么dp递推公式其实就呼之欲出了,就是最典型的0-1背包问题的dp公式。

代码示例:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        vector<int> weight = nums;
        int sum = 0;
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
        }
        if(sum%2!=0) return false; //要想划分为等和子集就必须保证集合总和是一个偶数
        int bagweight = sum/2;
        vector<int> dp(10001,0);   //题目给出的数据范围来初始化,只用遍历到sum/2,而sum<20000
        for(int i=0;i<nums.size();i++){
            for(int j=bagweight;j>=nums[i];j--){
                dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        } 
        if (dp[bagweight] == bagweight) return true;
        return false;
    }
};

来源:力扣(LeetCode)
链接: ​​​​​​​https://leetcode.cn/problems/partition-equal-subset-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 

2.2.2 Leecode494.目标和

题目描述:

        给你一个整数数组 nums 和一个整数 target 。向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。返回可以通过上述方法构造的、运算结果等于 target 的不同表达式的数目。

输入样例:

        nums = [1,1,1,1,1], target = 3

输出样例:

        5

        对题目分析后我们发现其实这道题目跟前面的分割等和子集有点像,但是本题需要注意一下这几个关系:sum = left + right 、targer = left - right 。数学推导就可以得到left = (sum+target)/2,要满足这个关系,也就是要装满容量为left的背包一共有几种方法,这就转化成了一道0-1背包问题。接着确认dp[j]数组的含义:填满j(包括j)这么大容积的包,有dp[j]种方法。又因为要取到left这个值,所以初始化dp数组为vector<int> dp(left+1,0)。

代码示例:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int i=0;i<nums.size();i++){
             sum+=nums[i];
        }
        // 做一下简单的过滤
        if(abs(target) > sum) return 0;
        if((target+sum)%2 == 1) return 0;
        int left = (sum+target)/2;
        vector<int> dp(left+1,0);
        dp[0] = 1;
        for(int i=0;i<nums.size();i++){
            for(int j=left;j>=nums[i];j--){
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[left];
    }
};

这道题目重点需要理解的就是dp递推式如何推导,这里我们需要想一下要填满一个容量为j的背包,如果我的手上有1,那么这一部分的数据就转换为填满容量为j-1的背包。题目要求的dp[j]其实就是将给出的元素数组中的元素被j减去所得到的容量的背包被装满总共有几种方法。因此dp递推式就是:

dp[j] += dp[j-nums[i]]

 要时刻谨记dp[j]的含义,要不然做题做着做着就混乱了~~~

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/target-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

2.2.3 Leecode474.一和零

题目描述:

        给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的长度,该子集中 最多有 m 个 0 和 n 个 1 。如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的子集 。

输入样例:

        strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3

输出样例:

        4

 这道题目有点意思它给出了两个维度要求m、n。因此滚动数组的形式也需要随之相应变化。我们首先确定dp数组的含义dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。这道题也是在求解最长子集的长度,只是在0和1的数量上有约束罢了,递推公式其实可以对比一个维度的0-1背包问题来设置即可。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // 两个维度的01背包问题
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));
        for(string str:strs){
            int numZero = 0;
            int numOne = 0;
            for(char c:str){
                if(c=='0') numZero++;
                else numOne++;
            }
            for(int i=m;i>=numZero;i--){
                for(int j=n;j>=numOne;j--){
                    dp[i][j] = max(dp[i][j],dp[i-numZero][j-numOne]+1);
                }
            }
        }
        return dp[m][n];
    }
};

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/ones-and-zeroes
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


三、完全背包问题

        完全背包问题相比于0-1背包问题其实就是在遍历的顺序上做出了改变,这是由于背包问题的场景发生了改变而导致的。对于完全背包问题由于物品的数量是无限的,这时候就需要我们在背包的遍历顺序中修正一下,在前面讲滚动数组的时候我们为了保证背包中取得物品无重复而在遍历背包的时候采用逆序遍历,这时候就相应改成正序遍历即可。

由于遍历顺序的改变其实滚动数组的内外层for循环的先后遍历物品还是背包是有讲究的:对于排列问题就需要先遍历背包再遍历物品;对于组合问题就需要先遍历物品再遍历背包

3.1 两种遍历顺序示例 

先遍历物品再遍历背包

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

先遍历背包再遍历物品

for(int j = 0; j <= bagWeight; j++) { // 遍历背包
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
    cout << endl;
}

3.2 完全背包问题典例

3.2.1 Leecode518.零钱兑换||

题目描述:

        给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个,题目数据保证结果符合 32 位带符号整数。

输入样例:

        amount = 5, coins = [1, 2, 5]

输出样例:

        4

根据题意我们首先设定dp数组的含义:凑成金额为i的组合数。体会一下这道题目其实就是再求装满背包的方法有几种,其实跟上面目标和的dp递推公式是一样的。

dp[j] += dp[j - nums[i]];

那么如何初始化呢?其实我们从常识来推论的话,amount=0时候的组合数应该为1,所以初始化dp[0] = 1;很明显这道题目是一道组合问题,那就确定下来是先遍历物品再遍历背包,至于为什么是这样子的遍历顺序其实荔枝感觉得自己假设一个用例下来手推一下dp过程就知道了。

代码示例:

//dp[i]:可以组成i的组合数。这里需要比较清晰地认知dp[i]的意义
//dp的递推公式:dp[j] += dp[j-coins[i]]
//初始化dp[0] = 1
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1,0);
        dp[0] = 1;
        for(int i=0;i<coins.size();i++){
            for(int j=coins[i];j<=amount;j++){
                dp[j]+=dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/coin-change-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

3.2.2 Leecode组合总和IV

题目描述:

        给你一个由不同整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数,题目数据保证答案符合 32 位整数范围。

输入用例:    

        nums = [1,2,3], target = 4

输出用例:

        7

解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

        这道题目其实很简单,跟上述的零钱兑换||的思路是一样的,都是求装满背包有几种方法的类型,区别在于这道题目再输出样例中特别注明了顺序不同的序列视为不同组合,也就代表着这道题目是一个排列问题,这样子仅需要改变一下遍历的内外for循环的顺序即可。 

代码示例:

//这道题目跟前一道问题零钱兑换||的区别就在于这是一个排列问题,求的是将背包填满之后的一个排列数
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target+1,0);
        dp[0] = 1;
        for(int j=0;j<=target;j++){
            for(int i=0;i<nums.size();i++){
                if (j - nums[i] >= 0 && dp[j] < INT_MAX - dp[j - nums[i]]){
                    dp[j] += dp[j-nums[i]];
                }
            }
        }
        return dp[target];
    }
};

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/combination-sum-iv
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

3.2.3 Leecode70.爬楼梯 

        爬楼梯问题其实是刚开始学动态规划最经典的题目了,但一开始我们大多采用的是使用斐波那契数列的推导式子来推导dp递推式子,而在这里荔枝会给出如何用完全背包的思路来解决这道题目。

题目描述:

        假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

输入样例:

        n = 2
输出样例:

        2

爬楼梯如何转化成完全背包问题呢?其实我们可以将要爬的阶数v视作容量为v的背包,每一次爬其实就是在将一个价值和体积都为1或2的物品放入背包中,求的是装满背包的排列数。 

代码示例:

class Solution {
public:
    int climbStairs(int n) {
        // 使用完全背包问题的求解方法来求解
        vector<int> dp(n+1,0);
        dp[0] = 1;
        for(int j = 0;j<=n;j++){
            for(int i=1;i<=2;i++){
                if(j>=i){
                    dp[j]+=dp[j-i];
                }
            }
        }
        return dp[n];
    }
};

// 简单的斐波那契数列推导
// class Solution {
// public:
//     int climbStairs(int n) {
//         if(n<=1) return n;
//         vector<int> dp(n+1);
//         dp[1] = 1;
//         dp[2] = 2;
//         for(int i=3;i<=n;i++){
//             dp[i] = dp[i-1]+dp[i-2];
//         }
//         return dp[n];
//     }
// };

来源:力扣(LeetCode)
链接:​​​​​​​https://leetcode.cn/problems/climbing-stairs/
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

3.2.4 Leecode322.零钱兑换

题目描述:

        给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 ,你可以认为每种硬币的数量是无限的。

输入样例:

        coins = [1, 2, 5], amount = 11

输出样例:
        3

        首先我们需要弄清楚dp数组的含义dp[j]:凑足总额为j所需钱币的最少个数为dp[j]。由于题目要求的是求凑成目标金额的最少硬币数量,所以dp递推式需要修改成:dp[j] = min(dp[j - coins[i]] + 1, dp[j]),需要注意的是一个思维:这里通过转换我们已经将硬币面额作为了填满背包的weight,而其对于dp数组的value则是硬币数量,因此在递推公式中才是一个+1的操作。还有需要注意的是初始化的时候,由于我们求的是凑成目标面额的最少硬币数量,所以需要找一个最大值INT_MAX来初始化避免覆盖后面的dp递推值。

代码示例:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1,INT_MAX);
        dp[0] = 0;
        for(int i=0;i<coins.size();i++){
            for(int j=coins[i];j<=amount;j++){
                if (dp[j - coins[i]] != INT_MAX) { //判断是能够凑出题目要求的数
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/coin-change
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

3.2.5 Leecode139.单词拆分

题目描述:

        给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

输入示例:

        s = "leetcode", wordDict = ["leet", "code"]

输出示例:

        true

        这道题目难点其实在于:对于STL库不太熟悉或者对于字符处理的题目不太熟悉的同学会不清楚使用unordered_set来处理字符。首先确定dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

代码示例:

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(),wordDict.end());
        vector<bool> dp(s.size()+1,false);
        dp[0] = true;
        for(int i=1;i<=s.size();i++){  //遍历背包
            for(int j=0;j<i;j++){      //遍历物品
                string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
                if (wordSet.find(word) != wordSet.end() && dp[j]) {
                    dp[i] = true;
                }
            }
        }
        return dp[s.size()];
    }
};

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/word-break
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


四、多重背包问题

        荔枝在前面有提及多重背包问题最主要的特征就是有多个种类的物品且数量不同,有一种常见的处理思路就是将多重背包问题中的所有物品不按类分而是一一摊开,这样就把一个多重背包问题转化成一个0-1背包问题。Carl哥给出了具体的模拟过程:

void test_multi_pack() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    vector<int> nums = {2, 3, 2};
    int bagWeight = 10;
    for (int i = 0; i < nums.size(); i++) {
        while (nums[i] > 1) { // nums[i]保留到1,把其他物品都展开
            weight.push_back(weight[i]);
            value.push_back(value[i]);
            nums[i]--;
        }
    }

    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
        for (int j = 0; j <= bagWeight; j++) {
            cout << dp[j] << " ";
        }
        cout << endl;
    }
    cout << dp[bagWeight] << endl;

}
int main() {
    test_multi_pack();
}

总结

        在这篇文章中,荔枝主要侧重讲了0-1背包问题和完全背包问题以及相应的变式和应用。其实一整个流程刷下来发现其实这些题目都是遵循着某个规律。其中最主要的就是要谨记dp数组的含义以及如何取递推,同样的遍历顺序和初始化有时候也很难抉择。这其实就是动态规划的难点。写了三个钟总算整理好了背包系列,荔枝整理完了感觉确实对于背包问题的思路更加清晰了,荔枝也希望在上面的题目中的解析能够帮助到正在学习的小伙伴~~~

今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

compose之沉浸式(侵入式)状态栏(隐藏状态栏)

沉浸式(侵入式)状态栏 效果图&#xff1a; 1、代码加入&#xff1a;WindowCompat.setDecorFitsSystemWindows(window, false) ComposeTestTheme {WindowCompat.setDecorFitsSystemWindows(window, false)Greeting("Android")} 2、沉浸式(侵入式)主题&#xff1a; …

消息推送(websocket)集群化解决方案

目录 需求分析解决方案实现步骤架构图配置websocket请求地址配置websocket连接前置和连接关闭监听配置websocket处理程序配置redis交换机配置redis订阅监听配置redis发布监听需求分析 及时信息传递:消息推送功能能够确保网站向用户发送及时的重要信息,包括新闻更新、促销活动…

消息队列——rabbitmq的不同工作模式

目录 Work queues 工作队列模式 Pub/Sub 订阅模式 Routing路由模式 Topics通配符模式 工作模式总结 Work queues 工作队列模式 C1和C2属于竞争关系&#xff0c;一个消息只有一个消费者可以取到。 代码部分只需要用两个消费者进程监听同一个队里即可。 两个消费者呈现竞争关…

Redis进阶底层原理-主从复制

Redis的主从节点都会记录对方的信息&#xff0c;核心还包括ReplicationID 和 offset &#xff0c; ReplicationID &#xff1a; 主从节点实例的ID &#xff0c;redis内部就是通过这个id去识别主从节点。offset&#xff1a;数据同步偏移量&#xff0c;也就是从节点每次从主节点同…

3.6 Bootstrap 导航元素

文章目录 Bootstrap 导航元素表格导航或标签胶囊式的导航菜单基本的胶囊式导航菜单垂直的胶囊式导航菜单 两端对齐的导航禁用链接下拉菜单带有下拉菜单的标签带有下拉菜单的胶囊标签页与胶囊式标签页 Bootstrap 导航元素 本文将讲解 Bootstrap 提供的用于定义导航元素的一些选项…

使用thrift编写C++服务器、客户端

在上一节《Linux 下编译 thrift》中&#xff0c;我们成功编译出了thrift的库文件&#xff0c;本节我们来编写thrift的C服务器&#xff0c;客户端。 官网 https://thrift.apache.org/tutorial/cpp.html 有thrift的C例子。在我们之前下载下来的thrift 源码根目录的tutorial/cpp目…

MySQL高级管理

目录 一、指定主键的一种方式 1.1高级操作 1.2数据表高级操作,克隆表 1.2.1 克隆表名 1.2.2备份表内容 1.3复制表 1.4删除指令 方法一&#xff1a; 方法二&#xff1a; 删除速度 二、创建临时表 三、MySQL中6种常见的约束 3.1创建主表 3.2创建从表 3.3为主表test01添加…

[Docker异常篇]解决Linux[文件异常]导致开机Docker服务无法启动

文章目录 一&#xff1a;场景复现二&#xff1a;解决思路2.1&#xff1a; 对比其他节点docker配置2.2&#xff1a;试着修改为正常节点配置2.2&#xff1a;根据上面异常显示&#xff0c;不一定是配置不对&#xff0c;可能是文件系统有损坏 三&#xff1a;解决 -> 执行命令 mo…

【机器学习算法】奇异值分解(SVD)

文章目录 奇异值分解(SVD)1.理论部分1.1特征分解(ED)1.2 奇异值分解(SVD)求解U和V求解Σ 2.应用部分2.1图像压缩2.2图像数据集成分分析2.3 数据降维(PCA的一种解法) Reference 奇异值分解(SVD) 奇异值分解(Singular Value Decomposition) 是矩阵低秩分解的一种方法&#xff0c;…

太猛了!Web安全漏洞批量扫描框架

关注【Hack分享吧】公众号&#xff0c;回复关键字【230528】获取下载链接 工具介绍 一个应用于web安全领域的漏洞批量扫描框架&#xff0c;可被应用于但不限于如下场景&#xff1a; 0Day/1Day全网概念验证(在没有测试环境(各种商业、闭源软件)或懒得搭建测试环境的情况下&…

D. Binary String Sorting

Problem - 1809D - Codeforces 思路&#xff1a;最后得到的结果就是前面是一串0后面是一串1&#xff0c;那么我们可以枚举分界点&#xff0c;如果枚举到i&#xff0c;那么就将1~i变为0&#xff0c;将i1变为1,我们发现如果一个1在1~i中&#xff0c;如果他是第i-1个&#xff0c;那…

Redis进阶底层原理-Cluster集群底层

Redis实现高可用的方案有很多中&#xff0c;先了解下高可用和分区的概念&#xff1a; 高可用是指系统在面对硬件故障、网络故障、软件错误等意外问题时&#xff0c;仍能给客户端提供正常的服务&#xff0c;尽量的减少服务的阻塞、终端现象。在高可用的方案中一般会采用冗余备份…

《洛谷深浅》第五章---数组与数据批量存储

文章目录 前言一、小鱼比可爱二、小鱼的数字游戏三、冰雹猜想四、校门外的树五、旗鼓相当的对手六、旗鼓相当的对手总结 前言 本节主要学习一维数组 和 多维数组 后边的知识我觉得 可以试着了解并不要求你掌握这么难的题目 因为ACM更多都是思维题目 所以这里把重要的题目掌握就…

【多线程系列-03】深入理解java中线程的生命周期,任务调度

多线程系列整体栏目 内容链接地址【一】深入理解进程、线程和CPU之间的关系https://blog.csdn.net/zhenghuishengq/article/details/131714191【二】java创建线程的方式到底有几种&#xff1f;(详解)https://blog.csdn.net/zhenghuishengq/article/details/127968166【三】深入…

基于树莓派实现的IO-Link 项目

IO-Link 协议 &#xff08;IEC 61131-9&#xff09; 是从传感器或执行器到 IO-Link 主站的串行半双工点对点连接。目前IO-Link 的硬应已经越来越普及。国外产品以巴鲁夫为代表。如何开发IO-link 产品&#xff1f;可以参考国外的一些开源项目。 国外有人开发了开发一个IO-Link主…

soundfile torchaudio 读取音频文件

soundfile 和 torchaudio 读取音频文件后的数据格式不同&#xff0c;前者是numpy&#xff0c;后者是tensor。前者读取后可以直接用于一些python的基础函数输入&#xff0c;后者用于pytorch的一些函数的应用。两者互换用途时候需要进行格式转换。 import soundfile as sf impor…

智能指针使用及详细解析

文章目录 智能指针概念为什么使用智能指针智能指针使用智能指针的常用函数get() 获取智能指针托管的指针地址.reset() 重置智能指针托管的内存地址&#xff0c;如果地址不一致&#xff0c;原来的会被析构掉 auto_ptrunique_ptrshared_ptr**shared_ptr的原理**引用计数的使用构造…

Gradle 构建工具 #5 又冲突了!如何理解依赖冲突与版本决议?

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。 Gradle 作为官方主推的构建系统&#xff0c;目前已经深度应用于 Android 的多个技术体系中&#xff0c;例如组件化开发、产物构建、单元测试等…

STM32(HAL库)驱动SHT30温湿度传感器通过串口进行打印

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 软件IIC引脚配置 2.3 串口外设配置 2.4 项目生成 3、KEIL端程序整合 3.1 串口重映射 3.2 SHT30驱动添加 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机通过HAL库…

Spring Batch之读数据库——JdbcCursorItemReader之自定义RowMapper(三十七)

一、自定义RowMapper 详情参考我的另一篇博客&#xff1a; Spring Batch之读数据库——JdbcCursorItemReader&#xff08;三十五&#xff09;_人……杰的博客-CSDN博客 二、项目实例 1.项目框架 2.代码实现 BatchMain.java: package com.xj.demo28;import org.springfram…