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

CHUYÊN đề 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 (179.25 KB, 28 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 tốn xử lí trên dãy số, đặc biệt là các bài tố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 tố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 tốn đó để học sinh “ngấm” bài tố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 tố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
Thời gian chạy:

551
1s-3s

2



THUẬT TOÁN :

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++]);

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);
}

3


while (j < v.size()) ans.pb(v[j++]);
return ans;
}
void build(int k, int l, int r)
{
if (l == r) {
t[k].pb(a[l]);
return ;
}
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]);
}

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);
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;
}

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.
4


- 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
Output:
2
Thời gian chạy:

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);
5



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;
typedef long long ll;
typedef int64_t ll;
typedef double
real;
const int
const int
const ll
const real

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

t[k] = combine(t[k*2], t[k*2+1]);
}
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;
};
const int maxn = 100000, maxt = 4*maxn;
int
n, a[maxn], m, L, R;


}
int main()
{
//openf;
scanf("%d", &n);

6


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;
}

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 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:

7


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>
#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;

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

struct Tree {
int len, cnt;
};
//-----------------------------------//

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
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;
}


int main()
{
//freopen("INP.INP","r",stdin);
//freopen("OUT.OUT","w",stdout);
scanf("%d", &n);
for (int i=1;i≤n;i++) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1,&y1,&x2,&y2);

8


const int N = 30010;
int
n, x0; // x0 la vi tri cuoi cung dang
xet
ll
res;
Node
a[N];
Tree
t[5*N]; // luu ve phuong dien do
dai, khong phai toa do (*)
//------------------------------------//

a[i].x = x1;
a[i+n].x = x2;
a[i].y1 = a[i+n].y1 = y1;
a[i].y2 = a[i+n].y2 = y2;
a[i].type = 1;

a[i+n].type = -1;
}
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. />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 q 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.
9


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:
Ở 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.

10


+ 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 ln. 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>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>

bool compare(const ds u, const ds v)
{
if (u.d1 < v.d1) return true;
if (u.d1 == v.d1 and u.d2 < v.d2) return true;
return false;
}

11


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ị.
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
12


– 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 q 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],


13


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]).
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;

14


update(k*2,l,m);
update(k*2+1,m+1,r);
t[k] = max(t[k*2],t[k*2+1]);
}
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) ln 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 q 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ố ngun 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ụ
15



Dữ liệu:
63
1
7
3
4
2
5
15
46
22
Kết qủa
6
3
0
Thuật tốn:
Đây là 1 bài tố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.
16


#include <iostream>
#include <cmath>
int main()
#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
17




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

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 tố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:
18



For i:=1 to n do
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 tố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

19


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 tố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));

20


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 (c

if (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.
21


Nói cách khác, yenthanh132 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 ln 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
22


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 q 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 tồn bộ số điểm.

23


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) 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ố ngun trên một
dịng.
Ví dụ:
Dữ liệu:
45
012
024
123
024
114
Kết quả:
24


1
2
Thuật tố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 vng
gồm NxN ơ vng 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
25


×