算法学习|动态规划

学习材料声明

所有知识点都来自互联网,进行总结和梳理,侵权必删。
引用来源:计算机算法与设计分析(第5版)|山景城一姐|力扣动态规划|灵茶山艾府|灵茶山艾府-最长公共子序列|包教包会~最长公共子序列|灵茶山艾府-买卖股票的最佳时机【基础算法精讲 21】
动态规划找到子状态之间的关系很重要!| LeetCode:96.不同的二叉搜索树|

学习目的

算法考试怕不及格,以及长久以来对算法的恐惧。
因为算法问题(数学一直是自己的噩梦),失去了很多信心。
想要找到好工作。

什么是动态规划?

代码随想录|动规五部曲

  1. dp数组以及下标的含义
  2. 递推公式
  3. dp数组如何初始化
  4. 遍历顺序
  5. 打印dp数组

面向问题

  1. 动态基础
  2. 背包问题
  3. 打家劫舍
  4. 股票问题
  5. 子序列问题

1.自己叨叨叨

DP数组!
最小子问题-》初始化!
赋值规则-》遍历方向(从山景城一姐那里悟出来的!)。
Q1:到目前为止解题思路是:我知道这题是动态规划—》用动态规划的几个步骤解题。那么我要怎么在不知情情况下也能选择动态规划呢?
Q2:习题1和2,分别使用了二维和一维的dp数组,那么怎么确定用几维的动态数组呢?
A2:是状态决定的嘛?

2.回溯

例题| 17. 电话号码的字母组合

从灵茶山艾府老师那里学习到的。(我目前水平听up讲解,只是听个热闹。。思维完全跟不上。。)

  1. 回溯三问:当前操作?子问题是什么?下一个子问题是什么?
    @
  2. dfs(i)的意思是考虑下标>=i的元素的情况。

3.0-1背包

物品数量为1,选与不选。
分为最大价值和多少种方法。
其中还有一种是求对半分的–》碰撞石头以及目标和。

4.完全背包问题(物品可以无限次使用)|学习参考《代码随想录》

4.1问最大价值?

截图来自代码随想录

4.2问多少种方法?(遍历顺序决定是组合数还是排列数)

11|518. 零钱兑换 II(组合数)
12377. 组合总和 Ⅳ(排列数)
截图来源于代码随想录(侵权必删)

5.多重背包

给定一个容量为V的背包,给N个物品信息,包括物品重量,价值和个数(!!),要求背包可装的最大容量。

5.1本质:0-1背包(代码随想录)

来自代码随想录

6.股票问题

来自代码随想录

这里涉及状态转移。

6.1状态转移机

7.子序列问题

7.1什么是子序列?

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

7.2解题思路

子集型回溯思路 :(1)选或者不选?(2)枚举选哪个?在这里插入图片描述

习题

1.动规基础

1.1|509. 斐波那契数

class Solution {
public:
    int fib(int n) {
        int dp[n+1];
        memset(dp, 0, sizeof(dp));
        if(n==0){
            return 0;
        }
        if(n==1){
            return 1;
        }
        dp[0] = 0;
        dp[1] = 1;
        for(int i=2; i<=n; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];

    }
};

1.2|70. 爬楼梯

这一题,经常在各种算法课堂是听到,自己也做过。但是总是忘记。每一次听人提起,都心虚不已。似乎是一题每个人都得知道的题目。是一题能够把我推离算法远远的题目。

class Solution {
public:
    int climbStairs(int n) {
        int dp[n+1];
        memset(dp, 0, sizeof(dp));
        if(n==0){
            return 0;
        }
        if(n==1){
            return 1;
        }
        if(n==2){
            return 2;
        }
        dp[1]=1;
        dp[2]=2;
        for(int i=3; i<=n; i++){
            dp[i] = dp[i-1]+dp[i-2];//到达第i阶只有两种方式,跨1上来的,或者跨2上来的。
        }
        return dp[n];
    }
};

这里拓展一个问题,一次可以上1-m个台阶,问有几种方法?

class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题
                if (i - j >= 0) dp[i] += dp[i - j];
            }
        }
        return dp[n];
    }
};

1.3|746. 使用最小花费爬楼梯

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        int dp[n+1];
        memset(dp, 0, sizeof(dp));
        for(int i=2; i<=n; i++){
            dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
        }
        for(int i=0; i<=n; i++){
            cout<<dp[i]<<" ";
        }
        return dp[n];
    }
};

1.4|62. 不同路径|用时12min

class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[m][n];
        memset(dp, 0, sizeof(dp));
        for(int i=0; i<m; i++){
            dp[i][0] = 1;
        }
        for(int i=0; i<n; i++){
            dp[0][i] = 1;
        }
        
        for(int i=1; i<n; i++){
            for(int j=1; j<m; j++){
                dp[j][i] = dp[j-1][i] + dp[j][i-1];
            }
        }
        return dp[m-1][n-1];
    }
};

非常基础的动态规划问题,初始化条件和更新公式都在题目鲜明地给出了。就是动态顺序记住行列顺序!双重循环,以后还是用r,c比较合适。

1.5|63. 不同路径 II

