LeetCode Hot100 简明题解

LeetCode Hot100

温故而知新

哈希

1. 两数之和
枚举-查询-添加

for(int i = 0; i < n; i++){
    if(map.contains(target - nums[i])){
        return {map[target - nums[i]], i};
    }
    map[nums[i]] = i;
}

49. 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

枚举每个字符串,排序后作为同构词的Key

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

O(n)做法

int longestConsecutive(vector<int>& nums) {
    unordered_set<int> set(nums.begin(), nums.end());
    int ans = 0;
    for(auto v : set){
        if(set.contains(v - 1)) continue;
        int u = v;
        while(set.contains(u)) u++;
        ans = max(ans, u - v);
    }
    return ans;
}

双指针

283. 移动零
学吧

void moveZeroes(vector<int>& nums) {
    int L = 0;
    for(auto& v : nums) if(v) swap(v, nums[L++]);
}

11. 盛最多水的容器
通解:高度排序,枚举,更新左右最远可达位置,计算更新ans
双指针做法:

int maxArea(vector<int>& height) {
    int ans = 0, L = 0, R = height.size() - 1;
    while(L < R){
        ans = max(ans, min(height[L], height[R]) * (R - L));
        height[L] < height[R] ? L++ : R--;
    }
    return ans;
}

15. 三数之和

vector<vector<int>> threeSum(vector<int>& nums) {
  int n = nums.size();
  sort(nums.begin(), nums.end());
  vector<vector<int>> ans;
  for(int i = 0; i < n; i++){
      if(i > 0 && nums[i] == nums[i - 1]) continue; // i去重
      int k = n - 1; //j k同维
      for(int j = i + 1; j < n; j++)
      {
          if(j > i + 1 && nums[j] == nums[j - 1]) continue; // j去重
          while(k > j && (k < n - 1 && nums[k] == nums[k + 1])) k--; // k去重
          while(k > j && (nums[i] + nums[j] + nums[k] > 0)) k--; // > 0 的排除
          //合并写法
          //while(k > j && ((nums[i] + nums[j] + nums[k] > 0) || (k < n - 1 && nums[k] == nums[k + 1]))) k--;
          if(k > j && nums[i] + nums[j] + nums[k] == 0) ans.push_back({nums[i], nums[j], nums[k]});
      }
  }
  return ans;
}

42. 接雨水
计算的方式是 每个格子竖直方向最多接多高的雨水,即两侧最大值的较小者-当前格子高度,有点类似积分。
前后缀分解

int trap(vector<int>& height) {
    int ans = 0, n = height.size();
    vector<int> left(n), right(n);
    left[0] = height[0], right[n - 1] = height[n - 1];
    for(int i = 1; i < n; i++) left[i] = max(left[i - 1], height[i]);
    for(int i = n - 2; i >= 0; i--) right[i] = max(right[i + 1], height[i]);
    for(int i = 1; i < n - 1; i++) ans += min(left[i], right[i]) - height[i];
    return ans;
}

同向双指针

int trap(vector<int>& height) {
    int ans = 0, L = 0, R = height.size() - 1, mxL = 0, mxR = 0;
    while(L < R){
        mxL = max(mxL, height[L]);
        mxR = max(mxR, height[R]);
        ans += mxL < mxR ? mxL - height[L++] : mxR - height[R--];
    }
    return ans;
}

另一种计算方式是,累加每个水平方向的连续块
单调栈

int trap(vector<int>& height) {
    int ans = 0, n = height.size();
    stack<int> st;
    for(int i = 0; i < n; i++){
        int mxR = height[i];
        while(!st.empty() && mxR >= height[st.top()]){
            int bottom = height[st.top()];
            st.pop();
            if(!st.empty()){
                int mxL = height[st.top()];
                //(i - st.top() - 1) 水平方向宽度
                ans += (min(mxL, mxR) - bottom) * (i - st.top() - 1);
            }
        }
        st.push(i);
    }
    return ans;
}

滑动窗口

3. 无重复字符的最长子串

  • 枚举左端点
int lengthOfLongestSubstring(string s) {
    int ans = 0, n = s.size(), vis[128];
    for(int i = 0, j = 0; i < n; i++){
        while(j < n && !vis[s[j]]) vis[s[j++]]++;
        ans = max(ans, j - i);
        vis[s[i]]--;
    }
    return ans;
}
  • 枚举右端点
int lengthOfLongestSubstring(string s) {
    int ans = 0, n = s.size(), vis[128];
    for(int i = 0, j = 0; j < n; j++){
        vis[s[j]]++;
        while(vis[s[j]] > 1) vis[s[i++]]--;
        ans = max(ans, j - i + 1);
    }
    return ans;
}

438. 找到字符串中所有字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

不要O(26n),要O(n)。触发式修改差异数量。

vector<int> findAnagrams(string s, string p) {
    int n = s.size(), m = p.size();
    vector<int> ans;
    int count[26]{0};
    for(int i = 0; i < m; i++) count[p[i] - 'a']++;
    int diff = 0;
    for(int k = 0; k < 26; k++) if(count[k] > 0) diff++;
    for(int i = 0; i < n; i++){
        int v = s[i] - 'a';
        if(count[v] == 0) diff++;
        count[v]--;
        if(count[v] == 0) diff--;
        if(i >= m - 1){
            int L = i - m + 1;
            if(diff == 0) ans.push_back(L);
            int u = s[L] - 'a';
            if(count[u] == 0) diff++;
            count[u]++;
            if(count[u] == 0) diff--;
        }
    }
    return ans;
}

560. 和为 K 的子数组

int subarraySum(vector<int>& nums, int k) {
    int ans = 0, sum = 0;
    unordered_map<int, int> count;
    count[0] = 1;
    for(auto v : nums){
        sum += v;
        ans += count[sum - k]; //前缀和 sum-pre=k ->pre=sum-k
        count[sum]++;
    }
    return ans;
}

239. 滑动窗口最大值
单调队列
队列保持递减(单调不增),则左侧第一个即当前结果,左侧出k窗口的退出。

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    int n = nums.size();
    deque<int> deq;
    vector<int> ans; //size = n - k + 1;
    for(int i = 0; i < n; i++){
        int v = nums[i];
        while(!deq.empty() && v >= nums[deq.back()]) deq.pop_back();
        deq.push_back(i);
        if(i >= k - 1){
            ans.push_back(nums[deq.front()]);
            if(deq.front() == i - k + 1) deq.pop_front();
        }
    }
    return ans;
}

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

核心思路同 438. 找到字符串中所有字母异位词

string minWindow(string s, string t) {
    int n = s.size(), m = t.size();
    int count[128];
    for(auto v : t) count[v]++;
    int diff = 0;
    for(auto v : count) if(v) diff++;
    int L = -1, R = n;
    for(int i = 0, j = 0; i < n; i++){
        while(j < n && diff){
            count[s[j]]--;
            if(count[s[j]] == 0) diff--;
            j++;
        }
        if(diff > 0) break;
        if(j - i < R - L){
            L = i; R = j;
        }
        if(count[s[i]] == 0) diff++;
        count[s[i]]++;
    }
    return L == -1 ? "" : s.substr(L, R - L);
}

数组

53. 最大子数组和

int maxSubArray(vector<int>& nums) {
    int ans = INT_MIN, sum = 0; //有负值 ans起始MIN
    for(auto v : nums){
        sum += v;
        ans = max(ans, sum);
        if(sum < 0) sum = 0;
    }
    return ans;
}

56. 合并区间

vector<vector<int>> merge(vector<vector<int>>& intervals) {
    vector<vector<int>> ans;
    sort(intervals.begin(), intervals.end());
    for(auto v : intervals){
        if(ans.empty() || v[0] > ans.back()[1]) ans.push_back(v);
        else ans.back()[1] = max(ans.back()[1], v[1]); //取最大值
    }
    return ans;
}

189. 轮转数组
重点是空间复杂度O(1)的做法
模拟复制

nums2[(i + k) % n] = nums[i];

gcd

void rotate(vector<int>& nums, int k) {
    int n = nums.size();
    int gcd = __gcd(n, k);
    for(int p = 0; p < gcd; p++){
        int i = p, t = n / gcd, left = nums[p];
        while(t-- > 0){
            int j = (i + k) % n;
            swap(nums[j], left);
            i = j;
        }
    }
}

翻转数组

void rotate(vector<int>& nums, int k) {
    int n = nums.size();
    auto reverse = [&](int L, int R)->void{ while(L < R) swap(nums[L++], nums[R--]); };
    reverse(0, n - 1);
    reverse(0, k % n - 1);
    reverse(k % n, n - 1);
}

238. 除自身以外数组的乘积
前后缀分解

41. 缺失的第一个正数
置换

int firstMissingPositive(vector<int>& nums) {
    int n = nums.size();
    for(int i = 0; i < n; i++){
        while(nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i])
            swap(nums[nums[i] - 1], nums[i]);
    }
    for(int i = 0; i < n; i++) if(nums[i] != i + 1) return i + 1;
    return n + 1;
}

矩阵

73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。

感觉并不是什么很好的题目,这个要求的核心思想在于:
用矩阵的第一行和第一列存储每一列和每一行是否有0。
先记录原本第一行第一列是否有0,然后先二维枚举一遍,把行列是否有0的信息存到第一行第一列里。
如果某一行出现了0,则第一列的该行本身就会变为0,所以数据丢失也不会有问题。
最后处理第一行第一列是否需要置零。

54. 螺旋矩阵

vector<int> spiralOrder(vector<vector<int>>& matrix) {
    int n = matrix.size(), m = matrix[0].size();
    vector<int> ans;
    int top = 0, bottom = n - 1, left = 0, right = m - 1;
    int x = 0, y = 0;
    while(true){
        while(y <= right) ans.push_back(matrix[x][y++]);
        if(top == bottom) break;
        y--; x++; top++;
        while(x <= bottom) ans.push_back(matrix[x++][y]);
        if(left == right) break;
        x--; y--; right--;
        while(y >= left) ans.push_back(matrix[x][y--]);
        if(top == bottom) break;
        y++; x--; bottom--;
        while(x >= top) ans.push_back(matrix[x--][y]);
        if(left == right) break;
        x++; y++; left++;
    }
    return ans;
}

48. 旋转图像

顺时针旋转90度=先主对角线翻转再左右翻转=先上下翻转再主对角线翻转

240. 搜索二维矩阵 II
右上角折线搜索 本质是以右上(或左下)为根的BST

bool searchMatrix(vector<vector<int>>& matrix, int target) {
    int n = matrix.size(), m = matrix[0].size();
    int x = 0, y = m - 1;
    while(x < n && y >= 0){
        int v = matrix[x][y];
        if(v == target) return true;
        if(v < target) x++;
        else y--;
    }
    return false;
}

链表

160. 相交链表
空间O(1)的做法

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    auto pa = headA, pb = headB;
    while(pa != pb){
        //每次走一步 走到当前链表的末尾之后的空节点
        pa = pa ? pa->next : headB;
        pb = pb ? pb->next : headA;
    }
    return pa;
}

206. 反转链表

ListNode* reverseList(ListNode* head) {
    ListNode* pre = nullptr;
    while(head != nullptr){
        //循环置换 (head, head.next, pre)=(head.next, pre, head);

        /*
        auto pt = head;
        head = head->next;
        pt->next = pre;
        pre = pt;
        */

        /*
        auto pt = head->next;
        head->next = pre;
        pre = head;
        head = pt;
        */

        auto pt = pre;
        pre = head;
        head = head->next;
        pre->next = pt;
    }
    return pre;
}

234. 回文链表
递归写法

bool isPalindrome(ListNode* head) {
    auto left = head;
    auto dfs = [&](auto&& self, ListNode* cur)->bool {
        if(cur == nullptr) return true;
        if(!self(self, cur->next)) return false;
        if(left->val != cur->val) return false;
        left = left->next;
        return true;
    };
    return dfs(dfs, head);
}

