Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Đếm Cặp Version 2

Đếm Cặp Version 2
Nguồn: Codeforces Group Độ khó: Trung bình Toán học Công thức O(1)

Đề bài

Cho ba số nguyên $L, R, T$. Hãy đếm xem có bao nhiêu cặp số nguyên $x, y$ thoả mãn:

  • $L \le x < y \le R$;
  • $x + y = T$.

Dữ liệu: Vào từ thiết bị vào chuẩn gồm ba dòng chứa ba số nguyên $L, R, T$, mỗi số trên một dòng. $(-10^{18} \le L < R \le 10^{18};\ |T| \le 10^{18})$

Kết quả: Ghi ra thiết bị ra chuẩn gồm một số nguyên duy nhất là kết quả của bài toán.

Ràng buộc:

  • 60% số test tương ứng với 60% số điểm có $-10^6 \le L < R \le 10^6$;
  • 40% số test còn lại tương ứng với 40% số điểm không có ràng buộc gì thêm.
Ví dụ – input
5
10
13
Ví dụ – output
2

Giải thích: Có 2 bộ số thoả mãn: $(5, 8), (6, 7)$.

Lời mở đầu

Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇. 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é:

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ
Dành cho tất cả mọi ngườiNộp bài trên Codeforces

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên Codeforces

Bên dưới là toàn bộ cuộc trò chuyện giữa mình và em ấy, từ lúc em ấy mới được 60 điểm cho tới khi AC, mình biên tập lại theo từng phần cho dễ theo dõi.

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Cách 1Code của em: hai con trỏ, chỉ được 60%

HV
“e xin gợi ý ạ, e chỉ lm đc 60%, đây là code cua em:”
#include <bits/stdc++.h>
using namespace std;
int main()
{
    long long l,r,k;
    cin>>l>>r>>k;
    long long d=0;
    while (l<r)
    {
        if (l+r==k)d++,l++;
        else if (l+r<k)l++;
        else r--;
    }
    cout<<d;
}
l = int(input())
r = int(input())
k = int(input())
d = 0
while l < r:
    if l + r == k:
        d += 1
        l += 1
    elif l + r < k:
        l += 1
    else:
        r -= 1
print(d)
C++▶ Chạy tay từng dòng codeCách 1 (code của em, 60%): hai con trỏ l, r, mỗi bước dịch 1 đơn vị, O(R − L).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main()
4{
5 long long l,r,k;
6 cin>>l>>r>>k;
7 long long d=0;
8 while (l<r)
9 {
10 if (l+r==k)d++,l++;
11 else if (l+r<k)l++;
12 else r--;
13 }
14 cout<<d;
15}

Em làm được 60/100 tức là đúng 60% test, nghĩa là với các test thuộc subtask 1: $-10^6 \le L < R \le 10^6$.

Ý tưởng của em là hai con trỏ: l đứng ở đầu trái, r đứng ở đầu phải. Nếu l + r == k thì đếm 1 cặp rồi dịch l; nếu tổng còn nhỏ hơn $k$ thì phải tăng l; nếu tổng lớn hơn $k$ thì phải giảm r. Ý tưởng này đúng, chỉ có điều mỗi bước chỉ dịch được 1 đơn vị.

Anh xem code em rồi, em làm vòng lặp như thế tính ra nó sẽ lặp hết $R - L$ lần, như thế không ổn rồi vì đề bài cho giới hạn $-10^{18} \le L < R \le 10^{18}$. Lỡ như $L = -10^{18}$, $R = 10^{18}$ như thế em lặp hết $2 \cdot 10^{18}$ lần. Nếu theo ngôn ngữ C++ cho 1 giây máy chạy $10^8$ phép tính thì em sẽ cần $2 \cdot 10^{10}$ giây, tức là 20 tỷ giây.

1 giờ3,600 giây
1 ngày86,400 giây
1 năm31,536,000 giây
20 tỷ giây≈ 634 năm 😱

$20$ tỷ $/ 31{,}536{,}000 \approx 634$ năm thôi chứ mấy :)) nên time limit 1 giây thì chắc chắn TLE rồi.

