Tải bản đầy đủ (.docx) (27 trang)

CẤU TRÚC dữ LIỆU NÂNG CAO

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (226.84 KB, 27 trang )

CHUYÊN ĐỀ: CẤU TRÚC DỮ LIỆU NÂNG CAO
Interval Tree là công cụ rất hữu dụng được sử dụng nhiều trong các bài toán trên dãy
số, hoặc được quy về các bài toán xử lí trên dãy số, đặc biệt là các bài toán có nhiều
công việc cần xử lí và nhiều truy vấn xen kẽ nhau.
Phần lí thuyết về Interval Tree đã được trình bày rất rõ ràng ở nhiều tài liệu do các
chuyên gia, đồng nghiệp dạy bồi dưỡng học sinh giỏi chia sẻ, nên tôi mạn phép
không đề cập tại đây.
Do năng lực có hạn nên tôi không viết hoặc nghĩ ra những bài tập mới mà có sử dụng
Interval Tree để giải được. Vì thế, trong chuyên đề này thực chất là các bài tập tôi sưu
tầm, biên tập thành tập tài liệu để phục vụ trong công tác giảng dạy bồi dưỡng HSG
môn Tin học. Ở đây, tôi trích dẫn các bài tập nguồn từ SPOJ, Codeforce và nhiều
nguồn khác. Với mỗi bài tập tôi đề cập đến ba vấn đề:
• Tóm tắt đề bài rõ ràng
• Thuật toán tốt
• Code demo (nếu có).
Khi áp dụng tài liệu này vào giảng dạy, tôi thường bỏ phần “code demo” để không
“làm hỏng học sinh”, chỉ phát đề cho học sinh. Với mỗi bài tập, sau khi học sinh
nghiên cứu và đề xuất ý tưởng (hoặc code nộp mà chưa AC), tôi dẫn dắt, đưa ra giải
thuật của bài toán đó để học sinh “ngấm” bài toán hơn. Dần dần học sinh nắm được
tư tưởng Interval Tree và ứng dụng linh động vào các bài toán khác.
Tôi cũng xin trích dẫn các tài liệu tôi tham khảo để biên tập thành chuyên đề này:






/> /> /> /> />
tap-interval-tree/
• Quyển: Một số vấn đề chú ý môn Tin học – Nhóm tác giả của Đại học Vinh


1


Ứng dụng Interval Tree để giải các bài toán sau:
Bài 1. Phần tử thứ K />Cho dãy số A có N phần tử nguyên phân biệt.
Cho Q truy vấn, mỗi truy vấn có dạng: L R K
Yêu cầu: mỗi truy vấn xuất ra phần tử lớn thứ K sau khi sắp xếp các phần tử AL, AL+1,
…, AR theo thứ tự tăng dần.
Giới hạn:
1 ≤ N, Q ≤ 105
|Ai| ≤ 109 với 1 ≤ i ≤ N
1≤L≤R≤N
1 ≤ K ≤ R-L+1
Input:
-

Dòng đầu tiên chứa số N.

-

Dòng tiếp theo chứa N số A1, A2, …, AN.

-

Dòng tiếp theo chứa số Q.

-

Q dòng tiếp theo, mỗi dòng chứa 3 số L, R, K.


Output:
Q dòng, mỗi dòng chứa câu trả lời cho một truy vấn theo thứ tự nhập vào.
Ví dụ:
Input
7

Output
2

2154368

6

4

4

122

3

374
462
551
Thời gian chạy:

1s-3s

THUẬT TOÁN :


2


Dùng Segment Tree với mỗi nút lưu lại dãy con từ l->r đã được sort. Dùng vector cho
mỗi nút để giảm bộ nhớ: mỗi nút sẽ xuất hiện logN lần trên cây, do đó bộ nhớ là
NlogN. Có thể tạo cây trong O(NlogN), mỗi lần hợp hai nút con lại ta trộn hai đoạn
con trong O(n+m) với n, m là kích thước của hai đoạn con.
Với mỗi truy vấn ta làm như sau: Xét các giá trị (gọi là res) có trong dãy bằng cách
chặt nhị phân, (nút 1 thực chất đã sort dãy tăng dần nên có thể chặt nhị phân trên nút
1), đếm xem trong đoạn l..r có bao nhiêu phần tử nhỏ hơn nó, nếu nhỏ hơn k tức là
phải tìm số lớn hơn nữa và tương tự. Với mỗi lần truy vấn l..r thì ta lại chặt nhị phân
những nút nằm trong đoạn l..r để tìm phần tử lớn nhất ≤ res đồng thời kiểm tra xem
res có mặt cũng như đếm số lượng phần tử nhỏ hơn res (Chú ý là các phần tử là phân
biệt). Điều kiện để res là nghiệm chính là cnt == k-1 (cnt là số lượng số < res) và tìm
thấy res trong đoạn l..r.
Code demo: />#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef int64_t ll;
typedef pair<int,int> ii;
#define EL printf("\n")
#define pb push_back
#define mp make_pair
#define X first
#define Y second
typedef vector<int> data;
const int N = 100100;
int
n, q, a[N], L, R, k, res, cnt, f;
data

t[4*N], nil;
data combine(data u, data v)
{
data ans = nil;
int i = 0, j = 0;
while (i < u.size() and j < v.size()) {
if (u[i] < v[j]) ans.pb(u[i++]);
else ans.pb(v[j++]);
}
while (i < u.size()) ans.pb(u[i++]);
while (j < v.size()) ans.pb(v[j++]);
return ans;
}
void build(int k, int l, int r)
{
if (l == r) {

void get(int node, int l, int r)
{
if (r < L or R < l) return ;
if (L ≤ l and r ≤ R) {
int i = 0, j = t[node].size()-1, pos = -1;
while (i ≤ j) {
int mid = (i+j)/2;
if (t[node][mid] ≤ res) {
pos = mid;
i = mid+1;
}
else j = mid-1;
}

if (pos == -1) return ;
if (t[node][pos] == res) f = true;
cnt += pos + 1;
if (t[node][pos] == res) cnt--;
return ;
}
int mid = (l+r)/2;
get(node*2,l,mid);
get(node*2+1,mid+1,r);
}
int main()
{
//freopen("YPKTH.INP","r",stdin);
//freopen("YPKTH.OUT","w",stdout);
scanf("%d", &n);
for (int i=1;i≤n;i++) scanf("%d", &a[i]);
build(1,1,n);

3


t[k].pb(a[l]);
return ;

scanf("%d", &q);
while (q--) {
scanf("%d%d%d", &L,&R,&k);
int l = 0, r = t[1].size()-1;
while (l ≤ r) {
int mid = (l+r)/2;

res = t[1][mid];
cnt = 0;
f = 0;
get(1,1,n);
if (cnt == k-1 and f) {
printf("%d\n", res);
break;
}
if (cnt < k) l = mid+1; else r = mid-1;
}
}
return 0;

}
int mid = (l+r)/2;
build(k*2, l, mid);
build(k*2+1, mid+1, r);
t[k] = combine(t[k*2], t[k*2+1]);
}

}