空间复杂度O(1)做法:快慢指针找中点,反转后半部分,判定回文,恢复链表。

141. 环形链表
快慢指针

bool hasCycle(ListNode *head) {
    auto slow = head, fast = head;
    while(slow != nullptr && fast != nullptr && fast->next != nullptr){
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast) return true;
    }
    return false;
}

142. 环形链表 II

ListNode *detectCycle(ListNode *head) {
    auto slow = head, fast = head;
    while(true){
        if(fast == nullptr || fast->next == nullptr) return nullptr;
        slow = slow->next;
        fast = fast->next->next; 
        if(slow == fast) break;
    }
    slow = head;
    while(slow != fast) slow = slow->next, fast = fast->next;
    return slow;
}

21. 合并两个有序链表
递归写法

ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
    if(list1 == nullptr) return list2;
    if(list2 == nullptr) return list1;
    if(list1->val <= list2->val){
        list1->next = mergeTwoLists(list1->next, list2);
        return list1;
    }
    else{
        list2->next = mergeTwoLists(list2->next, list1);
        return list2;
    }
}

2. 两数相加

ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
    int add = 0;
    ListNode* head = new ListNode();
    auto node = head;
    while(l1 || l2){
        int total = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + add;
        node->next = new ListNode(total % 10);
        add = total / 10;
        if(l1) l1 = l1->next;
        if(l2) l2 = l2->next;
        node = node->next;
    }
    if(add > 0) node->next = new ListNode(add); //重要
    return head->next;
}

19. 删除链表的倒数第 N 个结点
头结点可能会为空 使用dummy节点方便。

ListNode* removeNthFromEnd(ListNode* head, int n) {
    auto dummy = new ListNode(0, head);
    auto slow = dummy, fast = dummy;
    while(n--) fast = fast->next;
    while(fast->next) slow = slow->next, fast = fast->next;
    slow->next = slow->next->next;
    return dummy->next;
}

24. 两两交换链表中的节点
递归做法

ListNode* swapPairs(ListNode* head) {
    if(!head || !head->next) return head;
    auto res = head->next;
    head->next = swapPairs(head->next->next);
    res->next = head;
    return res;
}

25. K 个一组翻转链表

ListNode* reverseKGroup(ListNode* head, int k) {
    ListNode dummy;
    int count = 0;
    auto Check = [&](ListNode* cur)->bool {
        auto pt = cur;
        int count = k;
        while(pt && count){
            pt = pt->next;
            count--;
        }
        return count == 0;
    };
    auto Reverse = [&](ListNode* cur)->vector<ListNode*> {
        count = 0;
        ListNode* pre = nullptr;
        ListNode* next = nullptr;
        vector<ListNode*> res(2);
        while(cur && count < k){
            next = cur->next;
            auto tmp = cur;
            cur = cur->next;
            tmp->next = pre;
            pre = tmp;
            count++;
        }
        return {pre, next};
    };
    auto pre = &dummy;
    while(Check(head)){
        auto tmp = head;
        auto res = Reverse(head);
        pre->next = res[0];
        head = res[1];
        pre = tmp;
    }
    pre->next = head;
    return dummy.next;
}

138. 随机链表的复制

unordered_map<Node*, Node*> cachedNode;
    Node* copyRandomList(Node* head) {
    if (head == nullptr) {
        return nullptr;
    }
    if (!cachedNode.count(head)) {
        Node* headNew = new Node(head->val);
        cachedNode[head] = headNew;
        headNew->next = copyRandomList(head->next);
        headNew->random = copyRandomList(head->random);
    }
    return cachedNode[head];
}

另外有一种修改原链表的处理方式,逐个插入

148. 排序链表
归并排序
分治
O(logn)空间复杂度
迭代
O(1)空间复杂度

23. 合并 K 个升序链表
标准优先队列做法。
注意C++优先队列自定义元素类型,自定义比较函数写法。
特别注意less对应大根堆,greater对应小根堆。

struct compare{
    bool operator() (const ListNode* A, const ListNode* B){
        return A->val > B->val;
    }
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
    ListNode dummy;
    priority_queue<ListNode*, vector<ListNode*>, compare> pq;
    for(auto node : lists) if(node) pq.push(node);
    auto node = &dummy;
    while(!pq.empty()){
        auto top = pq.top(); pq.pop();
        node->next = new ListNode(top->val);
        node = node->next;
        if(top->next) pq.push(top->next);
    }
    return dummy.next;
}

分治

146. LRU 缓存
哈希表+双向链表

二叉树

94. 二叉树的中序遍历
递归
迭代

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> ans;
    stack<TreeNode*> st;
    while(!st.empty() || root != nullptr){
        while(root != nullptr){
            st.push(root);
            root = root->left;
        }
        root = st.top();
        st.pop();
        ans.push_back(root->val);
        root = root->right;
    }
    return ans;
}

Morris遍历
104. 二叉树的最大深度
自底向上

int maxDepth(TreeNode* root) {
    if(root == nullptr) return 0;
    return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}

自顶向下

int maxDepth(TreeNode* root) {
    int ans = 0;
    auto dfs = [&](auto&& self, TreeNode* node, int depth)->void{
        if(node == nullptr){
            ans = max(ans, depth);
            return;
        }
        self(self, node->left, depth + 1);
        self(self, node->right, depth + 1);
    };
    dfs(dfs, root, 0);
    return ans;
}

226. 翻转二叉树
自底向上

TreeNode* invertTree(TreeNode* root) {
    if(root == nullptr) return root;
    auto left = invertTree(root->left);
    auto right = invertTree(root->right);
    root->left = right;
    root->right = left;
    return root;
}

自顶向下

TreeNode* invertTree(TreeNode* root) {
    if(root == nullptr) return root;
    swap(root->left, root->right);
    invertTree(root->left);
    invertTree(root->right);
    return root;
}

101. 对称二叉树

