Cây tìm kiếm nhị phân – Binary search tree

Bài số 8 trong 8 bài của series Cấu trúc dữ liệu

Trong bài viết này, chúng ta sẽ tiếp tục tìm hiểu về cấu trúc dữ liệu Cây, và cụ thể là cây tìm kiếm nhị phân. Đây là một cấu trúc dữ liệu được dùng khá phổ biến và có tính ứng dụng cao. Hãy cùng Nguyễn Văn Hiếu tìm hiểu và cài đặt cây tìm kiếm nhị phân sử dụng ngôn ngữ C/C++ nhé.

1. Lý thuyết về cây tìm kiếm nhị phân

Cây tìm kiếm nhị phân(TA: Binary Search Tree – viết tắt: BST) – là một cây nhị phân và có thêm các ràng buộc sau đây:

  1. Giá trị của tất cả các Node ở cây con bên trái phải <= giá trị của Node gốc.
  2. Giá trị của tất cả các Node ở cây con bên phải phải > giá trị của Node gốc.
  3. Tất cả các cây con(bao gồm bên trái và phải) cũng đều phải đảm bảo 2 tính chất trên.
Nhận biết cây tìm kiếm nhị phân
Nhận biết cây tìm kiếm nhị phân. Hình thứ 2 không phải BST do vi phạm ràng buộc số 2

Cây tìm kiếm nhị phân là một cấu trúc dữ liệu hiệu quả cho phép chúng ta xây dựng nên một danh sách mà dữ liệu trên đó được sắp xếp:

  • Nó được gọi là cây nhị phân vì mỗi Node của cây chỉ có tối đa hai con
  • Nó được gọi là cây tìm kiếm nhị phân vì nó có thể được sử dụng để tìm kiếm sự hiện diện của một phần tử trong thời gian O(log (n)).

2. Cài đặt cây BST

Sau đây, tôi sẽ cùng các bạn đi cài đặt cây tìm kiếm nhị phân sử dụng danh sách liên kết. Do đó, cái bạn cần lưu giữ là Node root của cây mà thôi. Có được root chúng ta có thể duyệt qua mọi phần tử của cây.

Đầu tiên, chúng ta sẽ định nghĩa kiểu dữ liệu cho các Node của cây:

Tiếp theo, chúng ta sẽ triển khai code cho tức chức năng của cây BST. Chúng ta sẽ sử dụng đệ quy rất nhiều trong các chức năng này. Bạn có thể tự tìm hiểu cách viết khử đệ quy nhé.

2.1. Hàm giải phóng dữ liệu

Chúng ta sẽ sử dụng hàm này khi muốn xóa một cây con hoặc toàn bộ cây bằng cách cung cấp Node root của cây muốn xóa. Việc giải phóng được thực hiện theo trình tự:

  1. Gọi đệ quy xóa cây con bên trái
  2. Gọi đệ quy xóa cây con bên phải
  3. Giải phóng vùng nhớ cho con trỏ hiện tại

2.2. Hàm điều hướng của cây

2 hàm dưới đây sẽ phục vụ chúng ta trong quá trình Insert(thêm phần tử vào cây) và Tìm kiếm trên cây tìm kiếm nhị phân.

Nếu bạn cho phép cây BST có giá trị trùng lặp, hãy lưu ý hàm LeftOf nhé. Ở đây tôi sẽ không cho phép thêm phần tử đã có trên cây(bỏ qua trùng lặp giá trị).

2.3. Thêm phần tử vào BST

Việc thêm 1 phần tử vào cây nhị phân tìm kiếm vẫn phải đảm bảo được các ràng buộc của một BST đã trình bày ở trên. Như vậy, bạn cần phải tìm kiếm vị trí thích hợp trong BST để lưu giữ nó.

Nếu bạn để ý, bạn sẽ nhận ra vị trí của các Node được thêm vào sẽ luôn Node lá(không có Child nào hết). Như vậy, tại vị trí đó trước khi các Node mới tới ở thì nó là NULL. Ta có quy trình như sau:

  1. Nếu Node hiện tại = NULL, đó là vị trí cần thêm. Thêm vào BST và kết thúc
  2. Nếu giá trị cần thêm < giá trị root hiện tại, gọi đệ quy Insert vào cây con bên trái
  3. Nếu giá trị cần thêm > giá trị root hiện tại, gọi đệ quy Insert vào cây con bên phải.