与上一题唯一的区别在于有障碍物的话,路线次数直接为0;

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int rnum = obstacleGrid.size();
        int cnum = obstacleGrid[0].size();
        cout<<rnum<<" "<<cnum<<endl;
        int dp[rnum+1][cnum+1];
        memset(dp, 0, sizeof(dp));
        for(int i=0; i<rnum; i++){
            if(obstacleGrid[i][0]==1){
                break;
            }
            dp[i][0]=1;
        }
        for(int i=0; i<cnum; i++){
            if(obstacleGrid[0][i]==1){
                break;
            }
            dp[0][i]=1;
        }
        for(int i=1; i<rnum; i++){
            for(int j=1; j<cnum; j++){
                if(obstacleGrid[i][j]==1){
                    dp[i][j]=0;
                }else{
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }               
            }
        }
        return dp[rnum-1][cnum-1];

    }
};

1.6|343. 整数拆分

确定dp数组就是最终答案,然后就是转换公式。

class Solution {
public:
    int integerBreak(int n) {
        int dp[n+1];
        for(int i=0; i<=n; i++){
            dp[i] = i-1;
        }
        for(int i=3; i<=n; i++){
            for(int j=2; j<=i-1; j++){
                dp[i] = max(dp[i], max(i-j, dp[i-j])*max(j, dp[j]));
            }
        }
        return dp[n];
    }
};

1.7|96. 不同的二叉搜索树

二刷了,很不错能够记住思路写出来。

拿到题目,感觉和不同路径那题的走格子很像。非常明显的动态规划题。
二叉搜索树的构建。左<根<右;
但是我依旧没有想到怎么做,10min过去了,满脑子都是糊里糊涂的回溯。
每次都在纠结用一维数组还是二维数组(不能明确dp数组含义),递推公式是啥?
看了视频才知道dp[i]表示以i个数组成的搜索二叉树个数。这题不在乎每个结点具体数值的!切记!

class Solution {
public:
    int numTrees(int n) {
        int dp[n+1];
        memset(dp, 0, sizeof(dp));
        dp[0] = 1;
        for(int i=1; i<=n; i++){
            for(int j=1; j<=i; j++){
                dp[i] += (dp[j-1] * dp[i-j]);//左子树*右子树
            }
        }
        return dp[n];

    }
};

2.0-1背包问题

2.1|46. 携带研究材料(第六期模拟笔试)

#include<bits/stdc++.h>
using namespace std;
int main(){
    int M, N;
    cin>>M>>N;
    int spa[M+1], val[M+1];
    for(int i=0; i<M; i++){
        cin>>spa[i];
    }
    for(int i=0; i<M; i++){
        cin>>val[i];
    }
    int dp[M+1][N+1];
    memset(dp, 0, sizeof(dp));
    for(int i=0; i<=N; i++){
        if(i>=spa[0]){
            dp[0][i] = val[0];
        }
    }
    for(int i=1; i<M; i++){
        for(int j=1; j<=N; j++){
            if(j>=spa[i]){
                dp[i][j]=max(dp[i-1][j], dp[i-1][j-spa[i]]+val[i]);
            }else{
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    // for(int i=0; i<M; i++){
    //     for(int j=0; j<=N; j++){
    //         cout<<dp[i][j]<<" ";
    //     }
    //     cout<<endl;
    // }
   cout<<dp[M-1][N];
   return 0;
}

2.2|一维背包滚动数组

倒序遍历是为了保证物品i只被放入一次!

#include<bits/stdc++.h>
using namespace std;
int main(){
    int M, N;
    cin>>M>>N;
    int spa[M+1], val[M+1];
    for(int i=0; i<M; i++){
        cin>>spa[i];
    }
    for(int i=0; i<M; i++){
        cin>>val[i];
    }
    int dp[N+1];
    memset(dp, 0, sizeof(dp));
    for(int i=0; i<M; i++){
        for(int j=N; j>=0; j--){
            if(j>=spa[i]){
                dp[j]=max(dp[j], dp[j-spa[i]]+val[i]);
            }else{
                dp[j] = dp[j];
            }
        }
    }
    // for(int i=0; i<M; i++){
    //     for(int j=0; j<=N; j++){
    //         cout<<dp[i][j]<<" ";
    //     }
    //     cout<<endl;
    // }
   cout<<dp[N];
   return 0;
}

2.3|416. 分割等和子集

想用回溯法做。但是超时了。

class Solution {
public:
    bool flag=false;
    int bagvalue;
    void backTtacing(int startidx, int sum, vector<int>& nums){
        if(sum > bagvalue||flag){
            return;
        }
        if(sum == bagvalue){
            flag=true;
            return;
        }    
        if(startidx==nums.size()){
            return;
        }
        for(int i=startidx; i<nums.size(); i++){
            sum += nums[i];
            backTtacing(i+1, sum, nums);
            sum -= nums[i];
        }
        return;
    }
    bool canPartition(vector<int>& nums) {
        int sum=0;
        for(int i=0; i<nums.size(); i++){
            sum += nums[i];
        }
        if(sum%2!=0){
            return false;
        }
        bagvalue=sum/2;
        backTtacing(0, 0, nums);
        return flag;

    }
};

将这个和作为背包容量。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum=0;
        for(int i=0; i<nums.size(); i++){
            sum += nums[i];
        }
        if(sum%2==1){
            return false;
        }
        int bagsize=sum/2;
        int dp[bagsize+1];
        memset(dp, 0, sizeof(dp));
        for(int i=0; i<nums.size(); i++){
            for(int j=bagsize; j>=nums[i]; j--){
                dp[j] = max(dp[j], dp[j-nums[i]]+nums[i]);
            }
        }
        return dp[bagsize]==bagsize;

    }
};

2.4|1049. 最后一块石头的重量 II

有一些想不到。
来自代码随想录

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum=0;
        for(int i=0; i<stones.size(); i++){
            sum += stones[i];
        }
        int bagsize=sum/2;
        int dp[bagsize+1];
        memset(dp, 0, sizeof(dp));
        for(int i=0; i<stones.size(); i++){
            for(int j=bagsize; j>=stones[i]; j--){
                dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-dp[bagsize]-dp[bagsize];
    }
};

2.5|494. 目标和](https://leetcode.cn/problems/target-sum/)

想回溯法,却怎么也写不出来。
来自代码随想录
原来都是能不能装满,现在问装满有几种方法。
代码思路看不懂,也想不明白。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (abs(S) > sum) return 0; // 此时没有方案
        if ((S + sum) % 2 == 1) return 0; // 此时没有方案
        int bagSize = (S + sum) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};

