Chuỗi hạt bồ đề (PTIT009)
Đề bài
PTIT009 – Chuỗi hạt bồ đề
Trong chuyến đi khám phá thế giới của mình, Grenore muốn tìm hiểu thêm về văn hóa phương Đông. Cậu được biết rằng để tìm hiểu văn hóa phương Đông một cách nhanh nhất, người ta sẽ đi đến những ngôi chùa. Khi Grenore đến nơi, thứ duy nhất làm cậu thích thú là những chuỗi hạt bồ đề, các chuỗi hạt bồ đề khi mới nhìn thoáng qua, chúng có vẻ khác nhau, nhưng thực chất đó chỉ là sự thay đổi trong thứ tự của chuỗi hạt.
Ví dụ như chuỗi hạt có chữ "ryuu" và "yuur" là như nhau, chỉ có một điều duy nhất là hạt chữ "r" từ vị trí đầu bị di chuyển xuống cuối. Bằng cách di chuyển này, từ các chuỗi hạt giống nhau, ta có thể tạo ra các chuỗi hạt khác nhau.
Grenore cực kỳ thấy hứng thú với điều này, cậu chọn ra $n$ chuỗi hạt, và kiểm tra xem liệu chúng có thể đưa về một dãy giống hệt nhau hay không (các hạt chỉ có thể di chuyển từ đầu về cuối, vì các nhà sư cho rằng di chuyển hạt từ cuối về đầu là xúc phạm thần linh) với số bước ít nhất.
Dữ liệu vào:
- Dòng đầu tiên gồm một số $n$ $(1 \le n \le 50)$ là số chuỗi hạt bồ đề mà Grenore đã lấy.
- $n$ dòng tiếp theo, mỗi dòng là một xâu chỉ gồm các chữ cái tiếng Anh, viết thường, biểu thị một chuỗi hạt.
Giới hạn:
- Các xâu đều có cùng một kích thước, mỗi xâu không có quá 50 ký tự $(1 \le |s| \le 50)$.
Dữ liệu ra: Gồm một số nguyên duy nhất là số bước biến đổi để các chuỗi hạt trở nên giống nhau, nếu không thể biến đổi, in ra -1.
2
ryuu
yuur12
abcdef
abcdee-1Lời mở đầu
Bài này mình được một em học viên hỏi và mình đã hướng dẫn chi tiết cách thức giải quyết cho em ấy qua tin nhắn. Tin rằng sẽ có giá trị với nhiều bạn nên mình viết lại thành bài giải đầy đủ để các bạn tham khảo nhen. Hy vọng mang đến được giá trị cho các bạn 😇.
Giải thích lại đề cho bạn nào chưa hiểu: ta có $n$ chuỗi, mỗi bước được chọn một chuỗi bất kỳ và đem ký tự đầu tiên của nó xuống cuối. Hỏi cần ít nhất bao nhiêu bước (tính tổng trên tất cả các chuỗi) để $n$ chuỗi trở nên giống hệt nhau.
- Ví dụ 1: "ryuu" đem chữ "r" xuống cuối thành "yuur", giống chuỗi thứ hai ⇒ chỉ cần 1 bước.
- Ví dụ 2: "abcdef" có chữ "f", còn "abcdee" không có chữ "f" mà lại có 2 chữ "e". Xoay kiểu gì cũng không thêm/bớt được ký tự nào ⇒ không bao giờ giống nhau được ⇒ in -1.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJLộ trình lời giải, bấm vào để nhảy tới:
Cách 1Vét cạn chọn chuỗi gốc – O(n² · |s|²)
Bài này mình gợi ý cho em học viên đúng một câu: dùng duyệt trâu, xét tất cả mọi trường hợp có thể có. "Duyệt trâu" nghe hơi lạ tai nhưng nó chính là vét cạn đó các bạn, tiếng Anh là brute force. Dân trong ngành hay gọi bằng thuật ngữ duyệt trâu, các bạn cũng nên làm quen đi 😄.
Tại sao dám vét cạn? Vì dữ liệu quá nhỏ: tối đa chỉ $50$ chuỗi, mỗi chuỗi dài tối đa $50$ ký tự. Còn vét cạn ra sao thì mình đi từng ý nhé.
Mỗi chuỗi có bao nhiêu trạng thái?
Mỗi chuỗi chỉ được quyền đem ký tự ở đầu về cuối. Ví dụ chuỗi ban đầu là "12345" thì nó có tất cả các trạng thái:
Tức là có đúng Length trạng thái với Length là độ dài chuỗi. Xoay thêm lần nữa (k = 5) thì lại quay về "12345" như ban đầu, nên không cần xét tiếp. Trạng thái sau $k$ bước chính là s.substr(k) + s.substr(0, k): phần từ vị trí $k$ trở đi, nối với $k$ ký tự đầu.
Xét trước khi nào chắc chắn in -1
Rõ ràng một điều là $n$ chuỗi đều phải có số lượng của từng ký tự bằng nhau. Ví dụ chuỗi thứ nhất có 3 chữ a, 2 chữ b, 4 chữ c thì các chuỗi còn lại cũng phải có đúng 3 chữ a, 2 chữ b, 4 chữ c như vậy, có thể nằm ở vị trí khác nhưng phải đủ số lượng ấy. Nếu có một chuỗi nào chỉ có 1 chữ a hay không có chữ b thì kết luận -1 ngay, bởi phép biến đổi chỉ là đem từ đầu về cuối, nó không làm thêm hay bớt ký tự nào cả.
Trong code, mỗi chuỗi có một mảng đếm check[i] kích thước 123 (mã ASCII của 'z' là 122), check[i][c] là số lần ký tự c xuất hiện trong chuỗi thứ $i$ (kỹ thuật đếm phân phối). Sau đó so sánh check[i] với check[i - 1] cho mọi $i$ là biết ngay.
sorted(a[i]) tạo ra một danh sách mới đã sắp, chuỗi gốc vẫn giữ nguyên.Tìm số bước ít nhất: chọn từng chuỗi làm gốc
Xét tất cả tình huống có thể xảy ra: lần lượt chọn từng chuỗi làm gốc và cố gắng biến đổi tất cả các chuỗi còn lại thành chuỗi gốc đó. Với mỗi chuỗi, vét cạn mọi khả năng biến đổi của nó ($k = 0, 1, \ldots, \text{Length} - 1$), đến khi nào trùng thì dừng và cộng $k$ vào tổng chi phí.
Ví dụ có $n = 3$ chuỗi: 123, 456, 789:
- Chọn
123làm gốc, cố gắng biến đổi456và789xem có về được123không. - Sau đó chọn
456làm gốc, cố gắng biến đổi123và789. - Sau đó chọn
789làm gốc, cố gắng biến đổi123và456.
Nếu một chuỗi đã xoay hết Length trạng thái mà vẫn không giống chuỗi gốc thì chuỗi gốc đó không chọn được, chuyển sang chọn chuỗi gốc khác. Trong các chuỗi gốc chọn được, lấy tổng chi phí nhỏ nhất. Nếu chọn hết $n$ chuỗi gốc mà không được cái nào thì kết luận -1.
Minh hoạ vét cạn
Em nhập các chuỗi (cách nhau bởi dấu cách) rồi bấm Tiếp: hàng xanh lá là chuỗi đang được chọn làm gốc, hàng cam là chuỗi đang được xoay thử, các hạt xanh dương là những hạt đã bị đem từ đầu xuống cuối.
Source code để các bạn tham khảo nếu làm theo hướng này. Code này mình code vội đúng theo tư tưởng đã trình bày ở trên (đã nộp thành công trên web), mình chỉ căn lại thụt lề cho dễ đọc, chưa clean nhiều để các bạn dễ đối chiếu với phần phân tích:
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
vector<string> a(n);
vector<vector<int>> check(n, vector<int>(123, 0));
for(int i = 0; i < n; ++i){
cin >> a[i];
for(int j = 0; j < a[i].length(); ++j){
check[i][a[i][j]]++;
}
}
bool flag = true;
for(int i = 1; i < n; ++i){
if(check[i] != check[i - 1]){
flag = false;
break;
}
}
if(flag == false){
cout << -1;
}
else{
long long Min = LLONG_MAX;
for(int i = 0; i < n; ++i){
long long sum = 0;
flag = true;
for(int j = 0; j < n; ++j){
bool flag2 = false;
for(int k = 0; k < a[j].length(); ++k){
string s = a[j].substr(k) + a[j].substr(0, k);
if(s == a[i]){
sum += k;
flag2 = true;
break;
}
}
if(flag2 == false){
flag = false;
break;
}
}
if(flag == true){
Min = min(Min, sum);
}
}
if(Min == LLONG_MAX){
cout << -1;
}
else{
cout << Min;
}
}
return 0;
}
n = int(input())
a = [input().strip() for _ in range(n)]
# Điều kiện -1: số lượng từng ký tự của mọi chuỗi phải giống nhau
flag = True
for i in range(1, n):
if sorted(a[i]) != sorted(a[i - 1]):
flag = False
break
if not flag:
print(-1)
else:
Min = -1
for i in range(n): # chọn a[i] làm chuỗi gốc
total = 0
flag = True
for j in range(n): # biến đổi a[j] về a[i]
flag2 = False
for k in range(len(a[j])):
s = a[j][k:] + a[j][:k]
if s == a[i]:
total += k
flag2 = True
break
if not flag2:
flag = False
break
if flag and (Min == -1 or total < Min):
Min = total
print(Min)
Một vài ghi chú về code:
check[i][a[i][j]]++: ký tựa[i][j]được dùng trực tiếp làm chỉ số (C++ tự đổicharsang mã ASCII), ví dụ 'a' là ô 97, 'z' là ô 122, nên mảng đếm cần 123 ô.check[i] != check[i - 1]: haivectorso sánh được trực tiếp bằng==/!=, nó so từng phần tử giúp mình luôn.Minkhởi tạoLLONG_MAX(sốlong longlớn nhất). Cuối cùng màMinvẫn làLLONG_MAXnghĩa là không có chuỗi gốc nào chọn được ⇒ in -1. Trường hợp này xảy ra khi số lượng ký tự giống hệt nhau nhưng thứ tự vòng tròn khác, ví dụ "abc" và "acb" (em bấm nút Đủ ký tự vẫn -1 trong khung chạy tay để xem).- Bản Python dùng
sorted(a[i]) != sorted(a[i - 1])thay cho mảng đếm, đúng ý "sắp xếp rồi so sánh" ở trên, và dùngMin = -1làm giá trị "chưa có".
Đánh giá độ phức tạp của cách làm này:
- Không gian: $n$ chuỗi và $n$ mảng đếm 123 ô ⇒ $O(n \cdot |s|)$.
- Thời gian: bước đếm ký tự mất $O(n \cdot |s|)$. Phần vét cạn: $n$ chuỗi gốc × $n$ chuỗi cần xoay × tối đa $|s|$ lần xoay, mỗi lần tạo chuỗi và so sánh mất $O(|s|)$ ⇒ $O(n^2 \cdot |s|^2) = 50^4 \approx 6.25 \cdot 10^6$ phép tính, quá nhẹ nhàng.
- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét