1. Trang chủ
  2. » Luận Văn - Báo Cáo

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

27 353 1

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 27
Dung lượng 88,32 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

CHUYÊN ĐỀ: CẤU TRÚC DỮ LIỆU NÂNG CAOInterval 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à

Trang 1

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ềucô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:

https://doraemonvodanh.wordpress.com/category/thuat-toan/segment-tree/bai-• Quyển: Một số vấn đề chú ý môn Tin học – Nhóm tác giả của Đại học Vinh

Trang 2

Ứng dụng Interval Tree để giải các bài toán sau:

Bài 1 Phần tử thứ K http://vn.spoj.com/problems/YPKTH/

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,

- 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

THUẬT TOÁN :

Trang 3

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: http://ideone.com/GTScHq

while (i < u.size()) ans.pb(u[i++]);

while (j < v.size()) ans.pb(v[j++]);

int mid = (i+j)/2;

if (t[node][mid] ≤ res) { pos = mid;

i = mid+1;

} else j = mid-1;

int mid = (l+r)/2;

get(node*2,l,mid);

get(node*2+1,mid+1,r);

} int main() {

Trang 4

int l = 0, r = t[1].size()-1;

while (l ≤ r) { int mid = (l+r)/2;

}

Bài 2 Đoạn con có tổng lớn nhất http://vn.spoj.com/problems/GSS/

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)

Trang 5

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

typedef long long ll;

data make_data (int x) {

if (l == r) { t[k] = make_data(a[l]);

return ; }

Trang 6

typedef int64_t ll;

typedef double real;

const int base = 1000000007;

const int oo = INT_MAX;

const ll ooo = LONG_LONG_MAX;

const real pi = acos(-1.0);

#define readln scanf("\n")

#define writeln printf("\n")

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 +

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

} int main() {

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 - http://vn.spoj.com/problems/AREA/

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

Trang 7

trái dưới và góc phải trên của hình chữ nhật thứ i.( 0 ≤ x1 ≤

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: http://ideone.com/tGGNUI

if (type == 1) // them hcn nen bao phu

ca canh nay t[k].len = (r-l+1); // chang han l = 5, r = 8

Trang 8

Tree t[5*N]; // luu ve phuong dien do

dai, khong phai toa do (*)

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() {

a[i+n].x = x2;

a[i].y1 = a[i+n].y1 = y1;

a[i].y2 = a[i+n].y2 = y2;

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 http://vn.spoj.com/problems/CRATE/

Trang 9

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

Trang 10

Ở 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

Trang 11

if (u.d1 < v.d1) return true;

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

return false;

} int main(){

sort(a+1,a+n+1,compare);

q = 0;

while (1 == 1) { q++;

if (q > n) { for (int i=1;i≤n;i++) res[a[i].vt] = f[i];

for (int i=1;i≤n;i++) printf("%d\n",res[i]); return 0;

for (int i=L+1;i≤R;i++)

if (a[i].d2 ≤ a[i-1].d2) f[i] = f[i-1];

q = R;

} return 0;

}

Bài 5 http://vn.spoj.com/problems/QMAX2/

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ị

Trang 12

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

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

Trang 13

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

t[k] la max cua doan nut k quan li

f[k] la mang phu luu phan can tang len cua

}

Trang 14

void findres(long k,long l,long r){

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

Trang 15

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

Trang 16

void update(long k,long l,long r) {

if (l == r) tmin[k] = tmax[k] = a[l];

for (long i=1;i≤n;i++) scanf("%li",&a[i]); update(1,1,n);

for (long Q=1;Q≤q;Q++) { scanf("%li%li",&L,&R);

}

Bài 7 http://vn.spoj.com/problems/QMAX/

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]

Trang 17

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

Trang 18

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

else { long m = (l+r)/2;

findres(k*2,l,m);

findres(k*2+1,m+1,r);

} } } int main() {

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 http://laptrinh.ntu.edu.vn/Problem/Details/3270/?contestid=9

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

Trang 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ố pi 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

if ((v-u+1) & 1) return 0;

int y = (u == 1 ? 0 : get(1, 1, n, u-1, u-1));

Trang 20

void update(int k, int x, int y, int d, int c, int v)

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

Ngày đăng: 02/06/2016, 21:54

TỪ KHÓA LIÊN QUAN

w