Bài này cách tối ưu giải quyết trong $O(1)$ em nha, không cần lặp gì cả, làm vài phép tính là ra thôi, ra kết quả NGAY LẬP TỨC. Để làm được điều đó em cần phải nháp nhiều chút để nhìn ra quy luật.

Kết quả lệch 1 đơn vị thì debug thế nào?

HV
“e giải ko ra a ơi. sai sót 1 đơn vị th a. E có thể xem giúp e sai chỗ nào mà kq chỉ lệch 1 đvi ko ạ”

Em có biết là trên trang web này nó cho xem trường hợp sai không? Chỉ cần em bấm vào đó nó sẽ cho xem đó. Còn nếu đã xem được test sai nhưng test sai lớn quá em khó debug thì em random test nhỏ, sau đó viết code vét cạn để check đi (mình có để công cụ kiểm tra ở cuối bài).

Ý tưởngĐặt mốc X = L, Y = T − X rồi đếm bằng công thức

HV
“a giải thích cách làm tối ưu giúp e với ạ”

Muốn hiểu nó thì em cứ thử nhiều ví dụ, chạy tay quy trình logic nó xử lý để hiểu cách thức thôi hà. Như anh đã nói trong phần gợi ý đó: nếu đặt mốc $X = L$ và $Y = T - X$, nếu $Y \le R$ thì lúc này em sẽ thấy nó có các cặp theo kiểu:

$(X, Y)$$(X+1, Y-1)$$(X+2, Y-2)$…

cứ thế… đến khi nào $X \ge Y$ thì dừng.

Ví dụ đề: L = 5, R = 10, T = 13 (Y ≤ R)
X567Y8910
$X = 5$, $Y = 8$. Các cặp: (5, 8), (6, 7) → 2 cặp.

Thì công thức tính nhanh như anh đã đưa ra là: tính khoảng cách ngay từ đầu giữa $Y$ và $X$. Nếu khoảng cách đó mà chia hết cho 2 thì đáp án chính là $\frac{Y - X}{2}$, còn nếu khoảng cách đó không chia hết cho 2 thì đáp án chính là $\frac{Y - X}{2} + 1$ (chia nguyên).

Còn trường hợp nếu đặt $X = L$, tính $Y = T - X$ mà thấy $Y > R$ thì reset cho $Y = R$ (tức là giảm $Y$ đi 1 khoảng bằng hiệu giữa $Y$ lúc chưa giảm và $R$), và $X$ cũng tăng đồng thời với khoảng cách hiệu đó. Rồi xét nếu $X$ vẫn thoả $X < Y$ thì đáp án vẫn được tính nhanh như công thức trên: tính hiệu $(Y - X)$, nếu chia hết cho 2 thì đáp án là $\frac{Y - X}{2}$, nếu không chia hết cho 2 thì đáp án là $\frac{Y - X}{2} + 1$.

L = 1, R = 12, T = 19 (Y > R, phải dời mốc)
123456X7891011Y12
$X = 7$, $Y = 12$. Ban đầu $Y = T - L = 18 > R = 12$ nên dời $Y$ về $R$ và $X$ tăng thêm $6$. Các cặp: (7, 12), (8, 11), (9, 10) → 3 cặp.

Lý do vì sao thì anh nháp qua nhiều tình huống và luôn thấy công thức này đúng :)) chứ chưa đi chứng minh làm gì, nhưng nháp qua cả chục trường hợp nhẩm thấy đều đúng cả. Ở đây mình viết thêm phần chứng minh ngắn cho bạn nào thích chắc chắn:

Vì sao số cặp bằng ⌈(Y − X) / 2⌉?
Các cặp có dạng $(X + i,\ Y - i)$ với $i = 0, 1, 2, \ldots$ Cặp hợp lệ khi $X + i < Y - i \iff 2i < Y - X \iff i < \frac{Y - X}{2}$. Số giá trị nguyên $i \ge 0$ thoả mãn là $\left\lceil \frac{Y - X}{2} \right\rceil$, chính là $\frac{Y-X}{2}$ khi $Y - X$ chẵn và $\frac{Y-X}{2} + 1$ (chia nguyên) khi $Y - X$ lẻ. Vì $X$ chỉ tăng, $Y$ chỉ giảm nên cả hai luôn nằm trong $[L, R]$.

