【LeetCode】15.贪心算法系列
总目录:
0.理论基础
0.1.概念
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
如果找出局部最优并可以推出全局最优,就是贪心,如果局部最优都没找出来,就不是贪心,可能是单纯的模拟。
贪心算法一般分为如下四步:
(1)将问题分解为若干个子问题;
(2)找出适合的贪心策略;
(3)求解每一个子问题的最优解;
(4)将局部最优解堆叠成全局最优解。
0.2.注意
贪心算法并没有特定的题型,是否适用贪心算法要试一试。
分析问题时要注意梳理局部最优是什么,如何由此推出全局最优。
1.分发饼干
1.1.问题描述
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
链接:https://leetcode.cn/problems/assign-cookies
1.2.要点
贪心策略:用尽量小的饼干应付胃口尽量小的人。
1.3.代码实例

1 class Solution { 2 public: 3 int findContentChildren(vector<int>& g, vector<int>& s) { 4 int ret=0; 5 sort(g.begin(),g.end()); 6 sort(s.begin(),s.end()); 7 8 bool getFeeded=false;//是否被满足 9 int firstCok=0;//第一个可用饼干 10 for(int& child:g){ 11 getFeeded=false; 12 for(int i=firstCok;i<s.size();i++){ 13 //不够 14 if(s[i]<child){ 15 continue; 16 } 17 18 firstCok=i+1;//更新下一个可用 19 ret++; 20 getFeeded=true; 21 break; 22 } 23 24 //未被满足,后面的也不用看了 25 if(!getFeeded){ 26 break; 27 } 28 } 29 30 return ret; 31 } 32 };
2.摆动序列
2.1.问题描述
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
链接:https://leetcode.cn/problems/wiggle-subsequence
2.2.要点
贪心策略:算是一种增长算法,遇到不符合条件的则跳过,只取符合趋势的内容。
2.3.代码实例

1 class Solution { 2 public: 3 int wiggleMaxLength(vector<int>& nums) { 4 int dataLen=nums.size(); 5 if(dataLen<=1){ 6 return dataLen; 7 } 8 9 int ret=1; 10 int startIndex=1; 11 bool preIsPos=false;//前一个差是否大于0 12 bool curIsPos=false; 13 14 //取第一个值 15 while(startIndex<dataLen){ 16 if(nums[startIndex]!=nums[startIndex-1]){ 17 preIsPos=(nums[startIndex]-nums[startIndex-1])>0; 18 startIndex++; 19 ret++; 20 break; 21 } 22 23 startIndex++; 24 } 25 26 //贪心查找后面的值 27 for(int i=startIndex;i<dataLen;i++){ 28 if(nums[i]==nums[i-1]){ 29 continue; 30 } 31 curIsPos=(nums[i]-nums[i-1])>0; 32 if(preIsPos==curIsPos){ 33 continue; 34 } 35 36 preIsPos=curIsPos; 37 ret++; 38 } 39 40 return ret; 41 } 42 };
3.最大子数组和
3.1.问题描述
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
链接:https://leetcode.cn/problems/maximum-subarray/
3.2.要点
贪心策略:当发现为负时,及时止损,重新开始。
3.3.代码实例

1 class Solution { 2 public: 3 int maxSubArray(vector<int>& nums) { 4 int dataLen=nums.size(); 5 if(dataLen<=0){ 6 return 0; 7 } 8 9 int maxSum = INT_MIN; 10 int tempSum = 0; 11 for(int i=0;i<dataLen;i++){ 12 tempSum+=nums[i]; 13 maxSum=max(maxSum,tempSum); 14 15 if(tempSum<0){ 16 tempSum=0; 17 } 18 } 19 20 return maxSum; 21 } 22 };
4.买卖股票的最大收益
4.1.问题描述
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii
4.2.要点
贪心策略:取所有正收益即为最大收益。
4.3.代码实例

1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 int ret=0; 5 for(int i=1;i<prices.size();i++){ 6 ret+=max(prices[i]-prices[i-1],0); 7 } 8 9 return ret; 10 } 11 };
5.跳跃游戏,能否到最后
5.1.问题描述
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
链接:https://leetcode.cn/problems/jump-game/
5.2.要点
贪心策略:每个格子都跳一下,记录可达的最远处。注意中止条件:遍历到的位置大于最大可达时即为失败。
5.3.代码实例

1 class Solution { 2 public: 3 bool canJump(vector<int>& nums) { 4 int lastPos=0; 5 for(int i=0;i<nums.size();i++){ 6 if(i>lastPos){return false;} 7 lastPos=max(lastPos,i+nums[i]); 8 } 9 return true; 10 } 11 };
6.跳跃游戏,到最后的最小步数
6.1.问题描述
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:其中0 <= j <= nums[i] ,i + j < n。
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
链接:https://leetcode.cn/problems/jump-game-ii
6.2.要点
贪心策略:搜索空间。在可选范围内从第1个开始搜,记录可到达的最大值,由此生成下一轮搜索范围。
6.3.代码实例

1 class Solution { 2 public: 3 int jump(vector<int>& nums) { 4 int steps=0; 5 int maxPos=0;//当前可达的最大位置 6 int start=0,end=1;//start是搜索范围内的第1个,end是搜索范围外的第1个 7 while(end<nums.size()){ 8 for(int i=start;i<end;i++){ 9 maxPos=max(maxPos,i+nums[i]); 10 } 11 12 start=end;//end是上一阶段范围外的第一个,也就是下一阶段的搜索起点 13 end=maxPos+1;//新的搜索结束点 14 steps++;//步数 15 } 16 return steps; 17 } 18 };
7.K次取反后最大化的数组和
7.1.问题描述
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
链接:https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations
7.2.要点
贪心策略:尽量将负数取反过来,优先取反绝对值较大的负数使收益最大。如果负数不够,就取反最小非负数使损失最低。
7.3.代码实例

1 class Solution { 2 static bool cmp(int a, int b) { 3 return abs(a) > abs(b); 4 } 5 public: 6 int largestSumAfterKNegations(vector<int>& A, int K) { 7 sort(A.begin(), A.end(), cmp); // 第一步 8 for (int i = 0; i < A.size(); i++) { // 第二步 9 if (A[i] < 0 && K > 0) { 10 A[i] *= -1; 11 K--; 12 } 13 } 14 if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步 15 int result = 0; 16 for (int a : A) result += a; // 第四步 17 return result; 18 } 19 };
8.加油站问题
8.1.问题描述
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
链接:https://leetcode.cn/problems/gas-station
8.2.要点
贪心策略:在总量可以满足的前提下,遍历寻找累计油量为负的油站,然后尝试从下一个油站出发。
8.3.代码实例

1 class Solution { 2 public: 3 int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { 4 //计算总油耗是否满足 5 int sumLeft=0; 6 for(int i=0;i<gas.size();i++){ 7 gas[i]=gas[i]-cost[i]; 8 sumLeft+=gas[i]; 9 } 10 if(sumLeft<0){ 11 return -1; 12 } 13 14 //寻找合适的起点,条件是前面累加过来的不能为负 15 int startPos=0; 16 sumLeft=0; 17 for(int i=0;i<gas.size();i++){ 18 sumLeft+=gas[i]; 19 if(sumLeft<0){ 20 startPos=i+1;//从下一个位置开始 21 sumLeft=0;//重新累加 22 } 23 } 24 25 return startPos; 26 } 27 };
9.分发糖果
9.1.问题描述
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
链接:https://leetcode.cn/problems/candy
9.2.要点
双维度问题,先确定一个维度再确定另外一个维度。
贪心策略:分数较高的比旁边人多1个就好。从左到右遍历一次,从右到左再遍历一次。
9.3.代码实例

1 class Solution { 2 public: 3 int candy(vector<int>& ratings) { 4 int chCnt=ratings.size(); 5 vector<int> candyVec(chCnt,1); 6 for(int i=0;i<chCnt;i++){ 7 if(i>0&&ratings[i]>ratings[i-1]){ 8 candyVec[i]=max(candyVec[i],candyVec[i-1]+1); 9 } 10 } 11 for(int i=chCnt-1;i>=0;i--){ 12 if(i<(chCnt-1)&&ratings[i]>ratings[i+1]){ 13 candyVec[i]=max(candyVec[i],candyVec[i+1]+1); 14 } 15 } 16 17 int canSum=0; 18 for(int& num:candyVec){ 19 canSum+=num; 20 } 21 return canSum; 22 } 23 };
10.卖货找零
10.1.问题描述
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
链接:https://leetcode.cn/problems/lemonade-change
10.2.要点
贪心策略:尽量用10块1张的找零,尽量少用万能的面值5块。
10.3.代码实例

1 class Solution { 2 public: 3 bool lemonadeChange(vector<int>& bills) { 4 int fiveCnt=0,tenCnt=0; 5 for(int i=0;i<bills.size();i++){ 6 if(bills[i]==5){ 7 fiveCnt++; 8 continue; 9 } 10 if(bills[i]==10){ 11 if(fiveCnt<=0){ 12 return false; 13 } 14 fiveCnt--; 15 tenCnt++; 16 continue; 17 } 18 if(bills[i]==20){ 19 //这里涉及贪心策略,优先消耗10美元! 20 if(tenCnt<=0){ 21 if(fiveCnt<3){ 22 return false; 23 } 24 fiveCnt-=3; 25 } 26 else{ 27 if(fiveCnt<=0){ 28 return false; 29 } 30 fiveCnt--; 31 tenCnt--; 32 } 33 } 34 } 35 36 return true; 37 } 38 };
11.根据身高重建队列
11.1.问题描述
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
链接:https://leetcode.cn/problems/queue-reconstruction-by-height
11.2.要点
双维度问题,先确定一个维度再确定另外一个维度。
贪心策略:先排队高的人在前,然后让高的里面要求少的人先选。
注意:频繁的插入数据,使用链表以增加性能。
11.3.代码实例

1 // 版本二 2 class Solution { 3 public: 4 // 身高从大到小排(身高相同k小的站前面) 5 static bool cmp(const vector<int>& a, const vector<int>& b) { 6 if (a[0] == b[0]) return a[1] < b[1]; 7 return a[0] > b[0]; 8 } 9 vector<vector<int>> reconstructQueue(vector<vector<int>>& people) { 10 sort (people.begin(), people.end(), cmp); 11 list<vector<int>> que; 12 for(int i=0;i<people.size();i++){ 13 std::list<vector<int>>::iterator iter=que.begin(); 14 int index=people[i][1]; 15 while(index--){ 16 iter++; 17 } 18 que.insert(iter,people[i]); 19 } 20 21 return vector<vector<int>>(que.begin(),que.end()); 22 } 23 };
12.用最少数量的箭引爆气球
12.1.问题描述
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
链接:https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons
12.2.要点
双维度问题:先确定一个维度,再确定另外一个维度。
贪心策略:萎缩的右边界。
先左边界排序,然后两两比较相邻两个气球。如果有覆盖则以较小值更新右边界,如果没覆盖则加一支箭。
12.3.代码实例

1 class Solution { 2 private: 3 static bool cmp(const vector<int>& a, const vector<int>& b) { 4 return a[0] < b[0]; 5 } 6 public: 7 int findMinArrowShots(vector<vector<int>>& points) { 8 if (points.size() == 0) return 0; 9 sort(points.begin(), points.end(), cmp); 10 11 int result = 1; // points 不为空至少需要一支箭 12 for (int i = 1; i < points.size(); i++) { 13 if (points[i][0] > points[i - 1][1]) { // 下一个气球左边界大于前一个气球的右边界 14 result++; // 需要一支箭 15 } 16 else { // 气球i和气球i-1挨着 17 points[i][1] = min(points[i - 1][1], points[i][1]); // 使用最小右边界 18 } 19 } 20 return result; 21 } 22 };
13.无重叠区间
13.1.问题描述
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
链接:https://leetcode.cn/problems/non-overlapping-intervals
13.2.要点
双维度问题,确定一个再确定另外一个。
贪心策略:先排序左边界,然后记录右边界,右重合则删除、未重合则更新右边界。
13.3.代码实例

1 class Solution { 2 public: 3 static bool cmp(vector<int> a,vector<int> b){ 4 return a[0]<=b[0]; 5 } 6 int eraseOverlapIntervals(vector<vector<int>>& intervals) { 7 int ret=0; 8 sort(intervals.begin(),intervals.end()); 9 10 int lastRight=intervals[0][1]; 11 for(int i=0;i<intervals.size();i++){ 12 //下一个的左边界小于前一个的右边界 13 if(i>0&&intervals[i][0]<lastRight){ 14 ret++; 15 lastRight=min(intervals[i][1],lastRight); 16 } 17 else{ 18 lastRight=intervals[i][1]; 19 } 20 } 21 22 return ret; 23 } 24 };
14.划分字母区间
14.1.问题描述
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
链接:https://leetcode.cn/problems/partition-labels
14.2.要点
记录所有字符最后一次出现的位置。遇到一个字符可知其最后一次出现位置,但不一定是合适的切割位置,因为中间字符有可能使得结束位置更靠后。
所以遍历过程中不停更新right,直到i==right结束这一段。
贪心策略:取得尽量右,包含本字符的所有字符。
14.3.代码实例

1 class Solution { 2 public: 3 vector<int> partitionLabels(string S) { 4 int hash[27] = {0}; // i为字符,hash[i]为字符出现的最后位置 5 for (int i = 0; i < S.size(); i++) { // 统计每一个字符最后出现的位置 6 hash[S[i] - 'a'] = i; 7 } 8 vector<int> result; 9 int left=0; 10 int right = 0; 11 for (int i = 0; i < S.size(); i++) { 12 right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界 13 14 //到了指定的位置 15 if(i==right){ 16 result.push_back(right - left + 1);//存储数据 17 left=right+1; 18 } 19 } 20 return result; 21 } 22 };
15.合并区间
15.1.问题描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
链接:https://leetcode.cn/problems/merge-intervals
15.2.要点
贪心策略:增长的右边界。
15.3.代码实例

1 class Solution { 2 public: 3 static bool cmp(vector<int> a,vector<int> b){ 4 return a[0]<b[0]; 5 } 6 vector<vector<int>> merge(vector<vector<int>>& intervals) { 7 sort(intervals.begin(),intervals.end()); 8 vector<vector<int>> ret; 9 if(intervals.size()==0) return ret; 10 11 int left=intervals[0][0]; 12 int right=intervals[0][1]; 13 for(int i=1;i<intervals.size();i++){ 14 //未重叠 15 if(intervals[i][0]>right){ 16 ret.push_back(vector<int>{left,right}); 17 left=intervals[i][0]; 18 right=intervals[i][1]; 19 } 20 else{ 21 right=max(right,intervals[i][1]); 22 } 23 } 24 25 //兜住最后一段 26 ret.push_back(vector<int>{left,right}); 27 28 return ret; 29 } 30 };
16.单调递增的数字
16.1.问题描述
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
链接:https://leetcode.cn/problems/monotone-increasing-digits
16.2.要点
转为字符串更好处理一些。
贪心策略:找到尽量高的需要修改的位,从它开始后面都换为9.
16.3.代码实例

1 class Solution { 2 public: 3 int monotoneIncreasingDigits(int N) { 4 string strNum = to_string(N); 5 // flag用来标记赋值9从哪里开始 6 // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行 7 int flag = strNum.size(); 8 for (int i = strNum.size() - 1; i > 0; i--) { 9 if (strNum[i - 1] > strNum[i] ) { 10 flag = i; 11 strNum[i - 1]--; 12 } 13 } 14 for (int i = flag; i < strNum.size(); i++) { 15 strNum[i] = '9'; 16 } 17 return stoi(strNum); 18 } 19 };
17.买卖股票的最佳时机——含手续费
17.1.问题描述
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee
17.2.要点
这题还是不要用贪心法来求解,用dp求解会更方便些。
17.3.代码实例
18.监控二叉树
18.1.问题描述
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
链接:https://leetcode.cn/problems/binary-tree-cameras/submissions/
18.2.要点
贪心策略:查看四周环境,能不放就不放。
定义节点被监控的状态机,按规则判断当前节点是否应该添加监控。
从下向上遍历,因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。因此采用后序遍历。
18.3.代码实例

1 class Solution { 2 public: 3 const int unCover=1,covered=2,hasMon=3; 4 int ret=0; 5 int recurve(TreeNode* root){ 6 if(root==NULL){ 7 return covered; 8 } 9 10 //后序遍历 11 int left=recurve(root->left); 12 int right=recurve(root->right); 13 14 //左右存在未覆盖,此处必须添加 15 if(left==unCover||right==unCover){ 16 ret++; 17 return hasMon; 18 } 19 20 //左右都是已覆盖状态,此处可以暂时是未覆盖状态 21 if(left==covered&&right==covered){ 22 return unCover; 23 } 24 25 //左右节点存在至少一个摄像头,根节点处于被覆盖状态 26 return covered; 27 } 28 29 int minCameraCover(TreeNode* root) { 30 if(root==NULL){ 31 return 0; 32 } 33 if(recurve(root)==unCover){ 34 ret++; 35 } 36 37 return ret; 38 } 39 };
xxx.问题
xxx.1.问题描述
111
xxx.2.要点
222
xxx.3.代码实例
333
本文作者:OhOfCourse
本文链接:https://www.cnblogs.com/OhOfCourse/p/17004480.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步