2.6|474. 一和零

感觉是0-1问题又像是子集问题。0-1问题,先物品再背包,倒序。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        int[] ones = new int[len];
        int[] zeros = new int[len];
        int[][] dp = new int[m+1][n+1];
        for(int i=1; i<=m; i++){
            Arrays.fill(dp[i], 0);
        }
        for(int i=0; i<len; i++){
            ones[i] = (int) strs[i].chars().filter(c -> c == '1').count();
            zeros[i] = (int) strs[i].chars().filter(c -> c == '0').count();
        }
        for(int s=0; s<len; s++){
            for(int i=m; i>=zeros[s]; i--){
                for(int j=n; j>=ones[s]; j--){
                    dp[i][j] = Math.max(dp[i][j], dp[i-zeros[s]][j-ones[s]]+1);
                }
            }
        }
        for(int i=0; i<=m; i++){
            for(int j=0; j<=n; j++){
                System.out.print(dp[i][j] + " ");
            }
            System.out.print("\n");
        } 
        return dp[m][n];
    }
}

3.完全背包问题

3.1|52. 携带研究材料(第七期模拟笔试)

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N, V;
    cin>>N>>V;
    int spa[N+1], val[N+1];
    for(int i=0; i<N; i++){
        cin>>spa[i]>>val[i];
    }
    int dp[V+1];
    memset(dp, 0, sizeof(dp));
    for(int i=0; i<N; i++){
        for(int j=spa[i]; j<=V;j++){//主要的差别就在内嵌的遍历顺序上。
            dp[j]=max(dp[j], dp[j-spa[i]]+val[i]);
        }
    }
   cout<<dp[V];
   return 0;
}

3.2|518. 零钱兑换 II

以为很简单的问题,找不到转移方程。。。看了代码随想录的视频之后才知道,自己懂得知识太少了,只是套了一个浅层的模版,稍微深层一点的知识就会难倒自己。
做第二遍了,很不幸还是陷入了上一次的思维误差。记住,先遍历物品再遍历容量,最后根据是否完全确认内嵌循环的遍历顺序。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int dp[amount+1];
        memset(dp, 0, sizeof(dp));
        dp[0] = 1;
        for(int j=0; j<coins.size(); j++){
            for(int i=coins[j]; i<=amount; i++){
                dp[i] += dp[i-coins[j]];
            }
        }
        for(int i=1; i<=amount; i++){
            cout<<dp[i]<<" "; 
        }
        return dp[amount];
    }
};

3.3|377. 组合总和 Ⅳ

这里认为顺序不同,是不同组合,所以先容量后物品。可以和上题对比学习。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target+1];
        Arrays.fill(dp, 0);
        dp[0] = 1;
        for(int i=0; i<=target; i++){
            for(int j=0; j<nums.length; j++){
                if(nums[j]<=i)
                    dp[i] += dp[i-nums[j]];
            }
        }
        return dp[target];
    }
}
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        int dp[target+1];
        memset(dp, 0, sizeof(dp));
        dp[0]=1;
        for(int i=0; i<=target; i++){
            for(int j=0; j<nums.size(); j++){
                if(nums[j] <= i && dp[i - nums[j]] < INT_MAX - dp[i]){
                    dp[i] += dp[i-nums[j]];
                }
            }
        }
        for(int i=1; i<=target; i++){
            cout<<dp[i]<<" "; 
        }
        return dp[target];
    }
};

3.4|139. 单词拆分

这里强调物品装的顺序,所以先容量后物品。背包容量是目标字符串的长度,物品重量是长度,价值是匹配字符个数。最后结果dp[n]=n,即背包背满且价值最大。

class Solution {
public:
    int calVal(string s, string t, int indx){
        int sum=0;
        for(int i=0; i<t.length(); i++){
            if(s[indx+i]==t[i]){
                sum++;
            }
        }
        return sum;
    }
    bool wordBreak(string s, vector<string>& wordDict) {
        int n=s.length();
        int dp[n+1], val;
        memset(dp, 0, sizeof(dp));
        for(int j=0; j<=n; j++){
            for(int i=0; i<wordDict.size(); i++){
                if(wordDict[i].length()<=j){
                    val = calVal(s, wordDict[i], j-wordDict[i].length());
                    dp[j] = max(dp[j], dp[j-wordDict[i].length()]+val);
                }              
            }
        }
        
        if(dp[n]==n){
            return true;
        }else{
            return false;
        }
    }
};

4.多重背包问题

4.1|56. 携带矿石资源(第八期模拟笔试)