bool isSymmetric(TreeNode* root) {
    auto dfs = [&](auto&& self, TreeNode* L, TreeNode* R)->bool {
        if(L == nullptr && R == nullptr) return true;
        if(L == nullptr || R == nullptr) return false;
        if(L->val != R->val) return false;
        return self(self, L->right, R->left) && self(self, L->left, R->right);
    };
    return dfs(dfs, root->left, root->right);  
}

543. 二叉树的直径
树形dp

int diameterOfBinaryTree(TreeNode* root) {
    int ans = 0;
    auto dfs = [&](auto&& self, TreeNode* node)->int {
        if(node == nullptr) return 0;
        int L = self(self, node->left);
        int R = self(self, node->right);
        ans = max(ans, L + R);
        return max(L, R) + 1;
    };
    dfs(dfs, root);
    return ans;
}

另一种是两次dfs/bfs求解
102. 二叉树的层序遍历
BFS基本应用
108. 将有序数组转换为二叉搜索树

TreeNode* sortedArrayToBST(vector<int>& nums) {
    auto dfs = [&](auto&& self, int L, int R)->TreeNode* {
        if(L > R) return nullptr;
        int mid = L + (R - L) / 2;
        TreeNode* node = new TreeNode(nums[mid]);
        node->left = self(self, L, mid - 1);
        node->right = self(self, mid + 1, R);
        return node;
    };
    return dfs(dfs, 0, nums.size() - 1);
}

98. 验证二叉搜索树

bool isValidBST(TreeNode* root) {
    auto dfs = [&](auto&& self, TreeNode* node, i64 minVal, i64 maxVal)->bool{
        if(node == nullptr) return true;
        if(node->val >= maxVal || node->val <= minVal) return false;
        return self(self, node->left, minVal, min(maxVal, (i64)node->val))
        && self(self, node->right, max(minVal, (i64)node->val), maxVal);
    };
    return dfs(dfs, root, LONG_MIN, LONG_MAX);
}

另一种可以中序遍历 检测是否有序
230. 二叉搜索树中第 K 小的元素
中序遍历二叉搜索树的结果是有序列表

int kthSmallest(TreeNode* root, int k) {
    int ans = 0;
    auto dfs = [&](auto&& self, TreeNode* node){
        if(!node) return;
        self(self, node->left);
        if(--k == 0) ans = node->val;
        self(self, node->right);
    };
    dfs(dfs, root);
    return ans;
}

记录子树size可以快速多次查询
如果有增删操作 需要用平衡树
199. 二叉树的右视图

vector<int> rightSideView(TreeNode* root) {
    vector<int> ans;
    auto dfs = [&](this auto&& dfs, TreeNode* node, int depth) -> void {
        if (node == nullptr) {
            return;
        }
        if (depth == ans.size()) { // 这个深度首次遇到
            ans.push_back(node->val);
        }
        dfs(node->right, depth + 1); // 先递归右子树,保证首次遇到的一定是最右边的节点
        dfs(node->left, depth + 1);
    };
    dfs(root, 0);
    return ans;
}

114. 二叉树展开为链表

void flatten(TreeNode* root) {
    auto dfs = [&](auto&& self, TreeNode* node)->TreeNode* {
        auto left = node->left;
        auto right = node->right;
        node->left = nullptr;
        if(left){
            node->right = left;
            node = self(self, left);
        }
        if(right){
            node->right = right;
            node = self(self, right);
        }
        return node;
    };
    if(root) dfs(dfs, root);
}

105. 从前序与中序遍历序列构造二叉树

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    auto dfs = [&](auto&& self, int L1, int R1, int L2, int R2)->TreeNode* {
        if(L1 > R1) return nullptr;
        auto node = new TreeNode(preorder[L1]);
        int t = -1;
        for(int t = L2; t <= R2; t++) if(inorder[t] == preorder[L1]) break;
        node->left = self(self, L1 + 1, L1 + t - L2, L2, t - 1); //取相同长度 t - L2 + 1
        node->right = self(self, L1 + t - L2 + 1, R1, t + 1, R2);
        return node;
    };
    return dfs(dfs, 0, preorder.size() - 1, 0, inorder.size() - 1);
}

437. 路径总和 III

int pathSum(TreeNode* root, int targetSum) {
    unordered_map<long long, int> dict{{0, 1}};
    int ans = 0;
    auto dfs = [&](auto&& self, TreeNode* node, long long sum)->void{
        sum += node->val;
        ans += dict[sum - targetSum];
        dict[sum]++;
        if(node->left) self(self, node->left, sum);
        if(node->right) self(self, node->right, sum);
        dict[sum]--;
        sum -= node->val;
    };
    if(root) dfs(dfs, root, 0);
    return ans;
}

236. 二叉树的最近公共祖先
一个很巧妙的写法

//fa表示父节点映射
auto p2 = p, q2 = q;
    while(p2 != q2){
    p2 = fa[p2];
    if(p2 == nullptr) p2 = q;
    q2 = fa[q2];
    if(q2 == nullptr) q2 = p;
}
return p2;

纯递归写法

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    if(root == nullptr || root == p || root == q) return root;
    auto left = lowestCommonAncestor(root->left, p, q);
    auto right = lowestCommonAncestor(root->right, p, q);
    return left && right ? root : left ? left : right;
}

124. 二叉树中的最大路径和
简单树形dp

int maxPathSum(TreeNode* root) {
    int ans = INT_MIN;
    auto dfs = [&](auto&& self, TreeNode* node)->int{
        int left = 0, right = 0;
        if(node->left) left = self(self, node->left);
        if(node->right) right = self(self, node->right);
        ans = max(ans, node->val + left + right);
        return max(0, node->val + max(left, right));
    };
    dfs(dfs, root);
    return ans;
}

图论

200. 岛屿数量
矩阵DFS基本