Hãy xem hình ảnh mô phỏng việc thêm giá trị 4 vào BST dưới đây:

Thêm Node vào cây tìm kiếm nhị phân

2.4. Tìm kiếm trên BST

Việc tìm kiếm cũng tương tự như việc thêm phần tử vào BST. Ta có quy trình như sau:

  1. Nếu Node hiện tại có giá trị = giá trị cần tìm, trả về true và kết thúc.
  2. Nếu Node hiện tại có giá trị > giá trị cần tìm, gọi đệ quy tìm ở cây con bên trái.
  3. Nếu Node hiện tại có giá trị < giá trị cần tìm, gọi đệ quy tìm ở cây con bên phải
  4. Nếu tìm đến hết cây(Node đó = NULL) mà không xảy ra (1), trả về false và kết thúc.

Chúng ta sẽ quan sát cách BST tìm kiếm giá trị 4 trong hình ảnh dưới đây:

Tìm kiếm trên cây tìm kiếm nhị phân

2.5. Lấy giá trị con trái nhất

Rất đơi giản, duyệt theo con trỏ trỏ đến cây con bên trái tới chừng nào không còn con bên trái nữa thì đó là con trái nhất rồi.

Hàm này sẽ được chúng ta sử dụng trong việc xóa một Node có giá trị chỉ định trên BST ở mục tiếp theo.

Hình ảnh dưới đây mô phỏng hàm LestMostChild hoạt động:

Left most child
Con trái nhất của Node root(15) là 8; Con trái nhất của Node root(20) là 16

Vậy chúng ta có cần hàm RightMostChild không? Câu trả lời là có. Tuy nhiên, bạn có thể tận dùng hàm LeftMostChild này, chẳng hạn: Số 25 là right most child của root(15) có thể tính bằng LeftMostChild(root->right->right).

2.6. Xóa Node trong BST

Việc xóa Node trong BST có lẽ là phức tạp nhất trong các chức năng của cây tìm kiếm nhị phân. Nếu Node bạn cần xóa là Node lá thì việc xóa rất đơn giản. Cái khó ở chỗ xóa một Node ở trung gian, khi đó, bạn phải tìm cách xóa và nối lại mà vẫn đảm bảo cây mới vẫn là BST. Chúng ta sẽ xem xét từng trường hợp trước khi code nhé:

1. Node cần xóa là Node lá(không có child nào cả)

Giả sử ta cần xóa 20 trong hình dưới đây, đơn giản là chỉ cần giải phóng ô nhớ đó.

2. Node cần xóa có 1 child

Trong trường hợp này, Node bị xóa sẽ được giải phóng và cây con duy nhất của Node bị xóa sẽ được liên kết trực tiếp với cha của Node bị xóa.

3. Node cần xóa có đủ 2 child

Xóa node có 2 con trong BST
Giả sử cần xóa Node khoanh đỏ. Khi đó, ta cần tìm Node thế thân cho Node đỏ này, đó là Node LeftMostChild của cây con bên phải của Node đỏ(72) = Node khoanh màu xanh(54) . Sau đó, gọi đệ quy Xóa Node màu xanh. Khi đó Node cần xóa sẽ quay trở về trường hợp 1 hoặc 2(Trong ảnh này là trường hợp 2 – có 1 child).

Đây là trường hợp nan giải nhất, nhưng chúng ta vẫn có cách làm như sau:

  1. Tìm Node của con trái nhất(giả sử nó là leftmost) của cây con bên phải của Node cần xóa.
  2. Cập nhật giá trị của Node cần xóa = giá trị của Node leftmost.
  3. Gọi đệ quy hàm Delete xóa Node leftmost khỏi BST.

Giải thích:

  • Khi muốn xóa Node có 2 con, ta cần tìm Node nào đó trong BST thỏa mãn tính chất lớn hơn lớn hơn tất cả các con bên trái và nhỏ hơn tất cả các con bên phái -> Node đó chính là LeftMostChild của con bên phải của Node cần xóa.
  • Ta chỉ cần sửa giá trị của Node cần xóa, không cần xóa Node đó làm gì. Thay vào đó, xóa Node bị thế thân(LeftMostChild của con bên phải của Node cần xóa).

