LeetCode错题笔记

552. 学生出勤记录 II X

动态规划

时间复杂度O(n),空间复杂度O(1)

  1. 定义:根据题意,只有满足A的数量<2且连续的L<3才能获得出勤奖励,因此可定义动态规划方程为
    d p [ i ] [ j ] [ k ] , 0 ≤ i ≤ n , 0 ≤ j ≤ 1 , 0 ≤ k ≤ 2 dp[i][j][k],0\le i \le n, 0\le j \le 1, 0\le k \le 2 dp[i][j][k],0in,0j1,0k2
    表示前 i i i天中有 j j j个A且末尾有连续 k k k个L的合格方案。

  2. 初始化:第0天只有一种情况,0个A且0个L,即
    KaTeX parse error: Expected 'EOF', got '&' at position 45: …[k] = 0, j\ne 0&̲and& k\ne 0

  3. 状态转移

    • i i i天状态为P时,末尾连续的L数量清零,A的数量不变
      d p [ i ] [ j ] [ 0 ] = ∑ k d p [ i − 1 ] [ j ] [ k ] dp[i][j][0] = \sum_k dp[i - 1][j][k] dp[i][j][0]=kdp[i1][j][k]

    • i i i天状态为L时,末尾连续的L数量+1,A的数量不变
      d p [ i ] [ j ] [ k ] = d p [ i ] [ j ] [ k ] + d p [ i − 1 ] [ j ] [ k − 1 ] , 1 ≤ k ≤ 2 dp[i][j][k] = dp[i][j][k] + dp[i - 1][j][k - 1],1\le k \le 2 dp[i][j][k]=dp[i][j][k]+dp[i1][j][k1],1k2

    • i i i天状态为A时,末尾连续的L数量清零,A的数量+1
      d p [ i ] [ 1 ] [ 0 ] = d p [ i ] [ 1 ] [ 0 ] + ∑ k d p [ i − 1 ] [ 0 ] [ k ] dp[i][1][0] = dp[i][1][0] + \sum_k dp[i - 1][0][k] dp[i][1][0]=dp[i][1][0]+kdp[i1][0][k]

  4. 返回值
    ∑ j ∑ k d p [ n ] [ j ] [ k ] \sum_j\sum_k dp[n][j][k] jkdp[n][j][k]

由于第 i i i行之和第 i − 1 i-1 i1行有关,所以空间复杂度可由 O ( n ) O(n) O(n)优化至 O ( 1 ) O(1) O(1)

437. 路径总和 III O

前缀和+回溯

设从根到叶子为一条完整路径L,则本题就是要遍历所有路径上 s u m ( L [ i . . . j ] ) = = t a r g e t S u m sum(L[i...j]) == targetSum sum(L[i...j])==targetSum的数量,前缀和为常见的计算 s u m ( L [ i . . . j ] ) sum(L[i...j]) sum(L[i...j])的方式,在此用哈希表hashMap记录前缀和和前缀和出现的次数,并初始化hashMap[0] = 1。而考虑到要遍历所有可能路径,则采用回溯的方式。回溯套路:

  1. 终止条件:当node为空时,返回0,表示不存在路径

  2. 更新
    更新前缀和curSum += root->val
    初始化本层的答案ans = 0

    在前缀和哈希表中查找curSum - targetSum,即查找是否存在curSum - sum = targetSumsum值,若存在,则ans += hashMap[curSum - targetSum]

  3. 回溯
    记录本层前缀和hashMap[curSum]++
    ans加上左右子树的回溯递归值
    去掉本层前缀和hashMap[curSum]--

787. K 站中转内最便宜的航班 X

动态规划

时间复杂度 O ( s i z e ( e d g e s ) × n ) O(size(edges )\times n) O(size(edges)×n)

  1. 定义: d p [ i ] [ j ] dp[i][j] dp[i][j]表示由src经过 i i i次飞行到达 j j j的花费, 0 ≤ i ≤ k + 1 0\le i\le k + 1 0ik+1

  2. 初始化:$dp[i][j] = $ INT_MAX

  3. 状态转移:设from = edges[0]; to = edges[1]; cost = edges[2],遍历edges数组,则有
    d p [ i ] [ t o ] = min ⁡ ( d p [ i ] [ t o ] , d p [ i − 1 ] [ f r o m ] + c o s t ) dp[i][to] = \min(dp[i][to], dp[i-1][from] + cost) dp[i][to]=min(dp[i][to],dp[i1][from]+cost)

  4. 返回值: min ⁡ i ( d p [ i ] [ d s t ] ) \min_i(dp[i][dst]) mini(dp[i][dst])