int POS_X[4] = {-1, 1, 0, 0};
int POS_Y[4] = {0, 0, -1, 1};
int numIslands(vector<vector<char>>& grid) {
    int ans = 0;
    int n = grid.size(), m = grid[0].size();
    auto dfs = [&](auto&& self, int x, int y)->void {
        grid[x][y] = '0';
        for(int k = 0; k < 4; k++){
            int x2 = x + POS_X[k];
            int y2 = y + POS_Y[k];
            if(x2 >= 0 && x2 < n && y2 >= 0 && y2 < m && grid[x2][y2] == '1'){
                self(self, x2, y2);
            }
        }
    };
    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++){
            if(grid[i][j] == '1'){
                dfs(dfs, i, j);
                ans++;
            }
        }
    }
    return ans;
}

994. 腐烂的橘子
多源BFS

int POS_X[4] = {-1, 1, 0, 0};
int POS_Y[4] = {0, 0, -1, 1};
int orangesRotting(vector<vector<int>>& grid) {
    int n = grid.size(), m = grid[0].size();
    int total = 0;
    queue<int*> que;
    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++){
            if(grid[i][j] == 1) total++;
            else if(grid[i][j] == 2) que.push(new int[2]{i, j});
        }
    }
    if(!total) return 0; //小心
    int ans = 0;
    while(!que.empty()){
        int count = que.size();
        while(count--){
            auto v = que.front(); que.pop();
            for(int k = 0; k < 4; k++){
                int x = v[0] + POS_X[k];
                int y = v[1] + POS_Y[k];
                if(x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1){
                    que.push(new int[2]{x, y});
                    total--;
                    grid[x][y] = 0;
                }
            }
        }
        ans++;
    }
    return total ? -1 : ans - 1;
}

207. 课程表
拓扑排序/判环

bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
    vector<vector<int>> g(numCourses);
    vector<int> deg(numCourses);
    for(auto& e : prerequisites){
        g[e[1]].push_back(e[0]);
        deg[e[0]]++;
    }
    queue<int> que;
    for(int i = 0; i < numCourses; i++) if(!deg[i]) que.push(i);
    while(!que.empty()){
        numCourses--;
        int u = que.front(); que.pop();
        for(auto v : g[u]){
            deg[v]--;
            if(!deg[v]) que.push(v);
        }
    }
    return numCourses == 0;
}

208. 实现 Trie (前缀树)
Node节点的属性包括:
后续节点的引用vector<Node*>
当前节点是否是结束节点。

回溯

46. 全排列
暴力选择

vector<vector<int>> permute(vector<int>& nums) {
    int n = nums.size();
    vector<vector<int>> ans;
    vector<int> p;
    vector<int> vis(n);
    auto dfs = [&](auto&& self, int cur)->void{
        if(cur == n) ans.push_back(p);
        for(int i = 0; i < n; i++){
            if(!vis[i]){
                p.push_back(nums[i]);
                vis[i] = 1;
                self(self, cur + 1);
                vis[i] = 0;
                p.pop_back();
            }
        }
    };
    dfs(dfs, 0);
    return ans;
}

获取下一个排列

bool NextPermutation(vector<int>& p){
    int n = p.size(), t = -1;
    for (int i = 0; i < n - 1; i++) if (p[i + 1] - p[i] > 0) t = i;
    if (t == -1) return false;
    for (int i = n - 1; ; i--)
    {
        if (p[i] > p[t])
        {
            swap(p[i], p[t]);
            break;
        }
    }
    sort(p.begin() + t + 1, p.end());
    return true;
}
vector<vector<int>> permute(vector<int>& nums) {
    int n = nums.size();
    vector<vector<int>> ans;
    vector<int> index(n);
    for(int i = 0; i < n; i++) index[i] = i;
    do{
        vector<int> p;
        for(auto v : index) p.push_back(nums[v]);
        ans.push_back(p);
    }
    while(NextPermutation(index));
    return ans;
}

78. 子集

vector<vector<int>> subsets(vector<int>& nums) {
    int n = nums.size();
    vector<vector<int>> ans(1 << n);
    for(int mask = 0; mask < (1 << n); mask++){
        for(int i = 0; i < n; i++) {
            if((mask >> i) & 1) ans[mask].push_back(nums[i]);
        }
    }
    return ans;
}

17. 电话号码的字母组合
就是回溯

vector<string> letterCombinations(string digits) {
    vector<vector<char>> map = {
      {'a', 'b', 'c'},
      {'d', 'e', 'f'},
      {'g', 'h', 'i'},
      {'j', 'k', 'l'},
      {'m', 'n', 'o'},
      {'p', 'q', 'r', 's'},
      {'t', 'u', 'v'},
      {'w', 'x', 'y', 'z'}
  };
  vector<string> ans;
  if(digits == "") return ans;
  string s;
  auto dfs = [&](auto&& self, int cur)->void{
      if(cur == digits.size()){
          ans.push_back(s);
          return;
      }
      auto vec = map[digits[cur] - '2'];
      for(auto v : vec){
          s.push_back(v);
          self(self, cur + 1);
          s.pop_back();
      }
  };
  dfs(dfs, 0);
  return ans;
}

39. 组合总和

选与不选

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> ans;
    vector<int> p;
    auto dfs = [&](auto&& self, int cur, int sum)->void{
        if(cur == candidates.size()){
            if(sum == target){
                ans.push_back(p);
            }
            return;
        }
        self(self, cur + 1, sum);
        int v = candidates[cur];
        if(target - sum >= v){
            p.push_back(v);
            self(self, cur, sum + v);
            p.pop_back();
        }
    };
    dfs(dfs, 0, 0);
    return ans;
}

枚举选哪一个

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> ans;
    vector<int> p;
    auto dfs = [&](auto&& self, int cur, int sum)->void{
        if(sum == target){
            ans.push_back(p);
            return;
        }
        for(int i = cur; i < candidates.size(); i++){
            int v = candidates[i];
            if(sum + v <= target){
                p.push_back(v);
                self(self, i, sum + v);
                p.pop_back();
            }
        }
    };
    dfs(dfs, 0, 0);
    return ans;
}