两种思路:回归0-1背包物品信息。遍历物品个数。

#include<iostream>
#include<vector>
using namespace std;
int main() {
    int bagWeight,n;
    cin >> bagWeight >> n;
    vector<int> weight(n, 0);
    vector<int> value(n, 0);
    vector<int> nums(n, 0);
    for (int i = 0; i < n; i++) cin >> weight[i];
    for (int i = 0; i < n; i++) cin >> value[i];
    for (int i = 0; i < n; i++) cin >> nums[i];

    vector<int> dp(bagWeight + 1, 0);

    for(int i = 0; i < n; i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            // 以上为01背包,然后加一个遍历个数
            for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
                dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
            }
        }
    }

    cout << dp[bagWeight] << endl;
}

5.打家劫舍问题

5.1|198. 打家劫舍|用时11min

class Solution {
    public int rob(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 0);
        //初始化
        dp[0] = nums[0];
       
        for(int i=1; i<nums.length; i++){
            if(i==1){
                dp[1] = Math.max(nums[1], dp[i-1]);
            }else{
                dp[i] = Math.max(dp[i-2]+nums[i], dp[i-1]);
            }
        }
        return dp[nums.length-1];
    }
}

比较简单的动态规划问题,很像背包问题,给出的限制引导了赋值规则。

5.2|213. 打家劫舍 II

难以处理循环操作。代码随想录给出的思路是考虑打劫第一家,和打劫第二家两种情况。

class Solution {
public:
    int robSpe(vector<int>& nums, int startidx, int endidx){
        int n=endidx-startidx+1;
        if(n==0){
            return 0;
        }
        if(n==1){
            return nums[startidx];
        }
        int dp[n];
        memset(dp, 0, sizeof(dp));
        dp[0] = nums[startidx];
        dp[1] = max(nums[startidx+1], dp[0]);
        for(int i=2; i<n; i++){
            dp[i] = max(dp[i-1], dp[i-2]+nums[startidx+i]);
        }
        return dp[n-1];
    }
    int rob(vector<int>& nums) {
        int n=nums.size();
        if(n==0){
            return 0;
        }
        if(n==1){
            return nums[0];
        }
        int lres = robSpe(nums, 0, nums.size()-2);
        int rres = robSpe(nums, 1, nums.size()-1);
        return max(lres, rres);      
    }
};

5.3|337. 打家劫舍 III

一遇到树形结构我就怕。
想放弃。那就先放弃吧。
怎么样都要有递归的感觉在。当前节点,左子树,右子树。
代码来自代码随想录:

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]);
    }
    // 长度为2的数组,0:不偷,1:偷
    vector<int> robTree(TreeNode* cur) {
        if (cur == NULL) return vector<int>{0, 0};
        vector<int> left = robTree(cur->left);
        vector<int> right = robTree(cur->right);
        // 偷cur,那么就不能偷左右节点。
        int val1 = cur->val + left[0] + right[0];
        // 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
        int val2 = max(left[0], left[1]) + max(right[0], right[1]);
        return {val2, val1};
    }
};

6.股票问题

6.1|121. 买卖股票的最佳时机

暴力解法超时。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //暴力解放就是找left和right的最大差值。
        int maxgap = 0;
        for(int i=0; i<prices.size(); i++){
            for(int j=i+1; j<prices.size(); j++){
                if(maxgap<prices[j]-prices[i]){
                    maxgap=prices[j]-prices[i];
                }
            }
        }
        return maxgap;

    }
};

dp方法自己是列了三种状态。代码随想录给出了两种状态。而且贪心解法也应该学习。
在这里插入图片描述

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //dp解法
        int n = prices.size(), res=0;
        int dp[n+1][3];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        for(int i=1; i<n; i++){
            dp[i][0] = dp[i-1][0];
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
            dp[i][2] = dp[i-1][1]+prices[i];
            if(res<dp[i][2]){
                res = dp[i][2];
            }
        }
        return res;

    }
};

两种状态

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //dp解法
        int n = prices.size(), res=0;
        int dp[n+1][2];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i=1; i<n; i++){
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = max(dp[i-1][1], -prices[i]);//买不买
        }
        return dp[n-1][0];
    }
};

贪心:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int result = 0;
        for (int i = 0; i < prices.size(); i++) {
            low = min(low, prices[i]);  // 取最左最小价格
            result = max(result, prices[i] - low); // 直接取最大区间利润
        }
        return result;
    }
};

6.2|122. 买卖股票的最佳时机 II

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int dp[n+1][2];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i=1; i<n; i++){
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return max(dp[n-1][0], dp[n-1][1]);

    }
};

6.3|123. 买卖股票的最佳时机 III

思路是五个状态。但是转移方程找不对。(错啦,是初始化错了。)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //dp解法
        int n = prices.size(), res=0;
        int dp[n+1][5];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];//重点
        dp[0][4] = 0;
        for(int i=1; i<n; i++){
            dp[i][0] = dp[i-1][0];
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
            dp[i][2] = max(dp[i-1][2], dp[i-1][1]+prices[i]);
            dp[i][3] = max(dp[i-1][3], dp[i-1][2]-prices[i]);
            dp[i][4] = max(dp[i-1][4], dp[i-1][3]+prices[i]);
            dp[i][4] = max(dp[i][4], dp[i-1][1]+prices[i]);
        }
        
        return dp[n-1][4];
    }
};

在这里插入图片描述

