头图

本文将对力扣第409场周赛的T3和T4进行讲解:新增道路查询后的最短距离II、交替组III,涉及最短路、广度优先搜索、树状数组等知识点。

T3 - 新增道路查询后的最短距离 II

题目链接:100376. 新增道路查询后的最短距离 II

题目描述

给你一个整数 n 和一个二维整数数组 queries

n 个城市,编号从 0n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 10 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1最短路径长度

所有查询中不会存在两个查询都满足 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 ianswer[i] 是处理完 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度

数据范围

  • 3 <= n <= 105
  • 1 <= queries.length <= 105
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中不存在重复的道路。
  • 不存在两个查询都满足 i != jqueries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

解题思路

数据范围中的最后一个条件实则在提醒,区间是可以很容易合并的,因为不存在严格相交的两个区间。

初始有 [0,1],[1,2],...,[n-2,n-1]n 个区间,将这些区间加入区间集合 s。依次取出每个询问 (u,v)

  • s 中存在位于 [u,v] 之间的区间,则找出这样的区间,将其移出集合 s,然后合并这些区间,合并结果即为 [u,v],将区间 [u,v] 加入集合 s。此时集合大小即为最短路径的长度。
  • s 中不存在位于 [u,v] 之间的区间,说明区间 (u,v) 必定被 s 中某个区间包含,也即,询问 (u,v) 不影响最短路径的长度,此时无需进行任何操作,集合大小即为最短路径的长度。

考虑到初始时,区间是完整的,即,集合 s 中所有区间的和为 [,n-1],所以,无论如何合并,在任意时刻,区间之和都是完整的。因此,可使用链表来维护所有区间,每个节点表示一个区间端点。

vector<int> shortestDistanceAfterQueries(int n, vector<vector<int>> &queries) {
    int nxt[n];
    // 初始区间链
    for (int i = 0; i < n; i++)nxt[i] = i + 1;
    // 目前的区间数量
    int last = n - 1;
    vector<int> res;
    for (auto &q: queries) {
        int u = q[0], v = q[1];
        // 存在区间位于区间[u,v]之内
        if (nxt[u] != -1 && nxt[v] != -1) {
            for (int i = u, t; i != v;) {
                // nxt[i]=-1 标记i不再作为区间端点
                // last-- 表示区间[i,nxt[i]]被移出
                t = nxt[i], nxt[i] = -1, i = t, last--;
            }
            // nxt[u]=v 表示将合并得到的区间[u,v]加入区间链
            // last++ 表示[u,v]加入区间链
            nxt[u] = v, last++;
        }
        // 目前的区间数量即为询问后的最短路径的长度
        res.push_back(last);
    }
    return res;
}

时间复杂度:O(n+q)

空间复杂度:O(n),返回值不计。

T4 - 交替组 III

题目链接:100388. 交替组 III

题目描述

给你一个整数数组 colors 和一个二维整数数组 queriescolors表示一个由红色和蓝色瓷砖组成的环,第 i 块瓷砖的颜色为 colors[i]

  • colors[i] == 0 表示第 i 块瓷砖的颜色是 红色
  • colors[i] == 1 表示第 i 块瓷砖的颜色是 蓝色

环中连续若干块瓷砖的颜色如果是 交替 颜色(也就是说这组瓷砖中除了第一块和最后一块瓷砖以外,中间瓷砖的颜色与它 左边右边 的颜色都不同),那么它被称为一个 交替组

你需要处理两种类型的查询:

  • queries[i] = [1, sizei],确定大小为sizei交替组 的数量。
  • queries[i] = [2, indexi, colori],将colors[indexi]更改为colori

返回数组 answer,数组中按顺序包含第一种类型查询的结果。

注意 ,由于 colors 表示一个 第一块 瓷砖和 最后一块 瓷砖是相邻的。