Bài 2. Đoạn con có tổng lớn nhất />Cho dãy số a[1], a[2], ..., a[n] (|a[i]| ≤ 15000, n ≤ 50000).
Hàm q(x, y) = max { tổng(a[i]+a[i+1]+...+a[j]), x ≤ i ≤ j ≤y }.
Cho m câu hỏi dạng x, y (1 ≤ x ≤ y ≤ n), (m ≤ 50000) -> hãy tính các q(x, y).
Input
- Dòng đầu là n.
- Dòng thứ hai là dãy a.
- Dòng thứ 3 là m.
- m dòng tiếp theo mỗi dòng là 1 cặp số x, y.

Output
Lần lượt ghi ra các q(x, y) tương ứng. Mỗi kết quả ghi trên 1 dòng.
Example
Input:
3
-1 2 3
1
12
4


Output:
2
Thời gian chạy: 0.100s

Thuật toán: Sử dụng Segment Tree
Một nút lưu các giá trị :
sum : tổng đoạn
pre : tổng lớn nhất của đoạn tiền tố
suf : tổng lớn nhất của đoạn hậu tố
ans : tổng lớn nhất của đoạn
Công thức hợp hai nút trên cây như sau :
res.sum = l.sum + r.sum;
res.pre = max(l.pre, l.sum + r.pre);
res.suf = max(r.suf, r.sum + l.suf);
res.ans = max(l.ans, r.ans, l.suf + r.pre);
Code demo:
#include <iostream>
#include <fstream>
#include <sstream>

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <climits>
#include <vector>
#include <stack>
#include <queue>
#include <deque>
#include <set>
#include <map>
#include <bitset>
#include <ctime>
#include <list>

data make_data (int x)
{
data res;
res.sum = x;
res.pre = res.suf = res.ans = x;
return res;
}
void make_tree(int k, int l, int r)
{
if (l == r) {
t[k] = make_data(a[l]);
return ;
}
int mid = (l+r)/2;

make_tree(k*2, l, mid);
make_tree(k*2+1, mid+1, r);

using namespace std;
t[k] = combine(t[k*2], t[k*2+1]);
typedef long long ll;

}

5


typedef int64_t
typedef double
const int
const int
const ll
const real

ll;
real;

base = 1000000007;
oo = INT_MAX;
ooo = LONG_LONG_MAX;
pi = acos(-1.0);