6.4|188. 买卖股票的最佳时机 IV

限制k次。

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        //dp解法
        int n = prices.size(), res=0;
        int dp[n+1][2*k+1];
        memset(dp, 0, sizeof(dp));  
        for (int j = 1; j < 2 * k; j += 2) {
            dp[0][j] = -prices[0];
        }
        for(int i=1; i<n; i++){
            dp[i][0] = dp[i-1][0];
            for (int j = 0; j < 2 * k - 1; j += 2) {
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
                dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
            }
        }  
        return dp[n-1][2*k];
    }
};

6.5|309. 买卖股票的最佳时机含冷冻期

依旧是思考10min出不来,怎么这么菜呀!
灵茶山艾府:状态转移进行分析。
一共两个状态,四个转移。

class Solution {
    public int maxProfit(int[] prices) {
        int n =prices.length;
        int[][] dp = new int[n+1][2];
        for(int i=0; i<=n; i++){
            Arrays.fill(dp[i], 0);
        }  
        dp[0][0] = 0;
        dp[0][1] = Integer.MIN_VALUE;
        dp[1][1] = -prices[0];
        dp[1][0] = 0;
        for(int i=2; i<=n; i++){
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i-1]);
        }
        // for(int i=0; i<=n; i++){
        //     System.out.println(dp[i][0]+" "+dp[i][1]);
        // }
        return Math.max(dp[n][0], dp[n][1]);
    }
}

在这里插入图片描述

6.5|714. 买卖股票的最佳时机含手续费

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        int dp[n+1][2];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i=1; i<n; i++){
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]-fee);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return max(dp[n-1][0], dp[n-1][1]);
    }
};

7.子序列问题

7.1|300. 最长递增子序列

10min思考,没有合适的思路。找不到合适的更新公式。
子集型回溯思路 :(1)选或者不选?(2)枚举选哪个?在这里插入图片描述
官方题解给出的是枚举选哪个?

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 0);
        dp[0] = 1;
        int res = dp[0], ma;
        for(int i=1; i<n; i++){
            ma = 0;
            for(int j=i-1; j>=0; j--){
                if(nums[j]<nums[i]&&ma<dp[j]){
                    ma = dp[j];
                }
            }
            dp[i] = ma + 1;
            if(dp[i]>res)
                res=dp[i];
        }
        return res;
    }
}
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int res=0;
        int n=nums.size();
        vector<int> dp(n, 1);
        for(int i=0; i<n; i++){
            for(int j=i-1; j>=0; j--){
                if(nums[j]<nums[i]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
            if(dp[i]>res){
                res=dp[i];
            }
        }
        for(int i=0; i<n; i++){
            cout<<dp[i]<<" ";
        }
        return res;

    }
};

7.2|300. 最长递增子序列

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int res=1;
        int n=nums.size();
        vector<int> dp(n, 1);
        for(int i=1; i<n; i++){          
            if(nums[i]>nums[i-1]){
                dp[i] = dp[i-1]+1;
            }         
            if(dp[i]>res){
                res=dp[i];
            }
        }
        for(int i=0; i<n; i++){
            cout<<dp[i]<<" ";
        }
        return res;
    }
};

7.3|718. 最长重复子数组

dp[i][j] nums1[i]和nums[j]是否相等,分为两种情况。

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int n1=nums1.size(), n2=nums2.size(), res=0;
        int dp[n1][n2];
        memset(dp, 0, sizeof(dp));
        for(int i=0; i<n2; i++){
            if(nums1[0]==nums2[i]){
                dp[0][i]=1;
                res=max(res, dp[0][i]);
            }
        }
        for(int i=0; i<n1; i++){
            if(nums2[0]==nums1[i]){
                dp[i][0]=1;
                res=max(res, dp[i][0]);
            }
        }
        for(int i=1; i<n1; i++){
            for(int j=1; j<n2; j++){
                if(nums1[i]==nums2[j]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = 0;
                }
                res=max(res, dp[i][j]);
            }
        }
        return res;

    }
};

7.4|1143. 最长公共子序列

与上一题,除了状态转移不一样,思路完全一样。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n1 = text1.length();
        int n2 = text2.length();
        int dp[n1+1][n2+1];
        memset(dp, 0, sizeof(dp));
        for(int r=1; r<=n1; r++)
        {
            for(int c=1; c<=n2; c++){
                if(text1[r-1] == text2[c-1]){
                    dp[r][c] = dp[r-1][c-1] + 1;
                }else{
                    dp[r][c] = max(dp[r-1][c],  dp[r][c-1]);
                }
            }
        }
        return dp[n1][n2];

    }
};

7.5|1035. 不相交的线

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n1=nums1.size(), n2=nums2.size();
        int dp[n1+1][n2+1];
        memset(dp, 0, sizeof(dp));
        for(int i=1; i<=n1; i++){
            for(int j=1; j<=n2; j++){
                if(nums1[i-1]==nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = max(dp[i-1][j],  dp[i][j-1]);
                }
            }
        }
        return dp[n1][n2];
    }
};

7.6|53. 最大子数组和

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        //dp算法
       int n= nums.size(), res=INT_MIN;
       int dp[n];
       memset(dp, 0, sizeof(dp));
       dp[0] = nums[0];
       res = max(res, dp[0]);
       for(int i=1; i<n; i++){
           dp[i] = max(nums[i], dp[i-1]+nums[i]);
           res = max(res, dp[i]);
       }
       for(int i=0; i<n; i++){
           cout<<dp[i]<<" ";
       }
       return res;
    }
};