22. 括号生成
枚举子集
选左括号还是右括号

vector<string> generateParenthesis(int n) {
    int m = n * 2;
    int lc = 0;
    string s;
    vector<string> ans;
    auto dfs = [&](auto&& self, int i)->void{
        if(i == m){
            if(lc == 0) ans.push_back(s);
            return;
        }
        lc++; s.push_back('(');
        self(self, i + 1);
        s.pop_back(); lc--;
        if(lc > 0){
            lc--; s.push_back(')');
            self(self, i + 1);
            s.pop_back(); lc++;
        }
    };
    dfs(dfs, 0);
    return ans;
}

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

这个凭啥进top100

对 凭啥进top100

131. 分割回文串

vector<vector<string>> partition(string s) {
    vector<vector<string>> ans;
    vector<string> p;
    auto check = [&](int L, int R)->bool{
        while(L < R){
            if(s[L] != s[R]) return false;
            L++; R--;
        }
        return true;
    };
    auto dfs = [&](auto&& self, int i)->void{
        if(i == s.size()) {
            ans.push_back(p);
            return;
        }
        for(int j = i; j < s.size(); j++){
            if(check(i, j)){
                p.push_back(s.substr(i, j - i + 1));
                self(self, j + 1);
                p.pop_back();
            }
        }
    };
    dfs(dfs, 0);
    return ans;
}

51. N 皇后

二分

35. 搜索插入位置

int searchInsert(vector<int>& nums, int target) {
    int low = 0, high = nums.size() - 1;
    while(low <= high){
        int mid = low + (high - low) / 2;
        if(nums[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return low; //low最终是>=target的第一个索引
}

74. 搜索二维矩阵
两次二分O(logn+logm)
一次二分O(log(nm))
右上角搜索O(n+m)

34. 在排序数组中查找元素的第一个和最后一个位置

33. 搜索旋转排序数组

int search(vector<int>& nums, int target) {
    int n = nums.size();
    int low = 0, high = n - 1;
    while(low <= high){
        int mid = low + (high - low) / 2;
        int v = nums[mid];
        if(v == target) return mid; //特殊
        if(v < target){
            if(v > nums[n - 1]) low = mid + 1;
            else {
                if(target > nums[n - 1]) high = mid - 1;
                else low = mid + 1;
            }
        }
        else{
            if(v < nums[n - 1]) high = mid - 1;
            else{
                if(target > nums[n - 1]) high = mid - 1;
                else low = mid + 1;
            }
        }
    }
    return (low < n && nums[low] == target) ? low : -1;
}

153. 寻找旋转排序数组中的最小值
闭区间的意义

int findMin(vector<int>& nums) {
    int low = 0, high = nums.size() - 1;
    while(low < high){
        int mid = low + (high - low) / 2;
        if(nums[mid] > nums[high]) low = mid + 1;
        else high = mid;
    }
    return nums[low];
}

4. 寻找两个正序数组的中位数

新手梦魇

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    if(nums1.size() > nums2.size()) swap(nums1, nums2);
    int n = nums1.size(), m = nums2.size();
    int low = 0, high = n;
    while(low <= high){
        int mid = low + (high - low) / 2;
        int j = (n + m - 1) / 2 - mid;
        if(mid == 0 || j == m - 1 || nums1[mid - 1] <= nums2[j + 1]) low = mid + 1;
        else high = mid - 1;
    }
    int L1 = high, L2 = (n + m - 1) / 2 - L1;
    int maxL = max(L1 == 0 ? INT_MIN : nums1[L1 - 1], L2 == -1 ? INT_MIN : nums2[L2]);
    int minR = L1 == n ? (L2 == m - 1 ? INT_MAX : nums2[L2 + 1]) : min(nums1[L1], L2 == m - 1 ? INT_MAX : nums2[L2 + 1]);
    return (n + m) % 2 == 0 ? (maxL + minR) / 2.0 : maxL;
}

20. 有效的括号

bool isValid(string s) {
    unordered_map<char, char> dict{{')', '('}, {']', '['}, {'}', '{'}};
    stack<char> st;
    for(auto v : s){
        if(dict.contains(v)){
            if(st.empty() || st.top() != dict[v]) return false;
            st.pop();
        }
        else st.push(v);
    }
    return st.empty();
}

155. 最小栈
前缀最小值

class MinStack {
public:
    stack<pair<int, int>> st;
    MinStack() {
        st.push({0, INT_MAX});   
    }
    
    void push(int val) {
        st.push({val, min(st.top().second, val)});
    }
    
    void pop() {
        st.pop();
    }
    
    int top() {
        return st.top().first;
    }
    
    int getMin() {
        return st.top().second;
    }
};

394. 字符串解码
栈或递归 有些麻烦

//递归写法
string decodeString(string s) {
    auto dfs = [&](auto&& self, int& i)->string {
        string res;
        while(i < s.size()){ //s可能是多个词组并列组成
            while(i < s.size() && s[i] >= 'a' && s[i] <= 'z') res.push_back(s[i++]);
            if(s[i] == ']'){ //递归出口 上层词组的末尾
                i++;
                return res;
            }

            //3[abc]
            int count = 0;
            while(s[i] >= '0' && s[i] <= '9') count = count * 10 + s[i++] - '0';
            string child = self(self, ++i); //由于可以嵌套 这里必然是递归调用
            for(int k = 0; k < count; k++) res += child;
        }
        return res;
    };
    int i = 0;
    return dfs(dfs, i);
}

739. 每日温度
单调栈

vector<int> dailyTemperatures(vector<int>& temperatures) {
    int n = temperatures.size();
    vector<int> ans(n);
    stack<int> st;
    for(int i = 0; i < n; i++){
        int v = temperatures[i];
        while(st.size() && temperatures[st.top()] < v){
            ans[st.top()] = i - st.top();
            st.pop();
        }
        st.push(i);
    }
    return ans;
}

84. 柱状图中最大的矩形
单调栈
可以遍历两次,可以倒序枚举

int largestRectangleArea(vector<int>& heights) {
    int n = heights.size();
    vector<int> mxL(n), mxR(n, n - 1);
    stack<int> st; //递增栈。 如果是递减栈 则较小的值完全可以取代左侧的较大值。
    //计算每个位置i的向左最远位置和向右最远位置
    for(int i = 0; i < n; i++){
        int v = heights[i];
        while(!st.empty() && heights[st.top()] > v){
            mxR[st.top()] = i - 1; //出栈时,栈顶的极右值就是i - 1
            st.pop();
        }
        mxL[i] = st.empty() ? 0 : st.top() + 1; //出栈结束后 当前i的极左值就是栈顶 + 1
        st.push(i);
    }
    int ans = 0;
    for(int i = 0; i < n; i++){
        ans = max(ans, (mxR[i] - mxL[i] + 1) * heights[i]);
    }
    return ans;
}

215. 数组中的第K个最大元素
347. 前 K 个高频元素
-快速选择
-堆的实现
295. 数据流的中位数
对顶堆的基本实现,切记每次添加数据 必须对两堆做一次平衡

class MedianFinder {
public:
    //最小堆 存储较大的一半 + 1
    priority_queue<double, vector<double>, greater<double>> pqL;
    //最大堆 存储较小的一半 - 1
    priority_queue<double> pqR;
    MedianFinder() {
        
    }
    
    void addNum(int num) {
        //放入最小堆 然后把最小堆的最小值 放到最大堆
        pqL.push(num); pqR.push(pqL.top()); pqL.pop();
        //如果最大堆个数超过最小堆 把最大堆的最大值 放回最小堆
        if(pqR.size() > pqL.size()){
            pqL.push(pqR.top()); pqR.pop();
        } 
    }
    
    double findMedian() {
        return pqL.size() == pqR.size() ? (pqL.top() + pqR.top()) / 2 : pqL.top();
    }
};

贪心

121. 买卖股票的最佳时机

int maxProfit(vector<int>& prices) {
    int ans = 0, minLeft = prices[0];
    for(auto v : prices){
        ans = max(ans, v - minLeft);
        minLeft = min(minLeft, v);
    }
    return ans;
}

55. 跳跃游戏
O(n)贪心解

45. 跳跃游戏 II
O(n2)

bool canJump(vector<int>& nums) {
    int n = nums.size();
    int mx = 0;
    for(int i = 0; i < n; i++){
        if(i > mx) return false; //关键
        mx = max(mx, i + nums[i]);
    }
    return true;
}

O(n)贪心解

763. 划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

记录每个字母出现的最右端位置

动态规划

70. 爬楼梯
多解

118. 杨辉三角

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

198. 打家劫舍
a表示不选当前元素的最大值,b表选择当前元素的最大值

int rob(vector<int>& nums) {
    int a = 0, b = 0;
    for(auto v : nums){
        int c = a + v;
        a = max(a, b);
        b = c;
    }
    return max(a, b);
}

a表示不选当前元素的最大值,b表示选与不选的最大值

int rob(vector<int>& nums) {
    int a = 0, b = 0;
    for(auto v : nums){
        int c = max(a + v, b);
        a = b;
        b = c;
    }
    return b;
}

279. 完全平方数

const int MX = INT_MAX / 2;
int numSquares(int n) {
    vector<int> dp(n + 1, MX);
    dp[0] = 0;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= i / j; j++){
            dp[i] = min(dp[i], dp[i - j * j] + 1);
        }
    }
    return dp[n];
}

