1. 问题描述:
给定一个长度为 n 的环形数组 a0,a1,…,an−1。现在要对该数组进行 m 次操作。
操作分为以下两种:
增值操作 l r d,将区间 [l,r] 上的每个元素都增加 d。
求最小值操作 l r,输出区间 [l,r] 内的所有元素的最小值。
注意,数组是环形的,所以当 n = 5 时,区间 [3,1] 内的所有元素依次为 a3,a4,a0,a1。
输入格式
第一行包含整数 n,表示数组长度。第二行包含 n 个整数,表示 a0,a1,…,an−1。第三行包含整数 m,表示操作数。接下来 m 行,每行描述一个操作,对于第 i 行,如果包含两个整数 l,r,则表示第 i 个操作为求最小值操作;如果包含三个整数 l,r,d,则表示第 i 个操作为增值操作。
输出格式
每个求最小值操作输出一行结果。
数据范围
前三个测试点满足 1 ≤ n,m ≤ 10。
所有测试点满足 1 ≤ n ≤ 2 × 10 ^ 5,0 ≤ m ≤ 2 × 10 ^ 5,−10 ^ 6 ≤ ai ≤ 10 ^ 6,0 ≤ l,r ≤ n − 1,−10 ^ 6 ≤ d ≤10 ^ 6。
输入样例:
4
1 2 3 4
4
3 0
3 0 -1
0 1
2 1
输出样例:
1
0
0
来源:https://www.acwing.com/problem/content/description/3808/
2. 思路分析:
分析题目可以知道这两种操作是线段树的经典操作,区间修改与区间查询(区间的动态修改与动态查询),因为涉及到整个区间加上一个数,所以线段树节点需要维护懒标记的信息(如果是单点修改则不需要使用懒标记维护),由题目可知我们在线段树中除了维护当前节点u的区间左右端点l,r的信息之外,还可以需要维护两个额外的信息,分别是懒标记dt和当前节点u对应的区间[l,r]的最小值minv;由于这道题目是环形数组,所以我们分情况讨论即可,如果l > r,说明需要分成[0,r]和[l,n - 1]两段区间,否则为区间[l,r],可以发现这道题目属于线段树的纯模板题(区间加上一个数字和查询区间的某个信息:最小值/最大值),涉及到下面的5个方法:
- void build(int u);初始化[0,n - 1]对应的所有区间的线段树节点,在创建节点的时候维护所有区间的最小值信息,当初始化所有区间[0,n - 1]的所有线段树节点之后可以维护处所有区间的最小值信息;
- void pushup(int u);将当前节点u的左右子树对应的最小值信息往上传递;
- void pushdown(int u);将当前节点u的懒标记信息dt传递到左右子树对应的节点,由于整个区间加上一个数不影响当前区间所有值的相对大小,所以将懒标记下传的过程中左右子树对应节点的最小值也加上dt,这样可以维护左右子树最小值的信息;
- void update(int u,int l,int r,int d);将区间[l,r]中的所有数加上d,其实是维护懒标记的信息和区间最小值信息,在递归的过程中需要将懒标记往下传递,根据当前修改的区间[l,r]与当前节点u维护的区间关系判断递归哪一边;
- void query(int u, int l,int r);查询区间[l,r]的最小值,查询的过程中也需要将懒标记往下传递,并且根据查询区间[l,r]与当前节点维护的区间的大小关系判断递归哪一边;
一般来说带有懒标记的线段树某个区间加上一个数或者是查询区间的某个值都需要将懒标记往下传递,并且线段树在查询与更新的时候节点编号是从1开始的,这样往下递归的时候对应的编号才可以递增。
c/c++语言可以使用结构体来描述一个线段树节点,一个线段树节点维护一个区间的相关信息(区间端点,区间最大值反之与题目需要维护的区间信息),java则可以使用对象数组(内部类)来描述线段树节点,与c++语言是类似的,而python感觉使用类来描述好像不太支持,而且比较复杂并且python的运行效率也确实比较慢;但是线段树节点本质上维护一个区间的相关信息,我们其实是可以使用数组来代替的,有多少个需要维护的信息那么就声明多少个数组,对于线段树的题目来说,如果有n个数那么需要声明4n个线段树节点,对于这道题目来说我们需要维护两个信息,第一个是懒标记,第二个是区间最小值,所以可以使用两个数组来维护,其中数组dt来记录懒标记,minv数组来记录区间最小值信息;每一个线段树对应的节点编号维护一个区间,例如长度为n的数字,编号为1的线段树节点维护[0,n - 1]的区间信息,编号为2的节点维护[0,(n - 1) / 2]的区间信息,编号为3的节点维护[(n - 1) / 2 + 1,n - 1]的区间信息...对应一棵二叉树;对于这道题目来说dt[u]表示的是编号为u的线段树节点维护的对应区间[x,y]的懒标记信息,minv[u]为编号为u的线段树节点维护的对应区间[x,y]的区间最小值信息,与c++的结构体或者java的对象数组描述的线段树节点是一样的,只是在区间修改与查询的时候需要传递两个变量,分别为当前节点编号为u的线段树节点维护的区间左端点与右端点l,r信息,这样使用两个数组来维护信息与c++或者java语言对应的数据结构描述信息是一致的:
3. 代码如下:
java:
import java.util.Scanner;
public class Main {
static Tree []tree;
static int []w;
// 声明一个全局最大值
static long INF = Long.MAX_VALUE;
// 初始化所有区间的线段树节点的值
public static void build(int u, int l, int r){
if (l == r){
tree[u].l = tree[u].r = l;
tree[u].minv = w[l];
return;
}
tree[u].l = l;
tree[u].r = r;
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
// 递归之后需要将子节点的信息往上传递, 维护区间最小值信息
pushup(u);
}
// 将子节点的信息(最小值)往上传递到父节点
public static void pushup(int u){
tree[u].minv = Math.min(tree[u << 1].minv, tree[u << 1 | 1].minv);
}
// 懒标记往下传递, 因为整个区间的加上一个数不影响相对大小所以当前区间加上这个最小值维护区间的最小值信息
public static void pushdown(int u){
Tree l = tree[u << 1];
Tree r = tree[u << 1 | 1];
l.dt += tree[u].dt;
r.dt += tree[u].dt;
l.minv += tree[u].dt;
r.minv += tree[u].dt;
// 将根节点的懒标记清空
tree[u].dt = 0;
}
public static void update(int u, int l, int r, int d){
if (tree[u].l >= l && tree[u].r <= r){
// 当前整段区间加上d相当于dt加上d, 并且不影响相对大小所以需要minv加上d这样可以维护最小值
tree[u].dt += d;
tree[u].minv += d;
}
else {
// 将懒标记往下传递
pushdown(u);
int mid = tree[u].l + tree[u].r >> 1;
if (mid >= l) {
update(u << 1, l, r, d);
}
if (mid < r) {
update(u << 1 | 1, l, r, d);
}
// 更新完之后将子节点的信息往上传递到父节点
pushup(u);
}
}
// 注意返回值是long, 防止发生溢出的情况
public static long query(int u, int l, int r){
if (tree[u].l >= l && tree[u].r <= r){
return tree[u].minv;
}
// 将懒标记往下传递
pushdown(u);
int mid = tree[u].l + tree[u].r >> 1;
long res = INF;
// 查询范围在当前节点的左子树对应的区间有元素
if (mid >= l){
res = query(u << 1, l, r);
}
// 查询范围在当前节点的右子树对应的区间有元素
if (mid < r){
res = Math.min(res, query(u << 1 | 1, l, r));
}
return res;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
// 声明为全局数组这样在方法中不用传递会比较方便
w = new int[n + 10];
for (int i = 0; i < n; ++i){
w[i] = sc.nextInt();
}
int m = sc.nextInt();
// 接受回车符, nextLine会接受多余的回车符
sc.nextLine();
tree = new Tree[n * 4];
// 初始化每一个线段树节点
for (int i = 0; i < n * 4; ++i){
tree[i] = new Tree();
}
// 创建[0, n - 1]所有的线段树节点
build(1, 0, n - 1);
// m个操作
while (m-- > 0){
// 先输出字符串然后判断
String []s = sc.nextLine().split(" ");
// 长度为2说明是查询区间的最小值
if (s.length == 2){
int a, b;
a = Integer.parseInt(s[0]);
b = Integer.parseInt(s[1]);
if (a <= b){
System.out.println(query(1, a, b));
}else{
long res = query(1, a, n - 1);
res = Math.min(res, query(1, 0, b));
System.out.println(res);
}
}else{
int a, b, c;
a = Integer.parseInt(s[0]);
b = Integer.parseInt(s[1]);
c = Integer.parseInt(s[2]);
if (a <= b) {
update(1, a, b, c);
}else {
update(1, a, n - 1, c);
update(1, 0, b, c);
}
}
}
}
// 线段树节点
public static class Tree{
int l, r;
long dt, minv;
}
}
c++:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200010;
const LL INF = 1e18;
int n, m;
int w[N];
struct Node
{
int l, r;
LL dt, mn;
}tr[N * 4];
void pushup(int u)
{
tr[u].mn = min(tr[u << 1].mn, tr[u << 1 | 1].mn);
}
void pushdown(int u)
{
auto &root = tr[u], &l = tr[u << 1], &r = tr[u << 1 | 1];
l.dt += root.dt, l.mn += root.dt;
r.dt += root.dt, r.mn += root.dt;
root.dt = 0;
}
void build(int u, int l, int r)
{
if (l == r) tr[u] = {l, r, 0, w[l]};
else
{
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void update(int u, int l, int r, int d)
{
if (tr[u].l >= l && tr[u].r <= r)
{
tr[u].dt += d, tr[u].mn += d;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) update(u << 1, l, r, d);
if (r > mid) update(u << 1 | 1, l, r, d);
pushup(u);
}
}
LL query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r)
{
return tr[u].mn;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
LL res = INF;
if (l <= mid ) res = query(u << 1, l, r);
if (r > mid) res = min(res, query(u << 1 | 1, l, r));
return res;
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &w[i]);
build(1, 0, n - 1);
scanf("%d", &m);
while (m -- )
{
int l, r, d;
char c;
scanf("%d %d%c", &l, &r, &c);
if (c == '\n')
{
if (l <= r) printf("%lld\n", query(1, l, r));
else printf("%lld\n", min(query(1, l, n - 1), query(1, 0, r)));
}
else
{
scanf("%d", &d);
if (l <= r) update(1, l, r, d);
else update(1, l, n - 1, d), update(1, 0, r, d);
}
}
return 0;
}
python:python对于声明比较大的列表一般都会消耗比较多的时间,而且运行效率比较慢,所以提交上去超时了,只过了一半的数据
class Solution:
dt = minv = w = None
def build(self, u: int, l: int, r: int):
if l == r:
self.minv[u] = self.w[l]
return
mid = l + r >> 1
self.build(u << 1, l, mid)
self.build(u << 1 | 1, mid + 1, r)
self.pushup(u)
def pushup(self, u: int):
self.minv[u] = min(self.minv[u << 1], self.minv[u << 1 | 1])
def pushdown(self, u: int):
v = self.dt[u]
self.dt[u << 1] += v
self.minv[u << 1] += v
self.dt[u << 1 | 1] += v
self.minv[u << 1 | 1] += v
# ul, ur为当前节点编号为u维护的区间左端点与右端点
def query(self, u: int, ul: int, ur: int, l: int, r: int):
if ul >= l and ur <= r:
return self.minv[u]
mid = ul + ur >> 1
# res一开始为最大值
res = 10 ** 18
self.pushdown(u)
if mid >= l:
res = self.query(u << 1, ul, mid, l, r)
if mid < r:
res = min(res, self.query(u << 1 | 1, mid + 1, ur, l, r))
return res
# 整个区间加上一个数
def update(self, u: int, ul: int, ur: int, l: int, r: int, d: int):
if ul >= l and ur <= r:
self.dt[u] += d
self.minv[u] += d
# 注意return否则会报异常
return
self.pushdown(u)
mid = ul + ur >> 1
if mid >= l:
self.update(u << 1, ul, mid, l, r, d)
if mid < r:
self.update(u << 1 | 1, mid + 1, ur, l, r, d)
self.pushup(u)
def process(self):
# 对于这道题目来说线段树节点可以使用两个列表来维护相应的信息
n = int(input())
self.dt, self.minv = [0] * (4 * n), [0] * (4 * n)
self.w = list(map(int, input().split()))
m = int(input())
self.build(1, 0, n - 1)
for i in range(m):
t = list(map(int, input().split()))
if len(t) == 2:
a, b = t[0], t[1]
if a <= b:
print(self.query(1, 0, n - 1, a, b))
else:
print(min(self.query(1, 0, n - 1, a, n - 1), self.query(1, 0, n - 1, 0, b)))
else:
a, b, c = t[0], t[1], t[2]
if a <= b:
self.update(1, 0, n - 1, a, b, c)
else:
self.update(1, 0, n - 1, a, n - 1, c)
self.update(1, 0, n - 1, 0, b, c)
if __name__ == '__main__':
Solution().process()