Cắt Dây (CATDAY)
Đề bài
CẮT DÂY (CATDAY)
Để chuẩn bị cho ngày lễ Noel bạn An chuẩn bị $N$ đoạn dây trang trí (đoạn dây thứ $i$ có độ dài $a_i$) và $M$ đèn. An muốn cắt $N$ đoạn dây này thành $M$ đoạn bằng nhau để treo đèn, mỗi dây treo 1 đèn. Yêu cầu mỗi đoạn có độ dài nguyên. Không nhất thiết cắt hết tất cả đoạn dây ban đầu và phần thừa cắt đi có thể có độ dài khác 0.
Yêu cầu: Hãy giúp An tìm độ dài lớn nhất mà đoạn dây có thể nhận được. Nếu không thể cắt thì đưa ra số 0.
Dữ liệu vào: Tệp văn bản catday.inp:
- Dòng đầu chứa 2 số nguyên $N$, $M$.
- Dòng thứ $i$ trong $N$ dòng sau chứa số nguyên $a_i$.
Kết quả ra: Tệp văn bản catday.out ghi kết quả trên 1 dòng dưới dạng số nguyên.
(Code trong bài đọc/ghi chuẩn – bàn phím/màn hình. Nếu nộp ở nơi bắt dùng file thì các bạn thêm freopen("catday.inp", "r", stdin); freopen("catday.out", "w", stdout); vào đầu hàm main nhé.)
4 11
802
743
547
539200Lời mở đầu
Bữa giờ LeetCode hoài sợ anh em ngán 😅 Nay mình đi comment dạo thấy bài này cũng được nên đăng để anh em thử sức nhé. Bài này là của một bạn đăng trong nhóm lập trình hỏi ý tưởng tối ưu, bạn ấy "nghĩ cả chiều không ra mà deadline sắp dí tới" 🤣. Thật ra đây là một bài kinh điển của dạng tìm kiếm nhị phân theo kết quả, nắm được dạng này rồi thì gặp lại là xử lý trong vài phút thôi. Chúc anh em học tốt và gặt hái nhiều thành công 😇.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
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 OJGiải thích ví dụ cho bạn nào chưa hiểu đề: có 4 sợi dây dài $802, 743, 547, 539$ và cần $M = 11$ đoạn bằng nhau. Nếu chọn độ dài mỗi đoạn là 200:
| Sợi dây | 802 | 743 | 547 | 539 | Tổng |
|---|---|---|---|---|---|
| Cắt đoạn 200 | 4 (thừa 2) | 3 (thừa 143) | 2 (thừa 147) | 2 (thừa 139) | 11 ≥ 11 ✔ |
| Cắt đoạn 201 | 3 (thừa 199) | 3 (thừa 140) | 2 (thừa 145) | 2 (thừa 137) | 10 < 11 ✘ |
Độ dài 200 cắt được đủ 11 đoạn (phần thừa bỏ đi, đề cho phép), còn 201 thì chỉ được 10 đoạn, không đủ. Mà độ dài càng lớn thì cắt được càng ít đoạn, nên mọi độ dài $\gt 200$ cũng không đủ ⇒ đáp án là 200. Lưu ý mỗi đoạn phải nằm trọn trong một sợi dây, không được nối 2 mẩu thừa lại với nhau nha.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Thử từng độ dài từ lớn xuống – O(N · max a)
Nếu đã chốt độ dài mỗi đoạn là $len$ thì việc đếm xem cắt được bao nhiêu đoạn rất dễ: sợi dây dài $a_i$ cắt được $\left\lfloor \dfrac{a_i}{len} \right\rfloor$ đoạn (chia nguyên), phần dư bỏ đi. Gọi
Độ dài một đoạn không thể vượt quá sợi dây dài nhất $mx = \max a_i$ (vì đoạn phải nằm trọn trong một sợi). Vậy cách đơn giản nhất là thử $len = mx, mx - 1, \ldots, 1$: gặp độ dài đầu tiên có $f(len) \ge M$ thì đó là độ dài lớn nhất, in ra và dừng. Nếu thử hết tới 1 mà vẫn không đủ (tức là tổng độ dài cả $N$ sợi còn chưa tới $M$) thì in $0$.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
long long n, m;
cin >> n >> m;
vector<long long> a(n);
long long mx = 0;
for(int i = 0; i < n; i++){
cin >> a[i];
mx = max(mx, a[i]);
}
for(long long len = mx; len >= 1; len--){
long long cnt = 0;
for(int i = 0; i < n; i++){
cnt += a[i] / len;
}
if(cnt >= m){
cout << len;
return 0;
}
}
cout << 0;
return 0;
}
import sys
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
a = [int(x) for x in data[2:2 + n]]
mx = max(a) if a else 0
ans = 0
for length in range(mx, 0, -1):
cnt = 0
for x in a:
cnt += x // length
if cnt >= m:
ans = length
break
print(ans)
Đánh giá độ phức tạp của cách làm này:
- Không gian: lưu mảng $a$ ⇒ $O(N)$.
- Thời gian: trường hợp xấu nhất phải thử gần hết $mx$ độ dài, mỗi lần đếm mất $O(N)$ ⇒ $O(N \cdot mx)$.
Cách 2Tìm kiếm nhị phân độ dài – O(N · log max a)
Nhìn lại hàm $f(len)$: độ dài mỗi đoạn càng lớn thì số đoạn cắt được càng ít (mỗi $\left\lfloor a_i / len \right\rfloor$ chỉ có thể giảm hoặc giữ nguyên khi $len$ tăng). Nên nếu xếp các độ dài $1, 2, \ldots, mx$ thành một hàng thì nó chia làm đúng 2 khúc:
Cụ thể, giữ đoạn $[l, r]$ chứa đáp án, ban đầu $l = 1$, $r = mx$, $ans = 0$. Mỗi bước lấy $mid = (l + r) / 2$ và đếm $f(mid)$:
- Nếu $f(mid) \ge M$: độ dài $mid$ cắt được ⇒ ghi nhận $ans = mid$, nhưng biết đâu còn dài hơn nữa vẫn được ⇒ tìm tiếp bên phải: $l = mid + 1$.
- Ngược lại: $mid$ quá dài ⇒ mọi độ dài $\ge mid$ đều không được ⇒ tìm bên trái: $r = mid - 1$.
Khi $l \gt r$ thì $ans$ chính là độ dài lớn nhất cắt đủ $M$ đoạn. Nếu chưa lần nào cắt đủ thì $ans$ vẫn là $0$ – đúng yêu cầu "không thể cắt thì đưa ra số 0" luôn, khỏi xử lý riêng 😎.
Minh hoạ tìm kiếm nhị phân
Em nhập $M$ và độ dài các sợi dây rồi bấm Tiếp. Mỗi sợi dây được vẽ thành một thanh: phần xanh (hoặc đỏ nếu không đủ) là các đoạn dài $mid$ cắt được, phần xám là phần thừa bỏ đi. Thanh ngang phía trên cho biết đoạn $[l, r]$ đang tìm.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
long long n, m;
vector<long long> a;
long long dem(long long len){
long long cnt = 0;
for(int i = 0; i < n; i++){
cnt += a[i] / len;
}
return cnt;
}
int main(){
cin >> n >> m;
a.resize(n);
long long mx = 0;
for(int i = 0; i < n; i++){
cin >> a[i];
mx = max(mx, a[i]);
}
long long l = 1, r = mx, ans = 0;
while(l <= r){
long long mid = (l + r) / 2;
if(dem(mid) >= m){
ans = mid;
l = mid + 1;
}
else{
r = mid - 1;
}
}
cout << ans;
return 0;
}
import sys
def dem(a, length):
cnt = 0
for x in a:
cnt += x // length
return cnt
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
a = [int(x) for x in data[2:2 + n]]
l, r, ans = 1, max(a) if a else 0, 0
while l <= r:
mid = (l + r) // 2
if dem(a, mid) >= m:
ans = mid
l = mid + 1
else:
r = mid - 1
print(ans)
main()
Đánh giá độ phức tạp của cách làm này:
- Không gian: lưu mảng $a$ ⇒ $O(N)$.
- Thời gian: đoạn tìm $[1, mx]$ mỗi bước giảm một nửa nên chỉ có khoảng $\log_2 mx$ bước (với $mx = 10^9$ là cỡ 30 bước), mỗi bước gọi
demmất $O(N)$ ⇒ $O(N \log mx)$. Với $N = 10^5$ chỉ khoảng $3 \cdot 10^6$ phép chia, chạy tức thì nha các bạn.
int. Vì vậy cnt, m, l, r, mid mình để hết long long cho chắc. Ngoài ra đừng bắt đầu tìm từ $l = 0$ – vì dem(0) sẽ chia cho 0 đó nha.- 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