由于 d p [ i ] dp[i] dp[i]只和 d p [ i − 1 ] dp[i-1] dp[i1]有关,可节约空间至 O ( 1 ) O(1) O(1)

//两个&&,因为flights类型是vector<vector<int>>
for (auto &&flight : flights) {...}
//move函数的使用
vector<int> cur(n, maxVal);
vector<int> next(n, maxVal);
cur = move(next);

1109. 航班预订统计 X

差分

差分属于前缀和的变种,前缀和解决了任意连续区间内所有元素之和/之积···运算,公式为 P [ j ] − P [ i ] = x i + . . . + x j − 1 , P [ i ] = x 0 + . . . + x i = P [ i − 1 ] + x i P[j]-P[i]=x_i+...+x_{j-1},P[i]=x_0+...+x_{i}=P[i-1]+x_i P[j]P[i]=xi+...+xj1,P[i]=x0+...+xi=P[i1]+xi。差分公式为 D P [ i ] = x [ i ] − x [ i − 1 ] DP[i]=x[i]-x[i-1] DP[i]=x[i]x[i1],与前缀和的关系在于,差分数组的前缀和为数组本身。

若要对某一连续区间 [ x i , . . . , x j ] [x_i,...,x_j] [xi,...,xj]内的值增加某一相同值 y y y,反应在差分数组上就是 D P ′ [ i ] = D P [ i ] + y DP'[i]=DP[i]+y DP[i]=DP[i]+y以及 D P ′ [ j + 1 ] = D P [ j + 1 ] − y DP'[j+1]=DP[j+1]-y DP[j+1]=DP[j+1]y

210. 课程表 II X

拓扑排序+dfs

图的dfs遍历,类似于回溯算法,区别在于判断在循环外,因为要对第一个遍历点进行操作。要求课程的学习顺序就是要求图的一个可能的拓扑排序,将无环图视作一种起点为根的多叉树,那么拓扑排序=后序遍历图+逆转返回数组。

// 图的遍历模板,注意从每个点出发有多条遍历路径
// on_path: 是否在当前遍历的路径上
// visited: 是否有路径遍历过该点
vector<int> ans;

void dfs(graph, index, on_path, visited, cur_path) {
    if (onPath[index]) {
        ...; // 出现环
        return;
    }
    
    if (visited[index]) {
        ...;
        return; // 已经被遍历过,通常直接返回防止重复
    }
    
    on_path[index] = true; // 记录路径,cur_path.emplace_back(index)
    visited[index] = true;
    ...; // 前序遍历操作
    
    for (next_node : graph[index]) {
        // 遍历相邻的下一点
        dfs(graph, next_node, on_path, visited);
    }
    
    ...; // 后序遍历操作
    ans.emplace_back(next_node); // 拓扑排序
    on_path[index] = false; // 当前路径遍历完毕,cur_path.pop_back(index)
}

int main() {
    for (auto &i : nodes) {
        // 对每个点执行dfs
        dfs(graph, i, on_path, visited);
    }
    ...; // 后序操作
}

1514. 概率最大的路径

743. 网络延迟时间 - 力扣(LeetCode) (leetcode-cn.com)

1631. 最小体力消耗路径 - 力扣(LeetCode) (leetcode-cn.com)

最短路径Dijkstra

无向图、有向图均能用,只要能够将题目抽象为图,再将最优目标抽象为路径上权值计算。

// 假设起点为start,终点为end
// 构建邻接表graph[i]: 第i个节点的邻居vector,vector中每个元素为[邻居节点,路线权值]
vector<vector<pair<int, T>>> graph(n);

for (int i = 0; i < n; ++i) {
    // 注意点1:empalce不用make_pair
    graph[edges[i][0]].emplace_back(edges[i][1], weight[i]);
    graph[edges[i][1]].emplace_back(edges[i][0], weight[i]); // 如果为无向图
}

vector<bool> visited(n, false); // 如果为无向图或有向有环图,防重复访问节点
vector<T> minPath(n, 0); // 从start到某个点的最小路径值
minPath[start] = 0;  // 初始化:到自身的路径长度为0

// 存储<节点,当前最小路径>,按权重从大到小排列
// 注意priority_queue存储pair的默认方式是按照first从大到小
priority_queue<pair<U, int>> pq;
pq.emplace(0, start); // 初始化:到自身的路径长度为0

