Đếm số phép toán sắp xếp

c++

(Kirito-kun) #1

Cho em hỏi thuật toán bài dưới được không ạ! Em cảm ơn!

Bài Toán:
Với một chồng sách hiện tại ta phải sắp xếp sao cho chiều rộng của chồng sách giảm dần từ dưới lên trên, chỉ có một thao tác chuyển sách duy nhất là ta sẽ đi từ trên xuống dưới nếu thấy một quyển sách nào có chiều rộng nhỏ hơn quyển sách ngay bên trên nó thì ta sẽ nhấc quyển sách đó lên và đặt lên trên đầu chồng sách, cứ làm như vậy đến khi nào không còn quyển sách nào lỗi nữa thì thôi. Vậy đấy, quả thực công việc không hề đơn giản chút nào cả, chính vì thế nên chủ hiệu sách trả rất nhiều tiền cho ai làm công việc đó. Anh M chấp nhận làm công việc đó để có thể kiếm được nhiều tiền, và tất nhiên nhiều tiền thì sẽ có khả năng tán được em Hot Girl đó. Anh M muốn biết trước với chồng sách sắp phải sắp xếp anh M phải thực hiện bao nhiêu lần chuyển sách để được chồng sách đúng như yêu cầu, đương nhiên với khả năng lập trình của mình anh M đã quyết định lập trình giải quyết bài toán đó, nhưng khi bắt đầu vào lập trình anh M mới phát hiện ra đây quả thật cũng là một bài toán không hề đơn giản, hèn nào chủ hiệu sách lại chả nhiều tiên cho công việc này như vậy.

Yêu cầu: Cho n là số lượng sách của chồng sách và dãy n phần tử là chiều rộng của chồng sách tính từ trên xuống dưới. Hãy giúp anh M tính toán số lượng lần cần chuyển sách.

Dữ liệu : Vào từ file văn bản SORT.INP gồm:

• Dòng đầu tiên chứa một số nguyên dương duy nhất n (1 ≤ n ≤ 105)

• Dòng thứ hai chứa n số nguyên dương a i là chiều rộng của các cuốn sách ( a i ≤ 109 )

Kết quả : Ghi ra file văn bản SORT.OUT một số duy nhất là kết quả tìm được, lấy số dư cho 109+7.

SORT.INP SORT.OUT
5
1 1 2 4 3
6

(Kirito-kun) #3

Độ phức tạp ở đây là O(n log n) . Còn sharker sort là O(n^2) liệu có chạy được không anh?


(Hung) #4

Phải sắp xếp như thế này không? Mình không hiểu đề lắm.

int naive_sort(int* arr, int len) {
    int count = 0;
    int i = 0, end = len-1;
    
    while (i < end) {
        while (i < end && arr[i] <= arr[i+1]) {
           i++;
        }
        
        if (i < end) {
            int temp = arr[i+1];
            for (int j = i+1; j > 0; j--) {
                arr[j] = arr[j-1];
            }
            arr[0] = temp;
            
            i = 0;
            count++;
        }
    }
    
    return count;
}

Link: https://ideone.com/eY9P4a


(Kirito-kun) #5

Kiểu giống như thế này đó anh! Nhưng độ phức tạp của bài toán là O(n log n) nên có lẽ code này không full được ạ!


(Hung) #6

Không thấy mình ghi là naive_sort() á. :kissing:
Mình viết sort này chỉ để kiểm tra cái sort khác xịn hơn thôi.


(rogp10) #7

5 1 1 2 4 3

2 1 1 5 * * (3 lần)
1 1 2 * * * (2 lần)
4 1 1 2 5 * (1 lần)
2 1 1 4 * * (2 lần)
1 1 2 4 * * (2 lần)

sao ra 6 được nhỉ.


(Kirito-kun) #8

Anh ơi, 5 đây là n (tức số chồng sách) chứ không phải là bề rộng của 1 quyển sách!


(Hung) #9

Xong, kiểm tra thử đúng không.
Chủ yếu tốn thời gian implement AVL Tree :v

Trong cả đống code chỉ có đoạn này quan trọng nhất.
Thao tác với tree luôn là O(logn), cộng thêm cái vòng for ở ngoài nữa là O(n). Tổng cộng O(n.logn).