3. Duyệt cây tìm kiếm nhị phân

Ở mục này mình sẽ trình bày về 3 cách duyệt cây tìm kiếm nhị phân. Chúng ta sẽ đi vào chi tiết từng cách nhé. Thứ tự duyệt được đặt tên phụ thuộc vào vị trí của Node root trong quá trình duyệt:

  • PreOrder: Node -> Left -> Right
  • InOrder: Left -> Node -> Right
  • PostOrder: Left -> Right -> Node
  • Thực ra 3 phần tử Left, Right, Node có tất cả 6 hoán vị cơ. Còn 3 cái nữa các bạn tự cài nhé.

3.1. Duyệt PreOrder

Quy trình duyệt PreOrder sẽ thực hiện theo thứ tự Node -> Left -> Right, cụ thể như sau:

  1. Ghé thăm Node root
  2. Gọi đệ quy duyệt qua cây con bên trái
  3. Gọi đệ quy duyệt qua cây con bên phải

Duyệt pre_order BTS
Thứ tự duyệt pre_order trên BTS

3.2. Duyệt InOrder

Quy trình duyệt PreOrder sẽ thực hiện theo thứ tự Left-> Node -> Right, cụ thể như sau:

  1. Gọi đệ quy duyệt qua cây con bên trái
  2. Ghé thăm Node root
  3. Gọi đệ quy duyệt qua cây con bên phải

Thứ tự duyệt in_order trên BTS
Thứ tự duyệt in_order trên BTS

3.1. Duyệt PostOrder

Quy trình duyệt PreOrder sẽ thực hiện theo thứ tự Left -> Right -> Node, cụ thể như sau:

  1. Gọi đệ quy duyệt qua cây con bên trái
  2. Gọi đệ quy duyệt qua cây con bên phải
  3. Ghé thăm Node root

Duyệt post_order trên BTS
Duyệt post_order trên BTS

4. Code đầy đủ cây tìm kiếm nhị phân

Trong hàm main tôi đã cài đặt BTS cho cây nhị phân sau:

Duyệt cây nhị phân tìm kiếm
Cây nhị phân tìm kiếm được sử dụng trong source code

Nhờ các bạn kiểm tra kết quả chạy của code:

5. Bài tập thực hành

  • https://vn.spoj.com/problems/NKTREE/
  • http://ntucoder.net/Problem/List?ThemeID=17
  • https://www.hackerearth.com/practice/data-structures/trees/binary-search-tree/practice-problems/
Các bài viết trong SeriesBài trước: Cây nhị phân – Binary Tree