7.7|392. 判断子序列

与前面判断子序列和不相交的线思路完全一样。

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int ns=s.length(), nt=t.length();
        int dp[nt+1][ns+1];
        memset(dp, 0, sizeof(dp));
        for(int i=1; i<=nt; i++){
            for(int j=1; j<=ns; j++){
                if(t[i-1]==s[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = dp[i-1][j];//相当于删除t中的一个元素。
                }
            }
        }
        return dp[nt][ns]==ns;
    }
};

7.8|115. 不同的子序列

这里参考了视频【LeetCode 每日一题】115. 不同的子序列 | 手写图解版思路 + 代码讲解
在这里插入图片描述
不相等,s[i]不作出贡献。
相等,考虑s[i]的贡献,和不考虑s[i]的贡献.
初始化,参考了代码随想录。
在这里插入图片描述

class Solution {
public:
    int numDistinct(string s, string t) {
        int ns=s.length(), nt=t.length();
        int dp[ns+1][nt+1];
        memset(dp, 0, sizeof(dp));
        for(int i=0; i<=ns; i++){
            dp[i][0]=1;
        }
        for(int i=1; i<=ns; i++){
            for(int j=1; j<=nt; j++){
                if(s[i-1]==t[j-1]){
                    dp[i][j]=(dp[i-1][j]+dp[i-1][j-1])%(1000000000+7);
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[ns][nt];

    }
};

7.9|583. 两个字符串的删除操作

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n1 = word1.length();
        int n2 = word2.length();
        int dp[n1+1][n2+1], res=0;
        memset(dp, 0, sizeof(dp));

        //相隔了快一周完全想不起来思路了,痛苦。|-.-|,不过这种子序列嘛,首要思路不就是当前字符相不相等嘛,so easy!
        for(int i=1; i<=n1; i++){
            for(int j=1; j<=n2; j++){
                if(word1[i-1]==word2[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        res = n1-dp[n1][n2] + n2-dp[n1][n2];
        return res;      
    }
};

在这里插入图片描述

7.10|72. 编辑距离

这一题首先的思路是延续上一题,用word1长度-配对的子序列长度,但是在示例二就错误了,因为牵扯了位置的问题。
所以我想先回归一下暴力,看看人脑是怎么操作这件事情的。
救命,我甚至想不到人脑要怎么处理这个问题。。。
这里参考【LeetCode 每日一题】72. 编辑距离 | 手写图解版思路 + 代码讲解
在这里插入图片描述

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n1 = word1.length();
        int n2 = word2.length();
        vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
        for(int i=1; i<=n1; i++){
            dp[i][0] = i;
        }
        for(int j=1; j<=n2; j++){
            dp[0][j] = j;
        }
        for(int i=1; i<=n1; i++){
            for(int j=1; j<=n2; j++){
                if(word1[i-1]==word2[j-1]){
                    dp[i][j] = dp[i-1][j-1];
                }else{                   
                    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
                }
            }  
        }
        return dp[n1][n2]; 

    }
};

这题一直感觉与前面做的题目跨度很大,没想到是7.9自己想得太简单了。

7.11|647. 回文子串

class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
                    result++;
                    dp[i][j] = true;
                }
            }
        }
        return result;
    }
};

在这里插入图片描述

7.12|516. 最长回文子序列

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        int result = 0;
        for (int i = s.size() - 1; i >= 0; i--) {
            for(int j = i; j < s.size(); j++){
                if(s[i] == s[j] && j - i == 0){
                    dp[i][j] = 1;
                }
                if(s[i] == s[j] && j - i == 1){
                    dp[i][j] = 2;
                }
                if(s[i] == s[j] && j - i >= 2){
                    dp[i][j] = dp[i+1][j-1] + 2;
                }
                if(s[i]!=s[j]){
                    dp[i][j] = max({dp[i+1][j], dp[i][j-1], dp[i+1][j-1]});
                }
                result = max(result, dp[i][j]);
            }
        }   
        return result;
    }
};

1|5. 最长回文子串|用时42min

class Solution {
public:
    string longestPalindrome(string s) {
        //先初始化dp, 0-false, 1-true
        int len = s.length();
        int dp[len][len];
        string res;
        memset(dp, 0, sizeof(dp));
        for(int i=0;i<len;i++){
            dp[i][i] = 1; 
            res = s.substr(i ,1);
        }
        for(int i=0;i<len-1;i++){
           if(s[i] == s[i+1]){
               dp[i][i+1] = 1;
               res = s.substr(i ,2);
           } 
        }
        //动态赋值条件 dp[i][j] = (s[i] == s[j]) && (dp[i+1][j-1] == 1)
        for(int j=2; j<len; j++){
            for(int i=0; i<j-2+1; i++){
                // cout<<i<<j<<s[i]<<" "<<s[j]<<" "<<dp[i+1][j-1]<<endl;
                if(s[i] == s[j] && dp[i+1][j-1] == 1){
                    dp[i][j] = 1;
                    
                } 
                if(dp[i][j] == 1 && abs(j-i+1)>res.length()){
                    res = s.substr(i, abs(j-i+1));

                }
            }
        }
        return res;
    }
};

想要小结一下,对于C++的确非常不熟悉,一直想要不要改用Java。可能二刷会用Java吧,毕竟Java才是自己的主力语言。
明明看过一姐的视频后刷题,但是因为最长子串的记录和C++的字符串语法问题,耽搁了很久。