for (int i = last_index+1; i < arr.size(); i++) {
    if (arr[i].first < tree.max()) {
        count += arr[i].second;
        count += tree.count(arr[i].first);
        tree.insert(arr[i].first, arr[i].second);
    }
}

Full code thì xem thêm link


(Hung) #10

Phần giải thích code, tại nó dài quá, nên viết nháp rồi sửa đi sửa lại. Cuối cùng đăng trễ. :v

Đổi cái mảng số nguyên thành mảng các pair. Giá trị thứ nhất là số nguyên key, giá trị thứ 2 là số lần lặp key trong mảng ban đầu. Ví dụ:

1 1 1 4 4 2 3 3 3 3
(1,3) (4,2) (2,1) (3,4)

2 2 2 2 2
(2,5)

4 3 5 1
(4,1) (3,1) (5,1) (1,1)

Thay vì sort mảng số nguyên, thì sort mảng các pair rồi chuyển sang lại mảng số nguyên.

(1,3) (2,1) (3,4) (4,2)
1 1 1 2 3 3 3 3 4 4

(2,5)
2 2 2 2 2

(1,1) (3,1) (4,1) (5,1)
1 3 4 5

Xét một mảng các pair có dạng: (b, l) (a1, l1) (a2, l2) … (an, ln), trong đó:

  • a1 < a2 < … < an < b,
  • ai, b, n là số nguyên dương.

Sau khi sort dãy trên, có được mảng: (a1, l1) (a2, l2) … (an, ln) (b, l).
Gọi tổng số lần chuyển ai lên đầu mảng trong khi sort là T(n).

Trường hợp n = 1: (b, l) (a1, l1)

  • (a1, 1) (b, l) (a1, l1 - 1), vì b > a1, có 1 bước
  • (a1, 2) (b, l) (a1, l1 - 2), có 1 bước
  • (a1, l1 - 1) (b, l) (a1, 1), có 1 bước
  • (a1, l1) (b, l), có 1 bước

Tổng cộng T(1) = l1

Trường hợp n = 2: (b, l) (a1, l1) (a2, l2)

  • (a1, l1) (b, l) (a2, l2), vì b > a1, có l1 bước
  • (a2, 1) (a1, l1) (b, l) (a2, l2 - 1), vì b > a2, có 1 bước
  • (a1, l1) (a2, 1) (b, l) (a2, l2 - 1), vì a2 > a1, có l1 bước
  • (a2, 1) (a1, l1) (a2, 1) (b, l) (a2, l2 - 2), vì b > a2, có 1 bước
  • (a1, l1) (a2, 2) (b, l) (a2, l2 - 2), vì a2 > a1, có l1 bước
  • (a2, 1) (a1, l1) (a2, l2 - 1) (b l), có 1 bước
  • (a1, l1) (a2, l2) (b l), có l1 bước

Tổng cộng T(2) = l1 + l2 . (l1 + 1)

Trường hợp n > 2: (b, l) (a1, l1) (a2, l2) … (an, ln)

  • (a1, l1) (b, l) (a2, l2) … (an, ln), vì b > a1, có l1 bước
  • (a1, l1) … (ai-1, li-1) (b, l) (ai, li) … (an, ln), với 2 ≤ i ≤ n
    • (ai, 1) (a1, l1) … (ai-1, li-1) (b, l) (ai, li - 1) … (an, ln), vì b > ai, có 1 bước
    • (a1, l1) … (ai-1, li-1) (ai, 1) (b, l) (ai, li - 1) … (an, ln)
      • xét mảng (ai, 1) (a1, l1) … (ai-1, li-1), lấy b = ai, và n = i-1, được T(i-1) bước
    • (ai, 1) (a1, l1) … (ai-1, li-1) (ai, 1) (b, l) (ai, li - 2) … (an, ln), vì b > ai, có 1 bước
    • (a1, l1) … (ai-1, li-1) (ai, 2) (b, l) (ai, li - 2) … (an, ln), với b = ai và n = i-1, được T(i-1) bước
    • (ai, 1) (a1, l1) … (ai-1, li-1) (ai, li - 1) (b, l) … (an, ln), có 1 bước
    • (a1, l1) … (ai-1, li-1) (ai, li) (b, l) … (an, ln), có T(i-1) bước
    • tổng cộng = li . (T(i-1) + 1)

Tổng cộng