322. 零钱兑换

int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount + 1, amount + 1);
    dp[0] = 0;
    for(int i = 1; i <= amount; i++){
        for(auto v : coins){
            if(i >= v) dp[i] = min(dp[i], dp[i - v] + 1);
        }
    }
    return dp[amount] > amount ? -1 : dp[amount];
}

139. 单词拆分
复杂度是O(nL2)

bool wordBreak(string s, vector<string>& wordDict) {
    int n = s.size();
    unordered_set<string> set(wordDict.begin(), wordDict.end());
    vector<int> dp(n + 1, 0);
    dp[0] = 1;
    for(int i = 1; i <= n; i++){
        for(int j = max(0, i - 20); j < i; j++){
            auto sub = s.substr(j, i - j);
            if(set.contains(sub)) dp[i] |= dp[j];
        }
    }
    return dp[n];
}

300. 最长递增子序列
O(nlogn)

int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    vector<int> dp;
    for(auto v : nums){
        if(dp.size() == 0 || dp.back() < v) dp.push_back(v);
        else *lower_bound(dp.begin(), dp.end(), v) = v;
    }
    return dp.size();
}

152. 乘积最大子数组

int maxProduct(vector<int>& nums) {
    int ans = INT_MIN;
    int pst = 1, neg = 1;
    for(auto v : nums){
        int tmp = pst;
        pst = max({pst * v, neg * v, v});
        neg = min({tmp * v, neg * v, v});
        ans = max(ans, pst);
    }
    return ans;
}

416. 分割等和子集
标准01背包

bool canPartition(vector<int>& nums) {
    int n = nums.size();
    int sum = 0;
    for(auto v : nums) sum += v;
    if(sum % 2) return false;
    sum /= 2;
    vector<int> dp(sum + 1, 0);
    dp[0] = 1;
    for(auto v : nums){
        for(int i = sum; i >= v; i--){
            dp[i] |= dp[i - v];
        }
    }
    return dp[sum];
}

32. 最长有效括号

  • 动态规划
int longestValidParentheses(string s) {
    int n = s.size();
    stack<int> st;
    int ans = 0;
    int start = -1;
    for(int i = 0; i < n; i++){
        if(s[i] == ')'){
            if(!st.empty()) st.pop();
            else start = i;
            ans = max(ans, i - (st.empty() ? start : st.top()));
        }
        else st.push(i);
    }
    return ans;
}
  • 两次遍历

62. 不同路径
标准二维dp
可以滚动数组
另组合数学解法