data query(int k, int l, int r, int L, int R)
{
if (l == L and r == R) return t[k];

int mid = (l+r)/2;
if (R ≤ mid)
return query(k*2, l, mid, L, R);

#define
openf
{freopen("INP.INP","r",stdin);freopen("OU
T.OUT","w",stdout);}
#define
closef
{fclose(stdin);fclose(stdout);}
#define readln
scanf("\n")
#define writeln printf("\n")

if (L > mid)
return query(k*2+1, mid+1, r, L, R);
return
combine (
query(k*2, l, mid, L, mid),
query(k*2+1, mid+1, r, mid+1, R)
);

//---------------------------------------------------/
/
}
struct data
{
int sum, pre, suf, ans;
};


int main()
{
//openf;

const int maxn = 100000, maxt = 4*maxn;
int
n, a[maxn], m, L, R;
data
t[maxt];
data combine (data l, data r)
{
data res;
res.sum = l.sum + r.sum;
res.pre = max(l.pre, l.sum + r.pre);
res.suf = max(r.suf, r.sum + l.suf);
res.ans = max( max(l.ans, r.ans), l.suf +
r.pre);
return res;
}

scanf("%d", &n);
for (int i=1;i≤n;i++) scanf("%d", &a[i]);
make_tree(1,1,n);
scanf("%d", &m);
while (m--) {
scanf("%d%d",&L,&R);
printf("%d\n",query(1,1,n,L,R).ans);
}
//closef;

return 0;
}

Bài 3. Diện tích hình chữ nhật - />Trên mặt phẳng toạ độ người ta vẽ ra N hình chữ nhật. Hãy tính diện tích che phủ bởi
N hình chữ nhật này, biết rằng N hình chữ nhật này song song với 2 trục Ox và Oy .
Input
Dòng

1

:

Số

nguyên

N

(

1



N



10000


).

N dòng tiếp theo, mỗi dòng gồm 4 số nguyên x1, y1, x2, y2 tương ứng là toạ độ góc
6


trái dưới và góc phải trên của hình chữ nhật thứ i.( 0 ≤ x1 ≤
x2 ≤ 30000, 0 ≤ y1 ≤ y2 ≤ 30000 ).
Output
Gồm 1 dòng ghi ra diện tích phủ bởi N hình chữ nhật
Example
Input:
2
10 10 20 20
15 15 25 30
Output:
225
Thời gian chạy: 0.100s

Thuật toán:
Sử dụng Segment Tree (IT).
Chuyển dữ liệu đề cho sang một dãy tọa độ x, mỗi x có lưu lại y1 và y2 tương ứng
hàng giới hạn dưới và trên, đồng thời lưu lại type là -1 hay 1 tương ứng là cạnh
đóng hay mở. Sau đó sort lại mảng này theo x.
Mục đích của cách xử lí trên là tại mỗi khoảng từ xi -> xi+1 xét trên dãy đã sort ta đi
tính phần diện tích được bao phủ bởi các y1 và y2. Lúc này ta dùng Segment
Tree, mỗi nút lưu lại cnt là số lượng đoạn phủ và len là tổng chiều dài.
Code demo: />#include <iostream>
#include <fstream>
#include <sstream>

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>

void update(int k, int l, int r, int L, int R, int type)
{
if (r < L or R < l) return ;
if (L ≤ l and r ≤ R) {
t[k].cnt += type;
if (type == 1)
// them hcn nen bao phu
ca canh nay
t[k].len = (r-l+1); // chang han l = 5, r = 8

7


#include <climits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
using namespace std;
typedef long long
typedef int64_t
typedef double


ll;
ll;
real;

//-------------------------------------//

thi t[k].len = 4 do (5,6,7,8)
else {
// truong hop xoa thi phai
lay thong tin tu node con
if (t[k].cnt == 0)
t[k].len = t[k*2].len + t[k*2+1].len;
}
return ;
}
int mid = (l+r)/2;
update(k*2,l,mid,L,R,type);
update(k*2+1,mid+1,r,L,R,type);
if (t[k].cnt == 0)
t[k].len = t[k*2].len + t[k*2+1].len;
}

struct Node {
int x, y1, y2, type;
};

int main()
{
//freopen("INP.INP","r",stdin);
struct Tree {

//freopen("OUT.OUT","w",stdout);
int len, cnt;
scanf("%d", &n);
};
for (int i=1;i≤n;i++) {
int x1, y1, x2, y2;
//-----------------------------------//
scanf("%d%d%d%d", &x1,&y1,&x2,&y2);
const int N = 30010;
a[i].x = x1;
int
n, x0; // x0 la vi tri cuoi cung dang
a[i+n].x = x2;
xet
a[i].y1 = a[i+n].y1 = y1;
ll
res;
a[i].y2 = a[i+n].y2 = y2;
Node
a[N];
a[i].type = 1;
Tree
t[5*N]; // luu ve phuong dien do
a[i+n].type = -1;
dai, khong phai toa do (*)
}
//------------------------------------//
n *= 2;
sort(a+1,a+n+1,cmp);
bool cmp(const Node u, const Node v)

{
//for (int i=1;i≤n;i++)
return (u.x < v.x or (u.x == v.x and u.type
// printf("%d %d %d %d\n", a[i].x, a[i].y1,
< v.type));
a[i].y2, a[i].type);
}
for (int i=1;i≤n;i++) {
//cout << (a[i].x-x0)*(t[1].len) << endl;
res += (a[i].x - x0) * (t[1].len);
x0 = a[i].x;
update(1,0,N, a[i].y1, a[i].y2-1, a[i].type); //
a[i].y2-1 la do (*)
}
cout << res;
return 0;
}

Bài 4. />8


Cho danh sách N lập trình viên (1 ≤ N ≤ 300000), đánh số lần lượt từ 1 đến N. Mỗi
người đều tham gia cả hai giải thi đấu: Giải THPT và giải Mở rộng. Với mỗi lập trình
viên, bạn sẽ được cung cấp điểm số của giải Mở rộng Ai và điểm số của giải THPT
Hi (Các điểm số đều là số nguyên không âm và không vượt quá 100000). Lập trình
viên i được coi là giỏi hơn lập trình viên j khi và chỉ khi cả 2 điểm số của lập trình
viên i đều lớn hơn hoặc bằng điểm số tương ứng của lập trình viên j, trong đó có ít
nhất 1 điểm số phải lớn hơn. Hãy tính xem với mỗi lập trình viên i thì có bao nhiêu
lập trình viên mà i giỏi hơn.
Input

Dòng đầu tiên chứa số nguyên N.
N dòng tiếp theo, dòng thứ i+1 chứa 2 số nguyên Ai và Hi.
Output
Dòng i chứa số lượng lập trình viên mà lập trình viên i giỏi hơn.
Example
Input:
8
1798 1832
862 700
1075 1089
1568 1557
2575 1984
1033 950
1656 1649
1014 1473
Output:
6
0
2
4
7
1
5
1
Thuật toán:

9


Ở bài này, giới hạn của bài toán khá lớn (300000) nên nếu dùng N log N thì cũng còn

nguy hiểm. Chú ý giới hạn của số điểm là 0..100000 nên ta sẽ xử lí trên số
điểm.
+ Tạo một danh sách các thông tin của từng người: struct(C++) ds gồm d1,d2 là
điểm lần 1 và 2, vt là vị trí ban đầu của nó. Mảng A sẽ lưu danh sách này.
+ Tạo một mảng res sẽ chứa mảng cần xuất ra khi đã thực hiện xong chương trình.
Cách tạo res là for i = 1 -> n rồi lưu res[a[i].vt] = giá trị tìm được của thằng
a[i].vt (vị trí ban đầu của nó). Sau đó xuất ra theo thứ tự trong res.
+ Tạo một cây IT là mảng t[k] lưu thông tin của nút k quản lí đoạn l..r (xét trên số
điểm) và một mảng f lưu nghiệm của bài toán, tức là f[i] là số lượng những
thằng tồi hơn thằng i sau khi đã sort lại.
+ Tạo thêm biến m lưu số điểm d2 lớn nhất vì theo như tài liệu d1 ta đã sort lại rồi thì
không cần quan tâm điểm lớn hay nhỏ.
+ Sau khi nhập dữ liệu thì sort lại dãy.
+ Tạo biến q là vị trí đang duyệt trên dãy a ( từ 1 -> n, lần lượt xét các đoạn có cùng
d1). Khi q > n thì xuất nghiệm ra, halt luôn. trước hết q = 0.
+ Tạo biến L và R lưu đoạn xét nói trên. Khi đó ban đầu L = q và R = q. lần lượt tăng
R lên đến khi d1 thay đổi. Xong thì xét đoạn L..R mới tìm được.
+ Khởi tạo f[L] = 0. Tạo f.
+ Sau đó lấy giá trị cho f trên cây t (nhớ là xét trên số điểm từ 0..m, đoạn ban đầu
findans) : điểm đang xét là x. Ta sẽ tìm những đoạn mà r ≤ x để lấy ans còn
những đoạn nào mà x < l ( điểm thấp hơn ) thì ta bỏ qua. Trường hợp còn lại
thì chia ra 2 đoạn mà làm như các bài IT cơ bản.
+ Lấy giá trị xong thì cập nhật lại cây t: bỏ qua những đoạn x không thuộc, tăng số
lượng nút này lên, nếu l = r thì thôi. chia hai đoạn để it.
+ q = R.
+ Duyệt
Code demo:
#include <iostream>
#include <fstream>


bool compare(const ds u, const ds v)
{

10


#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>

if (u.d1 < v.d1) return true;
if (u.d1 == v.d1 and u.d2 < v.d2) return true;
return false;
}

using namespace std;

int main(){
scanf("%d",&n);
struct ds {
for (int i=1;i≤n;i++) {
int d1,d2,vt;
scanf("%d%d",&a[i].d1,&a[i].d2);
};
a[i].vt = i;
m = max(m,a[i].d2);
const int
maxn = 300010, maxt =
}

maxn*4 + 1;
//-------------------------------------//
int
n,m,q,L,R;
sort(a+1,a+n+1,compare);
ds
a[maxn];
q = 0;
int
t[maxt],f[maxn],res[maxn];
while (1 == 1) {
q++;
void findans(int k, int l, int r, int x, int &ans)
if (q > n) {
{
for (int i=1;i≤n;i++) res[a[i].vt] = f[i];
if (x < l) return;
for (int i=1;i≤n;i++) printf("%d\n",res[i]);
if (r ≤ x) {
return 0;
ans += t[k];
}
return;
L = q;
}
R = q;
int m = (l+r)/2;
while (R < n and a[R].d1 == a[R+1].d1) R+
findans(k*2,l,m,x,ans);
+;

findans(k*2+1,m+1,r,x,ans);
f[L] = 0;
}
for (int i=L+1;i≤R;i++)
if (a[i].d2 ≤ a[i-1].d2) f[i] = f[i-1];
void update(int k, int l, int r, int x)
else f[i] = i - L;
{
for (int i=L;i≤R;i++) {
if (x < l or r < x) return;
int ans = 0;
t[k]++;
findans(1,0,m,a[i].d2,ans);
if (l == r) return;
f[i] += ans;
int m = (l+r)/2;
}
update(k*2,l,m,x);
for (int i=L;i≤R;i++) update(1,0,m,a[i].d2);
update(k*2+1,m+1,r,x);
q = R;
}
}
return 0;
}

Bài 5. />Cho một dãy gồm n phần tử có giá trị ban đầu bằng 0.
Cho m phép biến đổi, mỗi phép có dạng (u, v, k): tăng mỗi phần tử từ vị trí u đến vị
trí v lên k đơn vị.


11


Cho q câu hỏi, mỗi câu có dạng (u, v): Cho biết phần tử có giá trị lớn nhất thuộc đoạn
[u, v]
Input
– n: số phần tử của dãy (n ≤ 50000).
– m: số lượng biến đổi và câu hỏi (m ≤ 100000).
+) biến đổi có dạng: 0 x y value
+) câu hỏi có dạng : 1 x y.
Output
Ghi ra trả lời cho lần lượt từng câu hỏi.
Example
Input:
63
0133
0464
116
Output:
4
Thuật toán:
Để làm bài này, ta gọi F[k] là giá trị lớn nhất trong đoạn mà nút k quản lí, trong lúc
cập nhật, muốn đảm bảo thủ tục này không vượt quá log(n) thì khi đi đến 1 nút mà
nằm gọn trong đoạn x..y thì ta không được đi vào các nút con của nó nữa (nếu không
sẽ đi đến tất cả các đoạn có trong đoạn x..y và độ phức tạp tỷ lệ với n).
Nếu như vậy, ta cần có 1 mảng phụ T để lưu lại giá trị cần tăng lên cho tất cả các
phần tử của đoạn này, khi ta truy vấn đến 1 nút k đồng thời ta tăng T[k] lên cho
T[k*2],
T[k*2+1] và F[k]. (do T[k] là giá trị cần tăng lên cho tất cả những phần tử của đoạn
nút k quản lí, nên các nút con của nó cũng phải tăng lên T[k]).