while (!pq.empty()) {
    // 开始遍历图
    // 注意点2:可以直接auto赋值
    auto [curPath, curNode] = pq.top();
    pq.pop();
    
    if (curPath > minPath[curNode] || visited[curNode]) {
        // 如果已经有更短路径或者已经访问过该点
        continue;
    }
    
    if (curNode == end) {
        // 如果为无向图或有向有环图,到达终点直接退出
        break;
    }
    
    // 如果为无向图或有向有环图,防重复访问节点
    visited[curNode] = true;
    
    for (auto &[next, dist]: graph[curNode]) {
        U distToNext = dist + curPath;
        if (distToNext < minPath[next]) {
            // 出现了更短路径,更新
            minPath[next] = distToNext;
            pq.empalce(distToNext, next);
        }
    }
}

return minPath[end];

109. 有序链表转换二叉搜索树

快慢指针+分治

时间复杂度O(nlog n),空间复杂度O(n)

  1. 利用快慢指针找中间节点:快指针走两步,慢指针走一步,最后慢指针指向中间节点
  2. 分治:将中间节点作为根节点,左右子树分别是以中间节点为分割,左右子区间结果
class Solution {
public:
    ListNode *findMidNode(ListNode *start, ListNode *end) {
        // 快慢指针找中间节点
        ListNode *fast = start, *slow = start;

        while (fast != end && fast->next != end) {
            fast = fast->next->next;
            slow = slow->next;
        }

        return slow;
    }

    TreeNode* helper(ListNode *l, ListNode *r) {
        // 分治
        if (l == r) {
            return nullptr;
        }
        ListNode *midNode = findMidNode(l, r);
        TreeNode *root = new TreeNode(midNode->val);
        root->left = helper(l, midNode);
        root->right = helper(midNode->next, r);

        return root;
    }

    TreeNode* sortedListToBST(ListNode* head) {
        return helper(head, nullptr);
    }
};

中序遍历

时间复杂度O(n)

已知对二叉搜索树中序遍历的结果就是链表顺序,因此也可通过顺序遍历链表中序构造二叉搜索树。同时设有左右端点index,即[l, r],方便判断当前节点是否为nullptr。

class Solution {
public:
    int getLen(ListNode* head) {
        // 获取链表长度
        int len = 0;
        
        while (head) {
            ++len;
            head = head->next;
        }
        
        return len;
    }
    
    TreeNode* helper(ListNode*& node, int l, int r) {
        // 为了判断当前节点是否为nullptr
        if (l > r) {
            return nullptr;
        }
        
        // 顺序遍历,中序构造
        // 注意参数为指针的引用,所以递归会改变指针指向
        int mid = (1 + l + r) / 2; // 奇数时取下界
        TreeNode *root = new TreeNode();
        root->left = helper(node, l, mid - 1);
        root->val = node->val;
        ++node;
        root->right = helper(node, mid + 1, r);

        return root;
    }

    TreeNode* sortedListToBST(ListNode* head) {
        return helper(head, 0, getLen(head) - 1);
    }
};

870. 优势洗牌

贪心

原始思路:对A升序排序,找到满足大于B[i]的最小A[i]值,如果找不到,就用A[i]的最小值顶替

问题:由于A是vector,不管是直接删除元素还是伪删除都需要大量时间,会导致超时

解决:将B降序排序为B_sort,遍历B_sort,每次拿A.back()即A的最大值跟B_sort[i]比较,如果比得过加入答案数组,比不过拿最小值顶替。但由于题目要求要按找未排序B的顺序返回答案,此时如果说明无重复元素可用unordered_map,可能有重复元素则用存储pair<val,idx>的优先队列(堆)。注意pair比较默认拿第一个值比,优先队列默认为大根堆,所以存储pair的优先队列以第一个值的降序排列

此外还有一个好处,因为每次比较的都是A的尾,删除的要不是A的头要不是A的尾,所以可以采用双指针指向头尾进行伪删除,避免pop_back或erase造成开销。

vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
    int n = nums1.size();
    int l = 0;
    int r = n - 1;
    priority_queue<int, int> B_sort; // 存储<val, idx>并为val的大根堆
    vector<int> ans;
    
    // 构造堆,相当于对B降序排列
    for (int i = 0; i < n; ++i) {
        B_sort.emplace(nums2[i], i);
    }
    
    // 遍历B_sort
    while(!B_sort.empty()) {
        int val = B_sort.top().first;
        int idx = B_sort.top().second;
        B_sort.pop();
        
        // 拿B_sort当前最大值跟A最大值比较
        if (A[r] > val) {
            // 比得过,答案为A最大值
            ans.emplace_back(A[r--]);
        } else {
            // 比不过,拿最小值顶替
            ans.emplace_back(A[l++]);
        }
    }
    
    return ans;
}

380. O(1) 时间插入、删除和获取随机元素

哈希set