Còn câu lệnh if đầu tiên trong code là để loại sớm trường hợp không có cặp nào: cặp nhỏ nhất có thể là $L + (L + 1) = 2L + 1$, cặp lớn nhất có thể là $(R - 1) + R = 2R - 1$. Nếu $T < 2L + 1$ hoặc $T > 2R - 1$ thì đáp án là $0$.

Mô phỏng ý tưởng

Em nhập $L, R, T$ (đoạn ngắn thôi, $R - L \le 20$) rồi bấm Tiếp để xem từng bước: đặt mốc $X, Y$, dời mốc nếu $Y > R$, ghép từng cặp và so với công thức:

Ví dụ nhanh:

Cách 2Code của anh – công thức O(1)

HV
“E AC đc r a. E muốn tham khảo code a”

Ok đây là code của anh cũng đã AC, code này thì anh code nhanh ra ngay khi nhìn thấy ý tưởng chứ cũng chưa có clean code và cũng không đảm bảo ý tưởng này là tối ưu nhất nha em:

#include <bits/stdc++.h>
using namespace std;

// L <= a < b <= R, a + b = t
long long solve2(long long l, long long r, long long t){
    if(t < l + l + 1 || t > r + r - 1){
        return 0;
    }
    long long x = l;
    long long y = t - x;
    if(y <= r){
        long long res = (y - x) / 2;
        if((y - x) % 2 != 0){
            res++;
        }
        return res;
    }
    // y > r
    x += y - r;
    y = r;
    if(x < y){
        long long res = (y - x) / 2;
        if((y - x) % 2 != 0){
            res++;
        }
        return res;
    }
    return 0;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long l, r, t;
    cin >> l >> r >> t;
    cout << solve2(l, r, t);
    return 0;
}
# L <= a < b <= R, a + b = t
def solve2(l, r, t):
    if t < l + l + 1 or t > r + r - 1:
        return 0

    x = l
    y = t - x
    if y <= r:
        res = (y - x) // 2
        if (y - x) % 2 != 0:
            res += 1
        return res
    # y > r
    x += y - r
    y = r
    if x < y:
        res = (y - x) // 2
        if (y - x) % 2 != 0:
            res += 1
        return res
    return 0

l = int(input())
r = int(input())
t = int(input())
print(solve2(l, r, t))
C++▶ Chạy tay từng dòng codeCách 2 (code của anh): đặt mốc X = L, Y = T − X, dời mốc nếu Y > R, rồi dùng công thức, O(1).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4// L <= a < b <= R, a + b = t
5long long solve2(long long l, long long r, long long t){
6 if(t < l + l + 1 || t > r + r - 1){
7 return 0;
8 }
9 long long x = l;
10 long long y = t - x;
11 if(y <= r){
12 long long res = (y - x) / 2;
13 if((y - x) % 2 != 0){
14 res++;
15 }
16 return res;
17 }
18 // y > r
19 x += y - r;
20 y = r;
21 if(x < y){
22 long long res = (y - x) / 2;
23 if((y - x) % 2 != 0){
24 res++;
25 }
26 return res;
27 }
28 return 0;
29}
30
31int main(){
32 ios::sync_with_stdio(0);
33 cin.tie(0);
34 long long l, r, t;
35 cin >> l >> r >> t;
36 cout << solve2(l, r, t);
37 return 0;
38}

Đánh giá độ phức tạp:

Không gian$O(1)$
Thời gian$O(1)$ – chỉ vài phép tính
Về tràn số: các giá trị trung gian lớn nhất là $2L + 1 \ge -2 \cdot 10^{18}$, $2R - 1 \le 2 \cdot 10^{18}$ và $Y - X = T - 2L \le 3 \cdot 10^{18}$, đều nằm trong long long (khoảng $\pm 9.2 \cdot 10^{18}$) nên yên tâm không bị tràn.

Cách 3Code AC của em – nhìn từ giữa ra hai bên