数据范围

  • 4 <= colors.length <= 5 * 104
  • 0 <= colors[i] <= 1
  • 1 <= queries.length <= 5 * 104
  • queries[i][0] == 1queries[i][0] == 2
  • 对于所有的 i

    • queries[i][0] == 1queries[i].length == 2, 3 <= queries[i][1] <= colors.length - 1
    • queries[i][0] == 2queries[i].length == 3, 0 <= queries[i][1] <= colors.length - 1, 0 <= queries[i][2] <= 1

解题思路

考虑长度为 a 的交替组,若 b≤a,则共包含 a-b+1 个长度为 b 的交替组。

将换划分为若干个交替组,要求交替组数量尽可能少。令 f[x] = 长度大于等于 x 的交替组的长度之和,g[x] = 长度大于等于 x 的交替组的数量,则,长度为 s 的交替组的数量为 f[x]-(s-1)*g[x]

通常情况下,“长度大于等于 x 的交替组的长度之和”会转换为“长度总和”-“长度小于等于 x-1 的交替组的长度之和”,g[x] 同理。

“长度小于等于 x 的交替组的长度之和”可用树状数组维护,“长度小于等于 x 的交替组的数量”同理。

对于修改操作,要么会导致交替组合并,要么会导致交替组拆分,所以需要维护交替组集合。由于所有交替组首尾相连可以组成环,所以,只需要记录每个交替组的右端点即可(左端点也可)。

综上,每次修改操作,先对交替组进行合并或者拆分,在根据交替组处理结果对树状数组进行修改,修改操作大体思路如下:

  • 先通过 ist()/del() 对交替组进行拆分/合并;
  • ist()/del() 中调用中间函数 update() 对树状数组进行更新;
  • update() 实际调用树状数组模板函数 add() 对树状数组进行更新。

代码实现