12


Sau khi đã cập nhật cho mảng F và các nút con, ta gán lại T[k]=0 để tránh trường hợp
cộng lại nhiều lần. Nếu đã đến đoạn nằm gọn trong đoạn x..y thì ta tăng mỗi biến
F[k], T[k*2], T[k*2+1] lên v. Khi muốn lấy kết quả, ta vẫn làm như bài QMAX,
nhưng đến mỗi nút con, ta vẫn cần thực hiện tăng giá trị T[k] cho T[k*2], T[k*2+1]
và F[k] để lấy được kết quả. Tức là khi đến được 1 nút nào đó (trong bất kì thao tác
nào, cập nhật hay lấy kết quả) thì đều thực hiện như vậy. Điều này đảm bảo là ta có
thể lấy được giá trị chính xác, đây là kiểu cây IT có thông tin truyền từ nút cha xuống
nút con. Các bạn có thể tham khảo chương trình để thấy rõ hơn.
#include <iostream>
#include <fstream>
#include <cmath>
#include <cstring>
#include <string>
#include <algorithm>
#include <cstdio>
#include <stdlib.h>
usingnamespace std;

void solve0(){
scanf("%li%li%li",&L,&R,&v);
update(1,1,n);
}
void solve1(){
scanf("%li%li",&L,&R);
res = 0;
findres(1,1,n);
printf("%li\n",res);

}