using i64 = long long;
i64 p = 2e9 + 11; //注意题目范围是 <=2e9 所有答案必须选择超过2e9的质数,同时小心爆int64 故选择最小的>=2e9的质数
i64 power(i64 x, i64 y, i64 p){
    i64 r = 1;
    while(y > 0){
        if(y & 1) r = r * x % p;
        y >>= 1;
        x = x * x % p;
    }
    return r;
}
int uniquePaths(int m, int n) {
   vector<i64> f(m + n, 1);
   for(int i = 1; i < m + n; i++) f[i] = f[i - 1] * i % p;
   return f[m + n - 2] * power(f[m - 1], p - 2, p) % p * power(f[n - 1], p - 2, p) % p;
}

64. 最小路径和
同上dp

int minPathSum(vector<vector<int>>& grid) {
    int n = grid.size(), m = grid[0].size();
    vector<int> dp(m);
    for(int i = 0; i < m; i++) dp[i] = i == 0 ? grid[0][0] : dp[i - 1] + grid[0][i];
    for(int i = 1; i < n; i++){
        dp[0] += grid[i][0];
        for(int j = 1; j < m; j++){
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}

5. 最长回文子串

  • 中心扩展
    O(n2)
string longestPalindrome(string s) {
    int n = s.size();
    int mx = 0, left = 0;
    auto dfs = [&](int L, int R)->int {
        while(L >= 0 && R < n && s[L] == s[R]){
            L--; R++;
        }
        return R - L - 1;
    };
    for(int i = 0; i < n; i++){
        //奇数中心
        int odd = dfs(i, i);
        if(odd > mx){
            mx = odd;
            left = i - odd / 2;
        }
        //偶数中心
        int even = dfs(i, i + 1);
        if(even >= mx){
            mx = even;
            left = i - even / 2 + 1;
        }
    }
    return s.substr(left, mx);
}
  • Manacher

1143. 最长公共子序列
dp[i][j]与dp[i - 1][j - 1] dp[i - 1][j] dp[i][j - 1]有关

int longestCommonSubsequence(string text1, string text2) {
    int n = text1.size(), m = text2.size();
    vector<int> dp(m + 1);
    for(auto v : text1){
        int lt = 0; //"左上角"
        for(int i = 1; i <= m; i++){
            int tmp = dp[i];
            dp[i] = v == text2[i - 1] ? lt + 1 : max(dp[i], dp[i - 1]);
            lt = tmp;
        }
    }
    return dp[m];
}

72. 编辑距离

int minDistance(string word1, string word2) {
    int n = word1.size(), m = word2.size();
    vector<int> dp(m + 1);
    for(int i = 0; i <= n; i++){
        int lt = 0;
        for(int j = 0; j <= m; j++){
            int tmp = dp[j];
            int cur = INT_MAX / 2;
            if(i > 0) cur = min(cur, dp[j] + 1);
            if(j > 0) cur = min(cur, dp[j - 1] + 1);
            if(i > 0 && j > 0) cur = min(cur, lt + (word1[i - 1] == word2[j - 1] ? 0 : 1));
            if(i > 0 || j > 0) dp[j] = cur, lt = tmp;
        }
    }
    return dp[m];
}

技巧

136. 只出现一次的数字

int singleNumber(vector<int>& nums) {
    int ans = 0;
    for(auto v : nums) ans ^= v;
    return ans;
}

169. 多数元素

int majorityElement(vector<int>& nums) {
    int ans = 0, count = 0;
    for(auto v : nums){
        if(v == ans) count++;
        else if(--count < 0) ans = v, count = 1;
    }
    return ans;
}

75. 颜色分类
两遍扫描

void sortColors(vector<int>& nums) {
    int n = nums.size(), pt = 0;
    for(int i = 0; i < n; i++){
        if(nums[i] == 0){
            swap(nums[pt], nums[i]);
            pt++;
        }
    }
    pt = n - 1;
    for(int i = n - 1; i >= 0; i--){
        if(nums[i] == 2){
            swap(nums[pt], nums[i]);
            pt--;
        }
    }
}

一遍扫描

void sortColors(vector<int>& nums) {
    int n = nums.size(), L = 0, R = n - 1;
    for(int i = 0; i <= R; i++){
        if(nums[i] == 0) swap(nums[L++], nums[i]);
        else if(nums[i] == 2){
            swap(nums[R--], nums[i]);
            if(nums[i] == 0 && i == L) L++;
            else i--;
        }
    }
}

31. 下一个排列
标准实现

bool NextPermutation(vector<int>& p)
{
    int n = p.size(), t = -1;
    //找最后一组相邻递增对 如[2,1,3,5,4]里的[3,5]
    for (int i = 0; i < n - 1; i++) if (p[i + 1] - p[i] > 0) t = i;
    //没找到说明是递减排列 如[5,4,3,2,1]
    if (t == -1) return false;
    for (int i = n - 1; ; i--)
    {
        if (p[i] > p[t])
        {
            //上面找到[3,5]后实际交换的并不一定是[3,5]
            //而是3后面最后一个比3大的数字 即4
            swap(p[i], p[t]);
            break;
        }
    }
    //交换[3,4]后变成[2,1,4,5,3] 由于3 4交换已经增大了,要找4开头的最小排列
    //故还需要对[t+1,n-1]排序
    sort(p.begin() + t + 1, p.end());
    return true;
}
void nextPermutation(vector<int>& nums) {
    if(!NextPermutation(nums)){
        for(int i = 0; i < nums.size() / 2; i++) swap(nums[i], nums[nums.size() - i - 1]);
    }
}

287. 寻找重复数
哈希
二分
二进制分解
快慢指针判环

int findDuplicate(vector<int>& nums) {
    int slow = 0, fast = 0;
    do {
        slow = nums[slow];
        fast = nums[nums[fast]];
    } while (slow != fast);
    slow = 0;
    while (slow != fast) {
        slow = nums[slow];
        fast = nums[fast];
    }
    return slow;
}
posted @   云上寒烟  阅读(60)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示