HV
“E thấy các cặp số đều từ n/2 và tăng dần x-1 y+1,, x-2 y+2 nên e lấy khoảng cách giới hạn nhỏ nhất theo giới hạn để in ra ntn có đr ko a”
#include <bits/stdc++.h>
using namespace std;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    long long r,l,n;
    cin >> l >> r >> n;
    if (n<l*2||n>r*2)
    {
        cout << 0;
    }
    else
    {
        if (n%2==0)
        {
            cout << min(n/2+1-l,r+1-n/2)-1;
        }
        else
            cout << min(n/2-l+1,r+1-(n-n/2))-(n<0);
    }
}
def half(n):
    # chia nguyên kiểu C++ (làm tròn về 0), khác với // của Python khi n âm
    return n // 2 if n >= 0 else -((-n) // 2)

l = int(input())
r = int(input())
n = int(input())
if n < l * 2 or n > r * 2:
    print(0)
elif n % 2 == 0:
    print(min(half(n) + 1 - l, r + 1 - half(n)) - 1)
else:
    print(min(half(n) - l + 1, r + 1 - (n - half(n))) - (1 if n < 0 else 0))
C++▶ Chạy tay từng dòng codeCách 3 (code AC của em): đứng ở n/2 toả ra hai bên, số cặp bị chặn bởi biên gần hơn, O(1).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main()
4{
5 ios::sync_with_stdio(false);
6 cin.tie(0);
7 cout.tie(0);
8 long long r,l,n;
9 cin >> l >> r >> n;
10 if (n<l*2||n>r*2)
11 {
12 cout << 0;
13 }
14 else
15 {
16 if (n%2==0)
17 {
18 cout << min(n/2+1-l,r+1-n/2)-1;
19 }
20 else
21 cout << min(n/2-l+1,r+1-(n-n/2))-(n<0);
22 }
23}

Thì em đúng em mới AC đó :)) Có nhiều cách thức xác định khác nhau mà, anh thì nhìn ra hướng như trên, em thì nhìn ra hướng khác, không sao, miễn cùng giải quyết được vấn đề, AC thể hiện rõ điều đó.

Mình giải thích thêm hướng của em cho các bạn dễ hình dung: em đứng ở chính giữa $\frac{n}{2}$ rồi toả ra hai bên $(\frac{n}{2} - 1, \frac{n}{2} + 1)$, $(\frac{n}{2} - 2, \frac{n}{2} + 2)$… Số cặp bị chặn bởi bên nào chạm biên trước: bên trái chạm $L$ hay bên phải chạm $R$, nên mới có min(...). Với $n$ chẵn thì cặp $(\frac{n}{2}, \frac{n}{2})$ không hợp lệ vì $x < y$ nên mới trừ 1. Với $n$ lẻ âm thì phép chia của C++ làm tròn về 0 (ví dụ $-7 / 2 = -3$ chứ không phải $-4$), nên phần tử bên trái bị lệch lên 1 và em phải trừ thêm (n < 0). Chi tiết này rất dễ sai, cũng là lý do bản Python mình viết thêm phải tự viết hàm chia kiểu C++.

Kiểm traCông cụ so sánh với code vét cạn

Còn nếu em lo lắng thì thử làm cái tool check cách tối ưu với cách vét cạn xem có trường hợp nào sai không? Nếu em chưa biết cách làm thì xem code dưới đây nha.

Hàm solve1 là code vét cạn của anh, solve2 là code tối ưu của anh, solve3 là code tối ưu của em. Thử random cùng bộ dữ liệu chạy vào 3 hàm xem có cùng trả ra kết quả giống nhau không? Nếu để tầm vài phút mà vẫn thấy toàn chữ OK thì ổn, nếu thấy chữ FAIL hiện ra thì có test case cụ thể được in ra và 3 đáp án trả ra để theo đó kiểm tra lại xem là cách làm nào bị sai. Khả năng chỉ có thể solve2 hay solve3 sai chứ solve1 là code vét cạn thì khó mà sai lắm.

#include <bits/stdc++.h>
using namespace std;