const long maxn = 50000 + 100;
long
int main()
n,m,L,R,loai,v,a[maxn],f[8*maxn],t[8*maxn],res {
;
scanf("%li%li",&n,&m);
/**
for(long i=1;i≤m;i++){
t[k] la max cua doan nut k quan li
scanf("%li",&loai);
f[k] la mang phu luu phan can tang len cua
if(loai == 0) solve0();
doan nut k
else solve1();
quan li
}
**/
return0;
void update(long k,long l,long r){
}
t[k] += f[k];
f[k*2] += f[k];
f[k*2+1] += f[k];
f[k] = 0;
if(r < L or R < l)return;
if(L ≤ l and r ≤ R){
t[k] += v;
f[k*2] += v;

f[k*2+1] += v;
return;
}
long m = (l+r)/2;
update(k*2,l,m);
update(k*2+1,m+1,r);
t[k] = max(t[k*2],t[k*2+1]);
}

13


void findres(long k,long l,long r){
t[k] += f[k];
f[k*2] += f[k];
f[k*2+1] += f[k];
f[k] = 0;
if(r < L or R < l)return;
if(L ≤ l and r ≤ R){
res = max(res,t[k]);
return;
}
long m = (l+r)/2;
findres(k*2,l,m);
findres(k*2+1,m+1,r);
}

Bài 6. />Hàng ngày khi lấy sữa, N con bò của bác John (1 ≤ N ≤ 50000) luôn xếp hàng theo
thứ tự không đổi. Một hôm bác John quyết định tổ chức một trò chơi cho một số con
bò. Để đơn giản, bác John sẽ chọn ra một đoạn liên tiếp các con bò để tham dự trò

chơi. Tuy nhiên để trò chơi diễn ra vui vẻ, các con bò phải không quá chênh lệch về
chiều cao. Bác John đã chuẩn bị một danh sách gồm Q (1 ≤ Q ≤ 200000) đoạn các
con bò và chiều cao của chúng (trong phạm vi [1, 1000000]). Với mỗi đoạn, bác John
muốn xác định chênh lệch chiều cao giữa con bò thấp nhất và cao nhất. Bạn hãy giúp
bác John thực hiện công việc này.
Dữ liệu


Dòng đầu tiên chứa 2 số nguyên N và Q.



Dòng thứ i trong số N dòng sau chứa 1 số nguyên duy nhất, là độ cao của con
bò thứ i.



Dòng thứ i trong số Q trong tiếp theo chứa 2 số nguyên A, B (1 ≤ A ≤ B ≤ N),
cho biết đoạn các con bò từ A đến B.

Kết quả
Gồm Q dòng, mỗi dòng chứa 1 số nguyên, là chênh lệch chiều cao giữa con bò thấp
nhất và cao nhất thuộc đoạn tương ứng.
Ví dụ
Dữ liệu:
63
14


1

7
3
4
2
5
15
46
22
Kết qủa
6
3
0
Thuật toán:
Đây là 1 bài toán xử lí trên dãy số và cần truy cập đến những đoạn A..B bất kì trong
dãy, vì vậy interval tree là 1 trong những lựa chọn tốt. Bài này chúng ta có 1 điều
may mắn là không cần phải cập nhật lại chiều cao của các con bò, vì vậy thông tin
trong cây interval tree là cố định và ta sẽ tạo cây interval tree dựa trên mảng chiều
cao của các con bò.
Mỗi đoạn thì ta cần in ra chênh lệch độ cao con bò cao nhất và con bò thấp nhất, vì
vậy chúng ta cần tìm được giá trị lớn nhất và giá trị nhỏ nhất trong các phần tử từ A
đến B. Ta có thể dùng 1 cây interval tree với mỗi nút lưu 2 thông tin, giá trị lớn nhất
và giá trị nhỏ nhất trong đoạn mà nó biểu diễn, cũng có thể dùng 2 cây interval tree, 1
cây dùng để lưu giá trị lớn nhất, cây còn lại là giá trị nhỏ nhất. Ở đây ta gọi 2 cây này
là maxt và mint.
Khi muốn tìm kết quả thì dựa vào mảng Maxt để tìm GTLN trên đoạn A..B, dùng
mảng Mint để tìm GTNN trên đoạn A..B, việc này làm tương tự như trong ví dụ của
bài viết giới thiệu về Interval tree phía trên. Chú ý là khi tìm max hay tìm min ta đều
phải đi đến những nút giống nhau (do đi đến những nút nào thì chỉ phụ thuộc A và B)
nên mỗi lần tìm chỉ cần gọi chung 1 thủ tục.
#include <iostream>

#include <cmath>

int main()

15


#include <algorithm>
{
#include <cstdio>
scanf("%li%li",&n,&q);
#include <stdlib.h>
for (long i=1;i≤n;i++) scanf("%li",&a[i]);
using namespace std;
update(1,1,n);
const long maxn = 50000+100, maxh =
for (long Q=1;Q≤q;Q++) {
1000000;
scanf("%li%li",&L,&R);
long
hmax = 1;
n,q,L,R,a[maxn],tmax[4*maxn],tmin[4*maxn],h
hmin = maxh;
max,hmin;
findres(1,1,n);
printf("%li\n",hmax-hmin);
void update(long k,long l,long r) {
}
if (l == r) tmin[k] = tmax[k] = a[l];
return 0;

else {
}
long m = (l+r)/2;
update(k*2,l,m);
update(k*2+1,m+1,r);
tmax[k] = max(tmax[k*2],tmax[k*2+1]);
tmin[k] = min(tmin[k*2],tmin[k*2+1]);
}
}
void findres(long k,long l,long r) {
if (not(r < L or R < l)) {
if (L ≤ l and r ≤ R) {
hmax = max(hmax,tmax[k]);
hmin = min(hmin,tmin[k]);
}
else {
long m = (l+r)/2;
findres(k*2,l,m);
findres(k*2+1,m+1,r);
}
}
}

Bài 7. />Cho một dãy gồm n phần tử có giá trị ban đầu bằng 0.
Cho m phép biến đổi, mỗi phép có dạng (u, v, k): tăng mỗi phần tử từ vị trí u đến vị
trí v lên k đơn vị.
Cho q câu hỏi, mỗi câu có dạng (u, v): cho biết phần tử có giá trị lớn nhất thuộc đoạn
[u, v].
Giới hạn



n, m, q ≤ 50000



k>0



Giá trị của một phần tử luôn không vượt quá 231-1
16


Input


Dòng 1: n, m