15 COMMENTS

  1. package BaiTap;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.util.Scanner;

    public class MarioClimb {
    public static int T;
    public static int M,N;
    public static int X,Y;
    public static int min;
    public static boolean kt;
    public static int[][] maTran, visited;
    public static int[] dx = {-1,0,1,0};
    public static int[] dy = {0,1,0,-1};
    public static marioQueue q = new marioQueue();

    public static boolean isOK(int x, int y) {
    if(x=M || y=N)
    return false;
    return true;

    }
    public static void main(String[] args) throws FileNotFoundException {
    System.setIn(new FileInputStream(“src/BaiTap/MarioClimb.txt”));
    Scanner sc = new Scanner(System.in);
    T = sc.nextInt();
    for(int stt=1; stt<=T; stt++) {
    M = sc.nextInt();
    N = sc.nextInt();
    maTran = new int[M][N];
    for(int i = 0 ; i<M; i++) {
    for(int j = 0 ; j<N; j++) {
    maTran[i][j] = sc.nextInt();
    if(maTran[i][j]==2) {
    X = i;
    Y = j;
    }
    }
    }
    kt = false;
    min = 0;
    for(int nhay = 1; nhay<=M; nhay++) {
    visited = new int[M][N];
    find(nhay);
    for(int i = 0 ; i<M; i++) {
    for(int j = 0 ; j<N; j++) {
    System.out.print(visited[i][j]+" ");
    }
    System.out.println();
    }
    System.out.println();
    if(min!=0)
    break;
    }
    System.out.println("#"+stt+" "+min);
    }
    }

    public static void find(int nhay) {
    q.init();
    q.enQueue(X, Y);
    visited[X][Y]=nhay;
    while(!q.isEmpty()) {
    mario mario = q.deQueue();
    int a = mario.x;
    int b = mario.y;
    for(int k = 0 ; k<=3; k++) {
    for(int i = 1; i<=nhay; i++) {
    int aTam = a;
    int bTam = b;
    if(k==0 || k==2) {
    aTam += dx[k]*i;
    bTam += dy[k]*i;
    }
    else if(k==1 || k==3) {
    aTam += dx[k];
    bTam += dy[k];
    }
    if(isOK(aTam, bTam)) {
    if(maTran[aTam][bTam]==3) {
    min = nhay;
    return;
    }
    if(maTran[aTam][bTam]==1 && visited[aTam][bTam]==0) {
    visited[aTam][bTam] = visited[a][b];
    q.enQueue(aTam, bTam);
    }
    }

    }
    }
    }
    }

    }

    class mario{
    int x;
    int y;
    mario(int x, int y){
    this.x = x;
    this.y = y;
    }
    }

    class marioQueue{
    mario[] mang = new mario[100000];
    int front, rear;
    public void init() {
    front = rear = -1;
    }
    public boolean isEmpty() {
    if(front==rear)
    return true;
    return false;
    }
    public void enQueue(int x, int y) {
    rear++;
    mang[rear] = new mario(x,y);
    }
    public mario deQueue() {
    if(!isEmpty()) {
    front++;
    return mang[front];
    }
    return new mario(-1,-1);
    }
    }

  2. package BaiTap;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.util.Scanner;

    public class BaseStation {
    public static int T;
    public static int N, M;
    public static int max, min, count;
    public static int[][] maTran, visited;
    public static int[] dxC = { -1, -1, -1, 0, 1, 0 };
    public static int[] dyC = { -1, 0, 1, 1, 0, -1 };
    public static int[] dxL = { -1, 0, 1, 1, 1, 0 };
    public static int[] dyL = { 0, 1, 1, 0, -1, -1 };

    public static boolean isOK(int x, int y) {
    if (x = M || y = N) {
    return false;
    }
    return true;
    }

    public static void main(String[] args) throws FileNotFoundException {
    System.setIn(new FileInputStream(“src/BaiTap/Station.txt”));
    Scanner sc = new Scanner(System.in);
    T = sc.nextInt();
    for (int stt = 1; stt <= T; stt++) {
    N = sc.nextInt();
    M = sc.nextInt();
    maTran = new int[M][N];
    visited = new int[M][N];
    for (int i = 0; i < M; i++) {
    for (int j = 0; j < N; j++) {
    maTran[i][j] = sc.nextInt();
    }
    }
    max = 0;
    min = 5000;
    for (int i = 0; i < M; i++) {
    for (int j = 0; j max)
    max = sum;
    }
    if(isOK(x-1,y-1) && isOK(x-1, y+1) && isOK(x+1, y)) {
    int sum = maTran[x][y] + maTran[x-1][y-1] + maTran[x-1][y+1] + maTran[x+1][y];
    if(sum>max)
    max = sum;
    }
    }
    if(x%2==1) {
    if(isOK(x-1,y) && isOK(x+1, y-1) && isOK(x+1, y+1)) {
    int sum = maTran[x][y] + maTran[x-1][y] + maTran[x+1][y-1] + maTran[x+1][y+1];
    if(sum>max)
    max = sum;
    }
    if(isOK(x,y-1) && isOK(x, y+1) && isOK(x+1, y)) {
    int sum = maTran[x][y] + maTran[x][y-1] + maTran[x][y+1] + maTran[x+1][y];
    if(sum>max)
    max = sum;
    }
    }
    }

    public static void backTrack(int step, int x, int y, int sum) {
    if (step == 3) {
    if (sum > max)
    max = sum;
    if (sum < min)
    min = sum;
    return;
    }
    for (int k = 0; k < 6; k++) {
    if (y % 2 == 0) {
    int a = x + dxC[k];
    int b = y + dyC[k];
    if (isOK(a, b)) {
    if (visited[a][b] == 0) {
    visited[a][b] = 1;
    backTrack(step + 1, a, b, sum + maTran[a][b]);
    backTrack(step + 1, x, y, sum);
    visited[a][b] = 0;
    }
    }
    }
    else {
    int a = x + dxL[k];
    int b = y + dyL[k];
    if (isOK(a, b)) {
    if (visited[a][b] == 0) {
    visited[a][b] = 1;
    backTrack(step + 1, a, b, sum + maTran[a][b]);
    backTrack(step + 1, x, y, sum);
    visited[a][b] = 0;
    }
    }
    }
    }
    }
    }

  3. package BaiTap;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.util.Scanner;

    public class Ring2 {
    public static int T;
    public static int N, stt;
    public static int coinMin, count;
    public static int[] mangLinh, mangTien, mangLinhMua;

    public static void main(String[] args) throws FileNotFoundException {
    System.setIn(new FileInputStream(“src/BaiTap/Ring.txt”));
    Scanner sc = new Scanner(System.in);
    T = sc.nextInt();
    for (stt = 1; stt <= T; stt++) {
    N = sc.nextInt();
    mangLinh = new int[N + 1];
    mangTien = new int[N + 1];
    mangLinhMua = new int[N + 1];
    for (int i = 1; i <= N; i++) {
    mangLinh[i] = sc.nextInt();
    mangTien[i] = sc.nextInt();
    }
    System.out.println();
    for (int i = 1; i <= N; i++) {
    System.out.print(mangLinh[i] + " ");
    }
    System.out.println();
    for (int i = 1; i 8300)
    // return;
    // if(stt==4 && coin>4800)
    // return;
    if(coin>coinMin)
    return;
    if (step == N + 1) {
    count++;
    if (coin < coinMin) {
    coinMin = coin;
    }
    return;
    }
    for (int k = 0; k <= 2; k++) {
    if (k == 0) {
    backTrack(step + 1, coin + mangTien[step]);
    } else if (k == 1) {
    mangLinhMua[3] += mangLinh[step];
    backTrack(step + 1, coin + mangTien[step] * 2);
    mangLinhMua[3] -= mangLinh[step];
    } else if (k == 2) {
    int dich = mangLinh[step];
    if(mangLinhMua[1]+mangLinhMua[2]+mangLinhMua[3]=dich) {
    mangLinhMua[1] = mangLinhMua[2];
    mangLinhMua[2] = mangLinhMua[3];
    mangLinhMua[3] = 0;
    backTrack(step + 1, coin);
    }else if(mangLinhMua[1]+mangLinhMua[2]>=dich) {
    mangLinhMua[1] = mangLinhMua[2]-(dich-mangLinhMua[1]);
    mangLinhMua[2] = mangLinhMua[3];
    mangLinhMua[3] = 0;
    backTrack(step + 1, coin);
    }else if(mangLinhMua[1]+mangLinhMua[2]<dich) {
    mangLinhMua[1] = 0;
    mangLinhMua[2] = mangLinhMua[3]-(dich-mangLinhMua[2]-mangLinhMua[1]);
    mangLinhMua[3] = 0;
    backTrack(step + 1, coin);
    }
    mangLinhMua[1] = a;
    mangLinhMua[2] = b;
    mangLinhMua[3] = c;
    }
    }
    }
    }

    }

  4. package BaiTap;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.util.Scanner;

    public class PhoneS6 {
    public static int T;
    public static int N;
    public static int[][] maTran, visited;
    public static int[] dx = {-1,0,1,0};
    public static int[] dy = {0,1,0,-1};
    public static int[] dem;
    public static phoneQueue q = new phoneQueue();

    public static boolean isOK(int x, int y) {
    if(x=N || y=N)
    return false;
    return true;

    }
    public static void main(String[] args) throws FileNotFoundException {
    System.setIn(new FileInputStream(“src/BaiTap/PhoneS6.txt”));
    Scanner sc = new Scanner(System.in);
    T = sc.nextInt();
    for(int stt=1; stt<=T; stt++) {
    N = sc.nextInt();
    maTran = new int[N][N];
    visited = new int[N][N];
    for(int i = 0 ; i<N; i++) {
    for(int j = 0 ; j<N; j++) {
    maTran[i][j] = sc.nextInt();
    }
    }
    for(int i = 0 ; i<N; i++) {
    for(int j = 0 ; j<N; j++) {
    if(maTran[i][j]==0) {
    dem = new int[6];
    lan(i, j);
    }
    }
    }
    }
    }

    public static void lan(int x, int y) {
    q.init();
    q.enQueue(x, y);
    visited[x][y] = 1;
    while(!q.isEmpty()) {
    phone p = q.deQueue();
    int a = p.x;
    int b = p.y;
    for(int k = 0 ; k<=3; k++) {
    int aTam = a + dx[k];
    int bTam = b + dy[k];
    if(isOK(aTam, bTam)) {
    if(maTran[aTam][bTam]==0 && visited[aTam][bTam]==0) {
    visited[aTam][bTam] = 1;
    q.enQueue(aTam, bTam);
    }
    else {
    dem[maTran[aTam][bTam]]++;
    }
    }
    }
    }
    }

    }

    class phone{
    int x;
    int y;
    phone(int x, int y){
    this.x = x;
    this.y = y;
    }
    }

    class phoneQueue{
    phone[] mang = new phone[100000];
    int front, rear;
    public void init() {
    front = rear = -1;
    }
    public boolean isEmpty() {
    if(front==rear)
    return true;
    return false;
    }
    public void enQueue(int x, int y) {
    rear++;
    mang[rear] = new phone(x,y);
    }
    public phone deQueue() {
    if(!isEmpty()) {
    front++;
    return mang[front];
    }
    return new phone(-1,-1);
    }
    }

  5. 3
    5
    5 5 1 4 4
    4 0 2 4 2
    5 0 0 2 0
    5 4 3 0 1
    1 3 3 2 1
    7
    0 0 0 0 0 0 0
    0 0 0 0 0 0 0
    0 0 0 0 0 0 0
    5 0 5 0 5 0 5
    0 0 0 0 0 0 0
    0 0 0 0 0 0 0
    0 0 0 0 0 0 0
    10
    1 3 5 1 4 0 0 4 2 1
    1 1 2 1 1 0 5 0 2 1
    5 0 2 0 4 4 4 0 1 1
    0 2 2 4 0 5 4 2 1 3
    1 1 2 2 2 3 3 2 1 1
    5 1 1 2 0 3 3 2 2 1
    3 1 1 1 0 0 1 2 2 5
    3 1 4 1 2 0 4 0 0 5
    4 0 3 3 1 3 3 0 0 1
    5 0 3 1 4 3 3 1 2 3

  6. 5
    5 3
    300 410 150 55 370
    120 185 440 190 450
    165 70 95 420 50
    5 5
    356 55 41 453 12
    401 506 274 506 379
    360 281 421 311 489
    425 74 276 371 164
    138 528 461 477 470
    13 3
    197 51 443 274 47 552 160 96 501 102 469 318 308
    516 128 506 471 381 418 328 517 380 78 569 58 90
    113 238 179 444 541 27 444 62 264 93 245 353 37
    7 11
    292 182 586 607 259 190 239
    511 716 425 367 511 462 714
    593 713 231 60 118 442 82
    626 577 579 682 136 176 681
    240 23 410 193 230 729 109
    453 231 287 383 444 578 409
    729 401 408 330 213 574 54
    684 224 75 62 660 472 227
    606 37 473 487 222 185 476
    84 477 158 94 141 484 122
    616 333 302 626 29 99 674
    15 15
    488 923 92 659 783 908 167 332 467 205 457 871 536 189 642
    676 729 520 687 276 13 709 305 315 621 19 606 201 722 671
    631 829 973 318 487 140 411 633 530 981 594 372 787 586 895
    520 938 375 770 495 310 59 820 840 785 457 454 967 178 507
    498 368 377 326 247 79 875 38 778 800 205 186 131 543 948
    672 530 848 342 397 751 192 265 763 447 869 223 950 636 34
    669 929 802 500 979 978 322 185 598 618 663 192 746 289 44
    77 271 943 874 211 532 441 567 396 141 527 286 755 95 206
    458 803 319 490 384 736 328 977 954 651 975 472 405 344 189
    624 725 838 159 624 269 400 855 63 924 349 711 473 115 446
    937 359 820 851 629 698 437 834 18 257 632 534 153 478 908
    205 875 185 508 373 826 432 487 522 10 663 870 711 566 941
    773 663 954 237 166 957 722 198 346 337 708 592 443 809 41
    634 174 193 733 800 227 418 503 903 405 261 805 234 461 191
    176 891 203 825 575 508 627 845 610 814 263 159 719 450 903

LEAVE A REPLY

Please enter your comment!
Please enter your name here