long long solve1(long long l, long long r, long long t){
    long long res = 0;
    for(long long i = l; i <= r; ++i){
        for(long long j = i + 1; j <= r; ++j){
            if(i + j == t){
                res++;
            }
        }
    }
    return res;
}

// L <= a < b <= R, a + b = t
long long solve2(long long l, long long r, long long t){
    if(t < l + l + 1 || t > r + r - 1){
        return 0;
    }
    long long x = l;
    long long y = t - x;
    if(y <= r){
        long long res = (y - x) / 2;
        if((y - x) % 2 != 0){
            res++;
        }
        return res;
    }
    // y > r
    x += y - r;
    y = r;
    if(x < y){
        long long res = (y - x) / 2;
        if((y - x) % 2 != 0){
            res++;
        }
        return res;
    }
    return 0;
}

long long solve3(long long l, long long r, long long n)
{
    if (n<l*2||n>r*2)
    {
        return 0;
    }
    else
    {
        if (n%2==0)
        {
            return min(n/2+1-l,r+1-n/2)-1;
        }
        else
            return min(n/2-l+1,r+1-(n-n/2))-(n<0);
    }
}

int main(){
    srand(time(0));
    while(1){
        long long l = -100 + rand() % 100;
        long long r = l + rand() % 100;
        long long t = rand() % 300;
        long long res1 = solve1(l, r, t);
        long long res2 = solve2(l, r, t);
        long long res3 = solve3(l, r, t);
        if(res1 == res2 && res2 == res3){
            cout << "OK\n";
        }
        else{
            cout << "FAIL\n";
            cout << l << " " << r << " " << t;
            cout << "\nres1 = " << res1;
            cout << "\nres2 = " << res2;
            cout << "\nres3 = " << res3;
            break;
        }
    }
    return 0;
}
import random
import time

def solve1(l, r, t):
    res = 0
    for i in range(l, r + 1):
        for j in range(i + 1, r + 1):
            if i + j == t:
                res += 1
    return res

# L <= a < b <= R, a + b = t
def solve2(l, r, t):
    if t < l + l + 1 or t > r + r - 1:
        return 0
    x = l
    y = t - x
    if y <= r:
        res = (y - x) // 2
        if (y - x) % 2 != 0:
            res += 1
        return res
    # y > r
    x += y - r
    y = r
    if x < y:
        res = (y - x) // 2
        if (y - x) % 2 != 0:
            res += 1
        return res
    return 0

def solve3(l, r, n):
    if n < l * 2 or n > r * 2:
        return 0
    else:
        if n % 2 == 0:
            return min(n // 2 + 1 - l, r + 1 - n // 2) - 1
        else:
            return min(n // 2 - l + 1, r + 1 - (n - n // 2)) - int(n < 0)

random.seed(time.time())

while True:
    l = -100 + random.randint(0, 99)
    r = l + random.randint(0, 99)
    t = random.randint(0, 299)

    res1 = solve1(l, r, t)
    res2 = solve2(l, r, t)
    res3 = solve3(l, r, t)

    if res1 == res2 and res2 == res3:
        print("OK")
    else:
        print("FAIL")
        print(f"{l} {r} {t}")
        print(f"res1 = {res1}")
        print(f"res2 = {res2}")
        print(f"res3 = {res3}")
        break
Ghi chú kiểm tra của mình: cả 3 code C++ (cách 1, 2, 3) cùng 3 bản Python đã được so với code vét cạn trên 400 bộ test ngẫu nhiên nhỏ (có cả $L, R, T$ âm) – khớp hết. Cách 2 và cách 3 (C++ lẫn Python) còn được so với công thức đếm chính xác trên 600 bộ test lớn tới $\pm 10^{18}$ (gồm cả các biên $L = -10^{18}, R = 10^{18}$) – khớp hết, mỗi test chạy chưa tới 0.01 giây. Cách 1 với $R - L = 2 \cdot 10^6$ chạy khoảng 0.01 giây nhưng với $R - L = 2 \cdot 10^{18}$ thì chắc chắn TLE như đã phân tích. Công cụ kiểm tra ở trên chạy liên tục in ra toàn OK.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Nhận xét