m dòng tiếp theo, mỗi dòng chứa u, v, k cho biết một phép biến đổi



Dòng thứ m+2: p



p dòng tiếp theo, mỗi dòng chứa u, v cho biết một phép biến đổi


Output


Gồm p dòng chứa kết quả tương ứng cho từng câu hỏi.

Example
Input:
62
132
463
1
34
Output:
3
Thuật toán:
Bài này thì ta có m phép biến đổi tăng dãy trước rồi mới yêu cầu tìm giá trị lớn nhất
trong các đoạn chứ không phải xen kẽ nhau, vì vậy ta sẽ tìm cách xây dựng dãy số
sau m phép biến đổi. Khi có được mảng giá trị sau m phép biến đổi, công việc còn lại
của ta là tạo 1 cây interval tree với mỗi nút lưu giá trị lớn nhất của đoạn mà nó quản lí
trong khi các phần tử của mảng đã xác định, với mối truy vấn tìm GTLN thì ta có thể
làm không mấy khó khăn.
Vấn đề bây giờ là xây dựng dãy số sau m phép biến đổi.
Ta có thể sử dụng 1 kĩ thuật đơn giản những rất hiệu quả như sau.
Giả sử mảng ta cần có là mảng A[0..n+1], lúc đầu A[i]=0 với mọi i.
Mỗi yêu cầu u,v,k tức là tăng các phần tử từ vị trí u đến vị trí v lên k đơn vị, ta làm
như sau: A[u]:=A[u]+k;A[v+1]:=A[v+1]-k;
Sau khi đọc xong m phép biến đổi và làm như trên, cuối cùng là tính mảng A:
For i:=1 to n do
17



A[i]:=A[i]+A[i-1];
Các bạn có thể tự chứng minh tính đúng đắn hay có thể viết đoạn chương trình này ra
và kiểm nghiệm lại. Như vậy ta đã có thể giải quyết trọn vẹn bài toán.
#include <iostream>
void findres(long k,long l,long r) {
#include <cmath>
if (not(r < L or R < l)) {
#include <algorithm>
if (L ≤ l and r ≤ R)
#include <cstdio>
res = max(res,t[k]);
#include <stdlib.h>
else {
using namespace std;
long m = (l+r)/2;
const long maxn = 50000+100;
findres(k*2,l,m);
long
n,m,k,u,v,q,L,R,a[maxn],t[4*maxn],res;
findres(k*2+1,m+1,r);
}
void update(long k,long l,long r) {
}
if (l == r) t[k] = a[l];
}
else {
int main()
long m = (l+r)/2;
{

update(k*2,l,m);
scanf("%li%li",&n,&m);
update(k*2+1,m+1,r);
for (long i=1;i≤m;i++) {
t[k] = max(t[k*2],t[k*2+1]);
scanf("%li%li%li",&u,&v,&k);
}
a[u] += k;
}
a[v+1] -= k;
}
for (long i=1;i≤n;i++) a[i] += a[i-1];
update(1,1,n);
scanf("%li",&q);
for (long Q=1;Q≤q;Q++) {
scanf("%li%li",&L,&R);
res = 0;
findres(1,1,n);
printf("%li\n",res);
}
return 0;}