----------------------------------------------------------------------------2023年10月16日----------------------------------------------------------

4|300. 最长递增子序列

10min思考,没有合适的思路。找不到合适的更新公式。
子集型回溯思路 :(1)选或者不选?(2)枚举选哪个?在这里插入图片描述
官方题解给出的是枚举选哪个?

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 0);
        dp[0] = 1;
        int res = dp[0], ma;
        for(int i=1; i<n; i++){
            ma = 0;
            for(int j=i-1; j>=0; j--){
                if(nums[j]<nums[i]&&ma<dp[j]){
                    ma = dp[j];
                }
            }
            dp[i] = ma + 1;
            if(dp[i]>res)
                res=dp[i];
        }
        return res;
    }
}

----------------------------------------------------------------------------2023年10月17日----------------------------------------------------------

5|1143. 最长公共子序列|8min

一拿到题目,感觉和之前做的题目有些没办法套思路。去看了灵茶山艾府的视频,在递推公式和证明的部分没看懂(看了三遍),又去找了其他up主的视频,最后才弄明白。
其实思路还是很简单的,弄懂dp[i][j]表示的状态(s中前i个字母和t中前j个字母,的 最长公共子序列)。
递推公式,dp[i][j],根据s[i]和t[j]相等与否设计了两个递推公式,相等dp[i-1][j-1] +1,不相等,max(dp[i-1][j], dp[i][j-1])。四种状态,选其中一个,两不选,两都选。艾府的视频中介绍并证明了。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n1 = text1.length();
        int n2 = text2.length();
        int dp[n1+1][n2+1];
        memset(dp, 0, sizeof(dp));
        for(int r=1; r<=n1; r++)
        {
            for(int c=1; c<=n2; c++){
                if(text1[r-1] == text2[c-1]){
                    dp[r][c] = dp[r-1][c-1] + 1;
                }else{
                    dp[r][c] = max(dp[r-1][c],  dp[r][c-1]);
                }
            }
        }
        return dp[n1][n2];

    }
};

----------------------------------------------------------------------------2023年10月19日----------------------------------------------------------

8|279. 完全平方数

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n+1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        dp[1] = 1;
        for(int i=2; i<=n; i++){   
            int a = (int)Math.sqrt(i);
            if(a*a==i){
                dp[i] = 1;
            }else{     
                for(int j=1; j<(int)i/2+1; j++){
                    int temp = dp[i-j] + dp[j];
                    if(temp<dp[i]){
                        dp[i] = temp;
                    }
                }
            }
        }
        return dp[n];
    }
}

----------------------------------------------------------------------------2023年10月20日----------------------------------------------------------

9|2140. 解决智力问题|40min

这题就差把动态规划四个字写在题目里了。子集型的问题,枚举还是选不选呢?
选择枚举法,时间复杂度很容易算出来是O(n^2),超时,代码如下:

class Solution {
public:
    long long mostPoints(vector<vector<int>>& questions) {
        int n = questions.size();
        int dp[n];
        memset(dp, 0, sizeof(dp));
        int maxAns=0;
        dp[0] = questions[0][0];
        maxAns = dp[0];
        for(int i=1; i<n; i++){
            int maxFore = 0;
            for(int j=0; j<i; j++){
                if(questions[j][1]+j<i&&maxFore<dp[j])
                {
                    maxFore = dp[j];
                }
            }
            dp[i] = maxFore + questions[i][0];
            if(dp[i]>maxAns){
                maxAns = dp[i];
            }
        }
        return maxAns;
    }
};

随后按照官方题解,反向思考,状态转移方程变成

class Solution {
public:
    long long mostPoints(vector<vector<int>>& questions) {
        int n = questions.size();
        long long dp[n];
        memset(dp, 0, sizeof(dp));
        long long maxAns=0;
        dp[n-1] = questions[n-1][0];
        maxAns = dp[n-1];
        for(int i=n-2; i>=0; i--){
            long long  temp = questions[i][1]+i+1;
            if(temp>n-1){
                temp=0;
            }else{
                temp=dp[temp];
            }
            dp[i] = max(dp[i+1], questions[i][0]+ temp);
            if(dp[i]>maxAns){
                maxAns = dp[i];
            }
        }
        for(int i=0; i<n; i++){
            cout<<dp[i]<<" ";
        }
        return maxAns;
    }
};

10|322. 零钱兑换|50min

主要花费递归方程的编写上。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount+1];
        Arrays.fill(dp, -1);
        dp[0] = 0;
        Arrays.sort(coins);
        for(int i=0; i<coins.length; i++){
            if(coins[i]<=amount)
                dp[coins[i]] = 1;
        }
        for(int i=coins[0]; i<=amount; i++){
            if(dp[i]==1){
                continue;
            }
            int temp = Integer.MAX_VALUE, flag=0;
            for(int j=0; j<coins.length; j++)
            {
                int cur = i - coins[j];
                if(cur<0)
                {
                    break;
                }
                if(dp[cur]>0){
                    flag=1;
                    if(temp>dp[cur]+1){
                        temp = dp[cur]+1;
                    }
                    dp[i] = temp;
                }

            }
            if(flag==0){
                dp[i] = -1;
            }

        }
        for(int i=1; i<=amount; i++){
            System.out.print(dp[i]+ " ");
        }
        return dp[amount];
    }
}