vector<int> numberOfAlternatingGroups(vector<int> &colors, vector<vector<int>> &queries) {

    int n = colors.size();

    // tr1[i] 表示长度小于或等于i的交替组的数量
    // tr2[i] 表示长度小于或等于i的交替组的长度之和
    int tr1[n + 1], tr2[n + 1];
    memset(tr1, 0, sizeof tr1);
    memset(tr2, 0, sizeof tr2);

    // 树状数组模板代码
    auto add = [&](int *tr, int p, int v) {
        for (; p <= n; p += p & -p)tr[p] += v;
    };
    auto query = [&](int *tr, int p) {
        int res = 0;
        for (; p; p -= p & -p)res += tr[p];
        return res;
    };

    /**
     * v=1 表示新增,v=-1 表示删除
     * 新增右端点p,即,将[l+1,r]拆分为[l+1,p]和[p+1,r]两个交替组
     * 或者
     * 删除右端点p,即,将[l+1,p]和[p+1,r]两个交替组合并为[l+1,r]
     */
    auto update = [&](int l, int r, int p, int v) {

        // v=1时删除[l+1,r]
        // v=-1时新增[l+1,r]
        int len = (r - l + n) % n;
        // 当仅有一个右端点且新增右端点p时,l和r相等,len为0
        if (len == 0)len = n;
        add(tr1, len, -v), add(tr2, len, -v * len);

        // v=1时新增[l+1,p]和[p+1,r]
        // v=-1时删除[l+1,p]和[p+1,r]

        // 右端点不重复,故p和l必定不相等,故len必不为0
        len = (p - l + n) % n;
        add(tr1, len, v), add(tr2, len, v * len);

        // 右端点不重复,故p和r必定不相等,故len必不为0
        len = (r - p + n) % n;
        add(tr1, len, v), add(tr2, len, v * len);

    };

    // 存储交替组右端点
    set<int> st;

    // 往st中新增交替组右端点p
    auto ist = [&](int p) {
        if (st.empty()) {
            // 在无右端点的情况下新增一个右端点
            // 相当于新增1个长度为n的交替组,长度小于等于n的交替组的总和加n
            // 由于此时将端点p添加到st中后,p的前后均没有右端点,故无法走else中的逻辑,需要特殊处理
            st.insert(p);
            add(tr1, n, 1), add(tr2, n, n);
        } else {
            // 在有右端点的情况下新增一个右端点
            // 相当于将一个交替组拆分成两个交替组
            // 即将[l+1,r]拆分为[l+1,p]和[p+1,r]两个交替组
            // it指向右端点p
            auto it = st.insert(p).first;
            int l = it == st.begin() ? *prev(st.end()) : *prev(it);
            int r = next(it) == st.end() ? *st.begin() : *next(it);
            update(l, r, p, 1);
        }
    };
    // 从st中删除交替组右端点p
    auto del = [&](int p) {
        if (st.size() == 1) {
            // 在仅有1个右端点的情况下删除一个右端点
            // 相当于删除1个长度为n的交替组,长度小于等于n的交替组的总和减n
            // 由于此时将端点p从st中删除后,st中没有右端点,故无法走else中的逻辑,需要特殊处理
            st.erase(st.find(p));
            add(tr1, n, -1), add(tr2, n, -n);
        } else {
            // 在有多个右端点的情况下删除一个右端点
            // 相当于将两个交替组合并为一个交替组
            // 即将[l+1,p]和[p+1,r]两个交替组合并为[l+1,r]
            // it指向右端点p的下一个元素
            auto it = st.erase(st.find(p));
            int l = it == st.begin() ? *prev(st.end()) : *prev(it);
            int r = it == st.end() ? *st.begin() : *it;
            update(l, r, p, -1);
        }
    };

    // 记录每个最长交替组的右端点
    for (int i = 0; i < n; i++)
        if (colors[i] == colors[(i + 1) % n])ist(i);

    // 处理每个请求
    vector<int> res;
    for (auto &q: queries) {
        if (q[0] == 1) {
            // 查询
            if (!st.empty()) {
                // 长度大于q[1]的交替组的数量=所有交替组的数量-长度小于等于q[1]-1的交替组的数量
                int cnt = query(tr1, n) - query(tr1, q[1] - 1);
                // 长度大于q[1]的交替组的总和=所有交替组的总和-长度小于等于q[1]-1的交替组的总和
                int total = query(tr2, n) - query(tr2, q[1] - 1);
                res.push_back(total - (q[1] - 1) * cnt);
            }
                // 无右端点的情况并未记录到线段树,故特殊处理
            else res.push_back(n);
        } else {
            // 修改
            int p = q[1], c = q[2];
            // p的前后位置
            int pre = (p - 1 + n) % n, nxt = (p + 1) % n;
            // 改前pre和p一样,则先删除右端点pre
            if (colors[pre] == colors[p])del(pre);
            // 改前p和nxt一样,则先删除右端点p
            if (colors[p] == colors[nxt])del(p);
            // 修改p的颜色
            colors[p] = c;
            // 改后pre和p一样,则新增右端点pre
            if (colors[pre] == colors[p])ist(pre);
            // 改后p和nxt一样,则新增右端点p
            if (colors[p] == colors[nxt])ist(p);
            // 需要注意的是,改前pre和p一样,改后pre和p可能依旧一样(如改),所以,改前改后得分开处理
            // p和nxt同理
        }
    }
    return res;
}

时间复杂度:O((n+q)log(n))

空间复杂度:O(n),返回值不计。

END

以上就是本文的全部内容,如果觉得有一点点帮助,欢迎点赞、转发加关注,如果有任何问题,欢迎在评论区交流和讨论,咱们下期见!

文章声明:题目来源 力扣 平台,如有侵权,请联系删除!

题目来源:力扣第409场双周赛

文章文档:公众号 字节幺零二四 回复关键字可获取本文文档。


字节幺零二四
9 声望5 粉丝

talk is cheap, show me you code!