Bài 8. Dãy ngoặc />Khái niệm dãy ngoặc đúng được định nghĩa dưới dạng đệ quy như sau:
1. () là dãy ngoặc đúng
2. C là dãy ngoặc đúng nếu C = (A) hay C = AB với A, B là các dãy ngoặc đúng.
Ví dụ dãy ngoặc đúng: (), (()), ()(), (())()
Ví dụ dãy ngoặc sai: )(, ((((, ()((, )))), )()(
Cho trước một dãy ngoặc bất kỳ gồm n dấu ngoặc được đánh số từ 1 đến n. Có m câu
hỏi, mỗi câu gồm hai số nguyên 1 ≤ p ≤ r ≤ n, yêu cầu xác định xem dãy ngoặc
18



con từ vị trí p đến vị trí r có phải là dãy ngoặc đúng hay không. Hãy cho biết kết quả
của m câu hỏi trên.
Dữ liệu nhập:
– Dòng đầu tiên là hai số nguyên n, m cách nhau một khoảng trắng (1 ≤ n, m ≤ 105)
– Dòng thứ hai là dãy ngoặc ban đầu gồm n dấu ngoặc ‘(‘ hay ‘)’.
– Trong m dòng tiếp theo, tại dòng thứ i là hai số p i và ri của câu hỏi thứ i tương ứng,
hai số cách nhau một khoảng trắng. (1 ≤ pi ≤ ri ≤ n).
Dữ liệu xuất:
– Gồm m dòng ứng với m câu hỏi, nếu dãy ngoặc con tương ứng là dãy ngoặc đúng,
in ra YES, nếu dãy ngoặc con tương ứng là không phải dãy ngoặc đúng, in ra NO.
Ví dụ
input



85
(((())))
14
36
48
45
27
output
NO
YES
NO
YES
YES

Thuật toán:
#include <bits/stdc++.h>
using namespace std;
int n, m, x, y, bb[400001], cc[400001];
char ch;

bool check(int u, int v)
{
if ((v-u+1) & 1) return 0;
int y = (u == 1 ? 0 : get(1, 1, n, u-1, u-1));

19


void update(int k, int x, int y, int d, int c, int v)
{
if (cif (d≤x && y≤c) bb[k]+=v, cc[k]+=v;
else
{
int m = (x+y)/2;
update(2*k, x, m, d, c, v);
update(2*k+1, m+1, y, d, c, v);
bb[k] = min(bb[2*k],bb[2*k+1]) + cc[k];
}
}
int get(int k, int x, int y, int d, int c)
{
if (cif (d≤x && y≤c) return bb[k];

else
{
int m=(x+y)/2;
return min(get(2*k, x, m, d, c), get(2*k+1,
m+1, y, d, c)) + cc[k];
}
}

int z = get(1, 1, n, v, v);
if (y != z) return 0;
else return (get(1, 1, n, u, v-1) >= z);
}
main()
{
scanf("%d %d\n", &n, &m);
for (int i=1; i≤n; i++)
{
scanf("%c", &ch);
x += (ch == '(') ? 1 : -1;
update(1, 1, n, i, i, x);
}
while (m--)
{
scanf("%d %d\n", &x, &y);
printf("%s\n", check(x,y) ? "YES" :
"NO");
}
}

Bài 9. Búp bê Nga

Búp bê Nga(Búp bê lồng nhau, Búp bê làm tổ, …) là một loại búp bê đặc trưng của
Nga. Thật ra đó là một bộ gồm những búp bê rỗng ruột có kích thước từ lớn đến nhỏ.
Con búp bê nhỏ nhất sẽ được chứa đựng trong lòng con búp bê lớn hơn nó một chút,
đến lượt mình con búp bê lớn được chứa trong một con búp bê khác lớn hơn, và cứ
thế cho đến con lớn nhất sẽ chứa tất cả những con búp bê còn lại trong bộ.
Đầu tiên yenthanh132 sắp xếp N con búp bê của anh ta từ trái sang phải theo một thứ
tự bất kì, con thứ i có kích thước ai (1 ≤ ai ≤ M). Sau đó anh ta yêu cầu bạn tạo
khoảng trống ở đầu mút trái, để làm được điều đó, bạn cần xếp những con búp bê có
kích thước nhỏ hơn ở bên trái đặt vào những con búp bê có kích thước lớn hơn bên
phải, tuy nhiên mỗi con búp bê lớn hơn chỉ chứa đúng 1 con búp bê nhỏ hơn nó. Bạn
chỉ được quyền dùng 3 thao tác: Lấy một con búp bê ở bên trái lên, di chuyển nó sang
phải và đặt vào bên trong một con búp bê lớn hơn. Bạn cần tìm cách sắp xếp sao cho
khoảng trống ở đầu mút trái là lớn nhất.
20


Nói cách khác, yenthanh132 yêu cầu bạn tìm một số nguyên K lớn nhất (1 ≤ K ≤ N/2)
sao cho K con búp bê trái nhất có để đặt vào bên trong K con búp bê ngay sau đó (từ
k+1..k*2), mỗi con búp bê chỉ chứa đúng 1 con búp bê, theo một thứ tự nào đó. Lưu ý
con búp bê i có thể đặt vào bên trong con búp bê j nếu (ai < aj).
Dữ liệu vào:
Dòng đầu tiên có 2 số N, M lần lượt là số lượng con búp bê của yenthanh132 và kích
thước giới hạn của N con búp bê đó. N luôn là số chẵn.
Dòng thứ 2 chứa N số ai, là kích thước của N con búp bê từ trái sang phải. (1
≤ ai ≤ M).
Dữ liệu ra:
Xuất ra số K theo yêu cầu của bài. Nếu không có kết quả (không có cách sắp nào để
tạo khoảng trống ở đầu mút trái ) thì xuất ra -1.
Giới hạn test:
20% test đầu có (1 ≤ N ≤ 100; 1 ≤ M ≤ 1000).

20% test tiếp theo có (1 ≤ N ≤ 10000; 1 ≤ M ≤ 1000).
60% test còn lại có (1 ≤ N ≤ 105; 1 ≤ M ≤ 105).
Ví dụ:
Input:
10

5

2142324523
Output:
4
Giải thích:
Ta có thể đặt 4 con búp bê bên trái vào trong 4 con búp bê bên cạnh theo thứ tự như
sau: 1 đặt vào 5, 2 đặt vào 6, 3 đặt vào 8, 4 đặt vào 7.
Input 2:
45
2211
Output 2:
-1
21


Thuật toán: Nguồn Lê Yên Thanh - o
Cách làm O(n*m):
– Đầu tiên ta cho biến k chạy từ n/2 về 1. Với mỗi giá trị của k, ta gọi l[i] là số lượng
các phần tử có giá trị bằng i trong đoạn từ 1..k; r[i] là số lượng các phần tử bằng i
trong đoạn từ k+1..k*2; với i từ 1 đến m.
– Ta định nghĩa thêm một mảng d[1..m] như sau: d[m]=-l[m]; d[i] = d[i+1] + r[i+1] –
l[i] (với i từ 1 đến m-1).
– Ta có thể nhận thấy một điều là tồn tại một cách sắp k con búp bê ở đoạn từ 1..k

vào k con búp bê ở đoạn k+1..k*2 khi và chỉ khi d[i]>=0 với mọi i từ 1 đến m.
– Như vậy, với mỗi giá trị của k, ta sẽ xây dựng lại các giá trị của mảng l, r, d và thực
hiện kiểm tra điều kiện d[i] ≥ 0 với mọi i từ 1 đến m.
– Cách làm này sẽ ăn được 40% số điểm.
Cách làm O(n*log(m))
– Ta có thể thấy trong quá trình duyệt k từ n/2 về 1. Mỗi lần giảm giá trị k đi 1 đơn vị
thì ta sẽ giảm một phần tử trong mảng l[1..m] 1 đơn vị, tăng 1 phần tử và giảm 2 phần
tử trong mảng r[1..m] 1 đơn vị.
– Ta có thể thấy rằng giá trị của phần tử d[i] = r[m] + r[m-1] + … + r[i+1] – l[m] –
l[m-1] – … – l[i]. Như vậy, một thao tác tăng/giảm phần tử r[i] 1 đơn vị sẽ tương ứng
với thao tác tăng/giảm các phần tử trong đoạn d[1..i-1] 1 đơn vị; một thao tác
tăng/giảm phần tử l[i] 1 đơn vị sẽ tương ứng với thao tác tăng/giảm các phần tử trong
đoạn d[1..i] 1 đơn vị.
– Như vậy ta có thể sử dụng một Interval Tree để thực hiện 2 thao tác: Tăng/giảm giá
trị của 1 đoạn phần tử trong log(m); Lấy giá trị của phần tử nhỏ nhất trong O(1). Với
mỗi lần duyệt của biến k, ta sẽ thao tác cập nhật trên cây Interval Tree này và nếu giá
trị của phần tử nhỏ nhất trong mảng d[1..m] là một số không âm thì k chính là kết quả
cần tìm.
– Cách làm này sẽ đạt được toàn bộ số điểm.

22


Bài 10. Bật đèn (LITES)
Bác John giữ cho đàn bò thông minh bằng cách để chúng chơi các đồ chơi phát triển
trí tuệ. Một trong các trò chơi là các ngọn đèn trong chuồng. Mỗi trong số N (2 ≤ N ≤
100,000) con bò được đánh số từ 1..N có treo một ngọn đèn màu.
Vào đầu buổi tối, tất cả đèn đều tắt. ðàn bò điều khiển các ngọn đèn bằng N công tắc;
bấm công tắc i đổi trạng thái của đèn i từ tắt sang bật hoặc ngược lại.
Đàn bò đọc và thực thi một danh sách gồm M (1 ≤ M ≤ 100,000) thao tác mô tả bởi

một trong hai số nguyên (0 ≤ thao tác ≤ 1).
Thao tác thứ nhất (mô tả bởi số 0) theo sau bởi hai số nguyên S_i và E_i (1 ≤ S_i ≤
E_i ≤ N) cho biết công tắc đầu và công tắc cuối. Đàn bò sẽ bấm mỗi công tắc từ S_i
đến E_i đúng một lần.
Thao tác thứ hai (mô tả bởi số 1) yêu cầu đàn bò đến xem có bao nhiêu ngọn đèn giữa
S_i và E_i (1 ≤ S_i ≤ E_i ≤ N) đang bật. Hãy giúp bác John đảm bảo rằng đàn bò trả
lời đúng bằng cách xử lý danh sách và trả về các kết quả đúng.
Dữ liệu:
* Dòng 1: Hai số nguyên cách nhau bởi khoảng trắng: N và M
* Dòng 2..M+1: Mỗi dòng chứa một thao tác với ba số nguyên cách nhau bởi khoảng
trắng: thao tác, S_i, và E_i
Kết quả:
* Dòng 1..số truy vấn: Với mỗi truy vấn, in ra kết quả là một số nguyên trên một
dòng.
Ví dụ:
Dữ liệu:
45
012
024
123
024
114
Kết quả:
23


1
2
Thuật toán:
Ta có thể sử dụng INTERVAL TREE để làm như sau: Bài này có tư tưởng khá giống

với bài QMAX2, thông tin được truyền từ nút cha xuống nút con bất cứ khi nào có
thể. Ta có 1 mảng B kiểu logic, B[i]=true nghĩa là những đèn trong đoạn mà nút thứ i
quản lí cần thay đổi trạng thái. Khi đến 1 nút, nếu b[i]=true thì cần thay đổi trạng thái
của B[i*2] và B[i*2+1] đồng thời gán lại B[i]=false (đã thực hiện thay đổi rồi thì phải
gán lại false),nếu gọi mảng sl[i] là số lượng đèn đang bật trong đoạn do nút i quản lí
thì nếu gặp b[i]=true phải tiến hành sửa sl[i], sl[i] hiện tại là giá trị của số đèn đang
tắt( do khi thay đổi trạng thái thì bật chuyển thành tắt). Vậy chỉ cần 1 phép trừ là có
thể tính được số lượng đèn đang bật trong đoạn.
Các công việc này cần làm ở đầu ở cả 2 thủ tục cập nhật và lấy kết quả.
Khi thực hiện thao tác cập nhật kết quả cho đoạn x..y thì nếu gặp những đoạn nằm
gọn trong x..y thì ta lại thực hiện thay đổi trạng thái như trên. Phần lấy kết quả thì
không khó, các bạn chỉ cần nhớ là trong thủ tục lấy kết quả cũng cần thực hiện truyền
thông tin từ nút cha cho nút con( bất cứ khi nào có thể).
BÀI TẬP ĐỀ XUẤT GIẢI BẰNG INTERVAL TREE
Bài 11. A Marble Game(MARBLE)
Trong những ngày hè rảnh rỗi, ktuan thường chơi bắn bi trên một bảng hình vuông
gồm NxN ô vuông nhỏ. Trò chơi được thực hiện như sau:
- Ban đầu, ktuan đặt K vật cản vào K ô vuông của bảng.
- Sau đó, ktuan thực hiện lần lượt Q lượt chơi. Ở lượt chơi thứ i, ktuan lần lượt bắn
Di viên bi từ ngoài bảng vào một trong 4 đường biên của bảng. Kích thước của mỗi
viên bi đúng bằng kích thước của một ô vuông nhỏ. Viên bi sẽ đi qua các ô thuộc
cùng một hàng / cột cho đến khi đi ra ngoài bảng hoặc gặp một vật cản hay viên bi
khác. Nếu có vật cản hay viên bi khác ở ngay ô đầu tiên thì viên bi đó không được
đưa vào bảng.
24


- Ở mỗi lượt bắn, ktuan ghi lại tổng số ô mà các viên bi đã đi qua.
Bạn hãy viết chương trình mô phỏng lại trò chơi và với mỗi lượt bắn, in ra tổng số ô
mà các viên bi của lượt bắn đó đã đi qua.

Dữ liệu
- Dòng đầu ghi 3 số N, K, Q.
- K dòng sau, mỗi dòng ghi một cặp số (u,v) thể hiện toạ độ (dòng, cột) của một vật
cản.
- Q dòng sau, mỗi dòng ghi 4 giá trị c, D, u, v. Ký tự c có thể là 'L', 'R', 'T', hoặc 'B'
cho biết viên bi
được đưa vào từ biên trái, phải, trên hoặc dưới của bảng. (u,v) thể hiện toạ độ ô đầu
tiên mà viên bi
được đưa vào. ðây phải là một ô nằm trên biên của bảng ứng với ký tự c. D là số
lượng viên bi sẽ
bắn ở lượt chơi này.
Kết quả
Với mỗi lượt chơi, in ra tổng số ô mà các viên bi của lượt đó đã đi qua.
Ví dụ
Dữ liệu
513
33
L231
T111
B555
Kết quả
3
2
25
Giải thích
Viên bi đầu tiên của lượt 1 sẽ đi qua 2 ô (3,1) và (3,2) trước ghi gặp vật cản ở ô
(3,3)
25



×