官方的代码更加简单:

public class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/coin-change/solutions/132979/322-ling-qian-dui-huan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 云南茶山位于云南省曲靖市宣威市,东西跨江,北接滇池,南接大理市,是云南省茶叶的发源地,是中国著名的茶叶之乡。茶山原名叫茶城,古时候有“三茶一石”之称,即茶山、茶城两座山,加上一块大石,因此得名。茶山以其独特的茶文化、历史文化和自然风景吸引着海内外游客,是一个具有浓郁的茶文化气息的绿色旅游景点。茶山以它的深厚的文化底蕴、美丽的自然风光和独特的民族风情深受游客的喜爱。茶山的风景独特,是一个绿色的森林公园,有空旷的茶园、茶山茶山湖、茶厂、民居和古迹等,是云南省最大的茶叶生产基地。茶山有众多的历史文物,如景福寺、海拔殿、滇池殿、山西院等,历史悠久,非常值得一游。茶山也是云南省茶叶的发源地,以其独特的茶文化、历史文化和自然风景吸引着海内外游客,是一个具有浓郁的茶文化气息的绿色旅游景点。茶山还有著名的山茶花,每年的三月份到五月份是山茶花的盛开期,是观赏山茶花的最佳时机,可以看到满山遍野的山茶花,芳香四溢,令人流连忘返。茶山是一个具有独特风景和茶文化的绿色旅游胜地,可以让游客领略到古朴的茶文化,给游客留下深刻的印象。 ### 回答2: 云南茶山位于中国云南省西双版纳傣族自治州勐海县,是中国最著名的茶叶产区之一。云南茶山是云南丰富优质茶叶资源的核心地带,茶园分布在海拔1400米至1800米的山坡上,土壤肥沃,气候温暖湿润,适宜茶树生长。茶山遍布云贵高原的低海拔山地,地形起伏多变,形成了独特的茶树生态。 云南茶山以菜籽油树林下茶园为特色,这种种植模式具有独特的生态优势,能够充分利用土壤、气候和生态环境资源,为茶树提供了良好的生长环境。而菜籽油树林也可以提供树荫,保护茶树不受强光直射,并减少土壤水分的蒸发,提高茶叶品质。 云南茶山以普洱茶为代表,普洱茶是中国六大茶类之一,有着悠久的历史和独特的工艺。普洱茶的采摘、制作、贮存和饮用都有严格的要求和程序。其茶树品种主要有大叶种和中小叶种,茶叶形状多样,有饼茶、沱茶、砖茶、散茶等。普洱茶口感醇厚,香气独特,有降脂减肥、抗氧化、降压、提神醒脑等功效,深受茶叶爱好者的喜爱。 除了普洱茶,云南茶山还产出其他优质茶叶,如滇红茶、云南白草茶、黑兔毛茶等。这些茶叶在国内外市场上都有着较高的知名度和需求量。 云南茶山的旅游资源也十分丰富,茶山风光如画,茶园群山环抱,绿意盎然,吸引了大批游客前来观光和品茶。旅游者可以在茶园中亲自体验采茶和制茶的过程,了解茶叶的生产工艺和茶文化。同时,云南茶山还有许多古茶树群落,这些具有几百年甚至千年以上历史的茶树古树,是茶叶品质的保证,也是茶文化的重要组成部分。 综上所述,云南茶山作为中国茶叶产区之一,以其独特的生态环境和种植模式,优质的茶叶品种和精湛的制作工艺,以及丰富多样的旅游资源,成为茶叶爱好者和游客们不可错过的地方。云南茶山的产茶历史悠久,是中国茶文化的重要组成部分,也是云南省重要的农业和旅游产业之一。 ### 回答3: 云南茶山位于中国云南省西南部地区,是中国著名的茶叶产区之一。茶山地区地势起伏,气候多样,土壤肥沃,是茶叶生长的理想环境。 云南茶山的主要茶叶品种有普洱茶、滇红茶、云南绿茶等。普洱茶以其特殊的发酵制造技艺而闻名,色泽红润,滋味浓郁,具有良好的陈化潜力。滇红茶则为红茶中的代表,茶叶呈现红褐色,滋味醇厚,带有果香和花香气息。云南绿茶则以其嫩绿的外观、清香的口感和鲜爽的回甘赢得了众多茶叶爱好者的喜爱。 云南茶山的茶叶种植历史悠久,可以追溯到公元前2世纪左右。受益于云南独特的地理环境和气候条件,在此地茶树的生长速度较快,茶叶的品质也非常优良。茶山地区的高海拔和丰富的降水量为茶树提供了充足的水源,而茶园周围的山脉则保护了茶叶的生长环境,使其不受污染。 茶山的茶叶栽种以手工采摘为主,采摘过程中只选择嫩叶,以确保茶叶的质量和口感。采摘后的茶叶经过日光萎凋、揉捻、发酵、干燥等工序后,最终成为优质的茶叶产品。 云南茶山的茶叶不仅在国内畅销,也出口到世界各地。当地的茶叶产业为当地农民提供了就业机会和增加收入的途径,对地方经济的发展有着积极的影响。 总之,云南茶山凭借其独特的生态环境和理想的气候条件,生产出了许多优质的茶叶品种。这些茶叶不仅具有浓郁的香气和独特的口感,还具有丰富的营养成分。云南茶山的茶叶产业在国内外市场上受到了广泛的认可和青睐。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值