唯一的问题是获取随机元素,先获取[0, set.size() - 1]的随机数n,然后返回*(set.begin() + n)。但这么做获取随机数的事件还是过长

class RandomizedSet {
public:
    RandomizedSet() {

    }
    
    bool insert(int val) {
        auto iter = data.find(val);
        if (iter == data.end()) {
            data.insert(val);
            return true;
        }

        return false;
    }
    
    bool remove(int val) {
        auto iter = data.find(val);
        if (iter == data.end()) {
            return false;
        }

        data.erase(iter);
        return true;
    }
    
    int getRandom() {
        int random_idx = random() % data.size();
        auto iter = data.begin();
        while (random_idx--) {
            ++iter;
        }
        return *iter;
    }

private:
    unordered_set<int> data;
};

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet* obj = new RandomizedSet();
 * bool param_1 = obj->insert(val);
 * bool param_2 = obj->remove(val);
 * int param_3 = obj->getRandom();
 */

哈希map+数组

数组满足以O(1)时间取随机数和末尾插入元素的要求,但却无法满足O(1)删除指定元素。考虑为什么无法以O(1)删除?是因为数组有一个查找指定元素下标,以及删除后向前移动元素填补空缺的过程。

对于查找指定下标,可以用哈希map存储<元素,对应下标>解决;对于向前移动元素,由于题目对数组存储顺序并无要求,所以我们每次将待删除的元素换到末尾,更新map,最后pop_back()并从map中删除指定项。

class RandomizedSet {
public:
    RandomizedSet() {

    }
    
    bool insert(int val) {
        if (data_idx.find(val) == data_idx.end()) {
            data.emplace_back(val);
            data_idx[val] = data.size() - 1;
            return true;
        }

        return false;
    }
    
    bool remove(int val) {
        auto iter = data_idx.find(val);
        if (iter == data_idx.end()) {
            return false; // 不存在指定元素
        }
        
        int idx = iter->second; // 获取待删除的下标
        data[data.back()] = idx; // 更新末尾元素下标
        swap(data[idx], data.back()); // 将待删除元素交换到末尾
        // 删除指定元素
        data.pop_back();
        data_idx.erase(iter);
        
        return true;
    }
    
    int getRandom() {
        int random_idx = random() % data.size();
        return data[random_idx];
    }

private:
    unordered_map<int, int> data_idx;
    vector<int> data;
};

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet* obj = new RandomizedSet();
 * bool param_1 = obj->insert(val);
 * bool param_2 = obj->remove(val);
 * int param_3 = obj->getRandom();
 */

41. 缺失的第一个正数

一个长为 n n n的数组,最差的情况是存储[1,…,n]所有数,这时候答案为n+1,其他情况下答案一定在[1,n]范围内。

原地哈希

原地哈希记住nums[nums[i] + k] = nums[i]的公式及其变种,本题公式为nums[nums[i] - 1] = nums[i],目的在于还原正数的位置(因为存在n所以需要-1)。还原之后再次遍历就能找出缺失的第一个正数

int firstMissingPositive(vector<int>& nums) {
    int n = nums.size();
    
    for (int i = 0; i < n; ++i) {
        // 如果nums[i]在[1,n]范围内
        // 就将其放到应该存在的位置上,即nums[nums[i] - 1] = nums[i]
        // 为了避免原来nums[nums[i] - 1](新nums[i])上的数被覆盖
        // 要继续判断并交换,直到无法再调整,即nums[nums[i] - 1] == nums[i]或nums[i]不在范围内
        while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
            swap(nums[i], nums[nums[i] - 1]);
        }
    }
    
    for (int i = 0; i < n; ++i) {
        // 现在,所有[1,n]范围内的数i都在下标i-1的位置上
        // 那么找到第一个不满足条件的i即为答案
        if (i + 1 != nums[i]) {
            return i + 1;
        }
    }
    
    return n + 1;
}

468. 验证IP地址

验证匹配类

验证匹配类题目不能总想着面向用例编程,一遍遍查缺补漏。要有一套完整的逻辑流程。

  1. 将字符串按照:或者.分割,如果:分割后>0则传入JudgeIPv6进行判断,否则进入JudgeIPv4判断。这里要注意分割出来的子字符串为空不能加入字符串数组
  2. IPv4判断
    • 首先判断是否为4个子字符串,不是则返回Neither
    • 逐个判断,出现>255、以0开头、字母返回Neither
    • 返回IPv4
  3. IPv6判断
    • 首先判断是否为8个子字符串,不是则返回Neither
    • 逐个判断,出现>f或者>F的字符、总字符数>4返回Neither
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值