Từ hệ thức truy hồi T(n), chứng minh công thức tổng quát của T(n) bằng phương pháp quy nạp:

Bước cơ sở:

Bước quy nạp (n ≥ 2):



Xét một dạng mảng khác tổng quát hơn: (a1, l1) (a2, l2) … (ak, lk) (ak+1, lk+1) … (an, ln) (b, l)

  • a1 < a2 < … < ak < b < ak+1 < … < an,
  • ai, b, n là các số nguyên dương.

Sau khi sort, ta được mảng: (a1, l1) (a2, l2) … (ak, lk) (b, l) (ak+1, lk+1) … (an, ln)

Gồm có 2 giai đoạn để sắp xếp mảng trên.
Giai đoạn 1: vì an > b nên (b, l) được chuyển đầu mảng, có l bước, thu được mảng

  • (b, l) (a1, l1) (a2, l2) … (ak, lk) (ak+1, lk+1)

Giai đoạn 2: chèn (b, l) vào giữa (ak, lk) và (ak+1, lk+1), số bước bằng T(k) (sắp xếp (b, l) vào mảng (a11, l1) … (ak, lk)).

  • (a11, l1) (a2, l2) … (ak, lk) (b, l) (ak+1, lk+1) … (an, ln)

Tổng cộng: l + T(k)


Từ các tính toán trên, xây được giải thuật, trong đó inputsarr là mảng các (ai, li), inputs là mảng ban đầu, arr là mảng các phần tử đã được duyệt bởi vòng lặp for:

Nếu sử dụng giải thuật trên, for chạy trong O(n), lệnh gán count chạy trong O(n). Do đó, tổng cộng thời gian chạy là O(n2), không thoả mãn yêu cầu đề bài. Nên cần phải có cách để giảm lệnh cập nhật count từ O(n) xuống còn O(logn). Một cách giải quyết là sử dụng self-balancing tree.


Tree được xây dựng dựa trên ý tưởng của AVL Tree, nhưng thay vì các giá trị value được lưu trữ ở tất cả các node, thì tree có 2 loại node.

  • Leaf node: node lá lưu trữ giá trị chính, key là b và value là l+1. Nếu duyệt theo inorder traversing thì thứ tự các node lá in ra theo thứ tự tăng dần.
  • Node còn lại: node lưu trữ giá trị được tính từ các subtree (computed value), đại diện cho một tập các node lá.
    • Giá trị [start, end] bao phủ tất cả giá trị key của các node lá, start cho giá trị key nhỏ nhất mà node bao phủ, end cho giá trị key lớn nhất mà node bao phủ.
    • Giá trị value là tích các của các value của node lá.

Ví dụ:
BoiTree%20(1)

  • Node lá lần lượt lưu theo thứ tự từ trái sang phải: (0,3), (1,5), (2,4), (3,4), (5,3)
  • Node [0,1] bao phủ (0,3), (1,5), có value = 15 = 3.5
  • Node [0,2] bao phủ (0,3), (1,5), (2,4), có value = 60 = 15.4 = 3.5.4
  • Node [3,5] bao phủ (3,4), (5,3), có value = 12 = 4.3
  • Node [0,5] bao phủ (0,3), (1,5), (2,4), (3,4), (5,3), có value = 720 = 60.12 = (15.4) . (4.3) = 3.5.4.4.3

3 thao tác chính trên tree, mỗi thao tác đều là O(logn)

  • max: duyệt từ root sang nhánh phải liên tục đến khi gặp leaf node, trả về value của leaf node.
  • insert: cập nhật hoặc thêm node mới vào tree.
    • nếu đã tồn tại key trong tree, cập nhật lại value của leaf node chứa key, cập nhật giá trị value của các node cha.
    • nếu chưa tồn tại key, tạo node mới tạo vị trí leaf node có key gần bằng với key, cập nhật các node cha, tự động chỉnh tree cân bằng nếu tạo node làm mất cân bằng tree.
  • count: Trả về giá trị tích (li + 1)-1, có ai nhỏ hơn key = b. Ví dụ count(4)
    • count(4) = count([0,2]) * count[3,4]) - 1
    • count([0,2]) = 60
    • count([3,4]) = count((3,4)) = 4
    • count(4) = 60 * 4 - 1 = 240 - 1 = 239

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?