本文将对力扣第409场周赛的T3和T4进行讲解:新增道路查询后的最短距离II、交替组III,涉及最短路、广度优先搜索、树状数组等知识点。
T3 - 新增道路查询后的最短距离 II
题目链接:100376. 新增道路查询后的最短距离 II。
题目描述
给你一个整数 n
和一个二维整数数组 queries
。
有 n
个城市,编号从 0
到 n - 1
。初始时,每个城市 i
都有一条单向道路通往城市 i + 1
( 0 <= 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]
中的每个 i
,answer[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 != j
且queries[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
和一个二维整数数组 queries
。colors
表示一个由红色和蓝色瓷砖组成的环,第 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] == 1
或queries[i][0] == 2
对于所有的
i
:queries[i][0] == 1
:queries[i].length == 2
,3 <= queries[i][1] <= colors.length - 1
queries[i][0] == 2
:queries[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场双周赛。
文章文档:公众号 字节幺零二四
回复关键字可获取本文文档。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。