Bài 31.2: Tham chiếu và giải tham chiếu trong C++

Chào mừng trở lại với series blog về C++! Hôm nay, chúng ta sẽ cùng nhau đi sâu vào hai khái niệm vô cùng quan trọngthường xuyên được sử dụng trong C++: Tham chiếu (References)Giải tham chiếu (Dereferencing). Hiểu rõ về chúng không chỉ giúp bạn viết code hiệu quả hơn mà còn là nền tảng để làm việc với con trỏ và quản lý bộ nhớ phức tạp hơn sau này.

Tại sao chúng ta cần đến tham chiếu hay giải tham chiếu? C++ là một ngôn ngữ mạnh mẽ cho phép kiểm soát sát sao tài nguyên máy tính, bao gồm cả bộ nhớ. Đôi khi, chúng ta cần thao tác trực tiếp với chính dữ liệu gốc thay vì một bản sao của nó, hoặc cần làm việc với các vùng nhớ thông qua địa chỉ của chúng. Đây chính là lúc tham chiếu và giải tham chiếu phát huy sức mạnh.

🚀 Tham chiếu (References): Cái tên gọi khác đầy quyền năng

Hãy tưởng tượng bạn có một người bạn thân và bạn đặt cho họ một biệt danh. Mỗi khi bạn gọi biệt danh đó, thực chất là bạn đang nói chuyện với chính người bạn đó, chứ không phải một người khác. Tham chiếu trong C++ cũng hoạt động tương tự vậy.

Tham chiếu là một bí danh (alias) cho một biến đã tồn tại. Nó không phải là một biến mới, cũng không phải là một bản sao của biến gốc. Thay vào đó, nó cung cấp một cách khác để truy cập và thao tác với cùng một vùng nhớ mà biến gốc đang chiếm giữ.

Đặc điểm nổi bật của Tham chiếu:
  • Phải được khởi tạo ngay lập tức: Khi khai báo một tham chiếu, bạn phải gán nó cho một biến đã tồn tại.
  • Không thể "gán lại" (re-bind): Sau khi được khởi tạo để tham chiếu đến một biến, tham chiếu đó sẽ luôn tham chiếu đến biến đó. Bạn không thể thay đổi để nó tham chiếu đến một biến khác.
  • Không thể là null: Một tham chiếu luôn tham chiếu đến một biến hợp lệ.
  • Sử dụng đơn giản: Khi đã khai báo, bạn sử dụng tên của tham chiếu giống như bạn sử dụng tên của biến gốc. Trình biên dịch sẽ tự động "theo dõi" đến biến gốc cho bạn.
Cú pháp khai báo Tham chiếu:

Bạn sử dụng ký hiệu & sau kiểu dữ liệu khi khai báo tham chiếu:

kiểu_dữ_liệu& tên_tham_chiếu = biến_gốc;
Ví dụ minh họa Tham chiếu cơ bản:
#include <iostream>

int main() {
    using namespace std;
    int a = 10;
    int& ra = a;

    cout << "Gia tri ban dau cua a: " << a << endl;
    cout << "Gia tri ban dau cua ra: " << ra << endl;

    ra = 25;

    cout << "\nSau khi thay doi qua tham chieu:" << endl;
    cout << "Gia tri cua a: " << a << endl;
    cout << "Gia tri cua ra: " << ra << endl;

    a = 5;

    cout << "\nSau khi thay doi qua bien goc:" << endl;
    cout << "Gia tri cua a: " << a << endl;
    cout << "Gia tri cua ra: " << ra << endl;

    cout << "\nDia chi cua a: " << &a << endl;
    cout << "Dia chi cua ra: " << &ra << endl;

    return 0;
}

Output:

Gia tri ban dau cua a: 10
Gia tri ban dau cua ra: 10

Sau khi thay doi qua tham chieu:
Gia tri cua a: 25
Gia tri cua ra: 25

Sau khi thay doi qua bien goc:
Gia tri cua a: 5
Gia tri cua ra: 5

Dia chi cua a: 0x7ffeee6717a4
Dia chi cua ra: 0x7ffeee6717a4

Giải thích: Trong ví dụ này, thamChieuSo không phải là một biến mới chứa giá trị 10, mà nó chỉ là một cái tên khác để chỉ cùng vùng bộ nhớ mà soGoc đang sử dụng. Bất kỳ thay đổi nào được thực hiện thông qua thamChieuSo sẽ ảnh hưởng trực tiếp đến soGoc và ngược lại. Khi in địa chỉ bộ nhớ bằng toán tử &, chúng ta thấy rõ ràng soGocthamChieuSo cùng trỏ đến một vị trí.

Tham chiếu và Hàm: Truyền dữ liệu hiệu quả

Một trong những ứng dụng phổ biến và mạnh mẽ nhất của tham chiếu là khi làm việc với hàm.

Truyền tham số theo tham chiếu (Pass by Reference):

Khi truyền một biến vào hàm theo tham chiếu, hàm sẽ làm việc trực tiếp trên biến gốc, chứ không phải một bản sao. Điều này cực kỳ hữu ích khi bạn muốn hàm thay đổi giá trị của biến được truyền vào, hoặc khi bạn muốn tránh việc tạo bản sao tốn kém cho các đối tượng lớn.

#include <iostream>
#include <string>

void hoanDoi(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}

void inTT(const string& ten, int tuoi) {
    cout << "Ten: " << ten << ", Tuoi: " << tuoi << endl;
}

int main() {
    using namespace std;
    int x = 5;
    int y = 10;

    cout << "Truoc khi hoan doi: x = " << x << ", y = " << y << endl;
    hoanDoi(x, y);
    cout << "Sau khi hoan doi: x = " << x << ", y = " << y << endl;

    string s = "Nguyen Van A rat rat nhieu ky tu de minh hoa";
    inTT(s, 30);

    return 0;
}

Output:

Truoc khi hoan doi: x = 5, y = 10
Sau khi hoan doi: x = 10, y = 5
Ten: Nguyen Van A rat rat nhieu ky tu de minh hoa, Tuoi: 30

Giải thích: Hàm swapValues nhận hai tham số là int&. Điều này có nghĩa là a là một tham chiếu đến biến đầu tiên được truyền vào (trong mainx), và b là tham chiếu đến biến thứ hai (y). Mọi thao tác trên ab trong hàm đều trực tiếp ảnh hưởng đến xy ở ngoài hàm. Kết quả là sau khi gọi swapValues(x, y), giá trị của xy trong main thực sự bị đổi chỗ.

Đối với printUserInfo, chúng ta truyền string theo tham chiếu const string&. const đảm bảo rằng hàm không thể sửa đổi chuỗi gốc. Việc sử dụng tham chiếu (dù là const) giúp tránh việc tạo một bản sao đầy đủ của chuỗi, điều này rất hiệu quả khi làm việc với các đối tượng lớn.

Trả về theo tham chiếu (Return by Reference):

Một hàm có thể trả về một tham chiếu đến một biến. Điều này cho phép bạn gán giá trị cho kết quả của hàm như thể nó là một biến bên trái của dấu gán (=). Tuy nhiên, bạn phải cực kỳ cẩn thận khi trả về tham chiếu để không bao giờ trả về tham chiếu đến một biến cục bộ (local variable) của hàm, vì biến đó sẽ bị hủy khi hàm kết thúc, dẫn đến tham chiếu "lơ lửng" (dangling reference) và hành vi không xác định.

#include <iostream>
#include <vector>

int& layPtu(vector<int>& v, int i) {
    return v[i];
}

int main() {
    using namespace std;
    vector<int> a = {10, 20, 30, 40, 50};

    cout << "Vector ban dau: ";
    for (int x : a) {
        cout << x << " ";
    }
    cout << endl;

    layPtu(a, 2) = 99;

    cout << "Vector sau khi thay doi phan tu thu 2: ";
    for (int x : a) {
        cout << x << " ";
    }
    cout << endl;

    int& r = layPtu(a, 2);
    cout << "Gia tri cua phan tu thu 3 (qua tham chieu moi): " << r << endl;

    return 0;
}

Output:

Vector ban dau: 10 20 30 40 50 
Vector sau khi thay doi phan tu thu 2: 10 20 99 40 50 
Gia tri cua phan tu thu 3 (qua tham chieu moi): 99

Giải thích: Hàm getElement trả về int&, tức là một tham chiếu đến một số nguyên. Khi chúng ta viết getElement(numbers, 2) = 99;, thực chất chúng ta đang gán giá trị 99 vào chính phần tử thứ 3 (index 2) bên trong vector numbers. Tương tự, int& refToThirdElement = getElement(numbers, 2); tạo ra một tham chiếu mới tên là refToThirdElement trỏ đến cùng phần tử đó.

🎯 Giải tham chiếu (Dereferencing): Mở khóa giá trị từ địa chỉ

Trong khi tham chiếu là một bí danh cho một biến, giải tham chiếu là hành động truy cập giá trị mà một con trỏ đang trỏ tới. Nó giống như việc bạn có địa chỉ nhà của ai đó và bạn sử dụng địa chỉ đó để tìm đến ngôi nhà và xem bên trong có gì.

Trước khi đi sâu vào giải tham chiếu, chúng ta cần hiểu sơ qua về con trỏ.

Con trỏ (Pointers): Biến lưu trữ Địa chỉ

Con trỏ là một loại biến đặc biệt trong C++ được thiết kế để lưu trữ địa chỉ bộ nhớ của các biến khác. Thay vì lưu trữ giá trị trực tiếp (như số nguyên, ký tự, v.v.), con trỏ lưu trữ "vị trí" của giá trị đó trong RAM.

Cú pháp khai báo Con trỏ:

Bạn sử dụng ký hiệu * sau kiểu dữ liệu khi khai báo con trỏ:

kiểu_dữ_liệu* tên_con_trỏ; // Khai báo con trỏ

Để lấy địa chỉ của một biến, bạn sử dụng toán tử lấy địa chỉ (&):

kiểu_dữ_liệu biến_gốc;
kiểu_dữ_liệu* tên_con_trỏ = &biến_gốc; // Con trỏ lưu trữ địa chỉ của biến_gốc

Con trỏ có thể là nullptr (hoặc NULL trong C++ cũ hơn), nghĩa là nó không trỏ đến bất kỳ vùng bộ nhớ hợp lệ nào.

Cú pháp Giải tham chiếu:

Để truy cập giá trị mà một con trỏ đang trỏ tới, bạn sử dụng toán tử giải tham chiếu (*) đứng trước tên con trỏ:

*tên_con_trỏ

Toán tử * ở đây không phải là khai báo con trỏ nữa, mà nó có nghĩa là "giá trị tại địa chỉ mà con trỏ này đang trỏ tới".

Ví dụ minh họa Giải tham chiếu cơ bản:
#include <iostream>

int main() {
    using namespace std;
    int v = 42;
    int* pv = &v;

    cout << "Gia tri cua bien v: " << v << endl;
    cout << "Dia chi cua bien v: " << &v << endl;
    cout << "Gia tri cua con tro pv: " << pv << endl;

    cout << "\nGia tri tai dia chi ma pv dang tro toi (*pv): " << *pv << endl;

    *pv = 100;

    cout << "\nSau khi thay doi qua giai tham chieu (*pv = 100):" << endl;
    cout << "Gia tri cua bien v: " << v << endl;
    cout << "Gia tri tai dia chi ma pv dang tro toi (*pv): " << *pv << endl;

    int av = 500;
    pv = &av;

    cout << "\nSau khi con tro pv tro toi av:" << endl;
    cout << "Dia chi cua av: " << &av << endl;
    cout << "Gia tri cua con tro pv: " << pv << endl;
    cout << "Gia tri tai dia chi ma pv dang tro toi (*pv): " << *pv << endl;

    return 0;
}

Output:

Gia tri cua bien v: 42
Dia chi cua bien v: 0x7ffee03167a4
Gia tri cua con tro pv: 0x7ffee03167a4

Gia tri tai dia chi ma pv dang tro toi (*pv): 42

Sau khi thay doi qua giai tham chieu (*pv = 100):
Gia tri cua bien v: 100
Gia tri tai dia chi ma pv dang tro toi (*pv): 100

Sau khi con tro pv tro toi av:
Dia chi cua av: 0x7ffee03167a8
Gia tri cua con tro pv: 0x7ffee03167a8
Gia tri tai dia chi ma pv dang tro toi (*pv): 500

Giải thích: Trong ví dụ này, pValue là một con trỏ kiểu int*. Dòng int* pValue = &value; làm cho pValue lưu trữ địa chỉ bộ nhớ của biến value. Khi chúng ta sử dụng *pValue, chúng ta đang yêu cầu trình biên dịch truy cập vào vùng bộ nhớ mà pValue đang trỏ tới và lấy giá trị ở đó (ban đầu là 42). Khi chúng ta gán *pValue = 100;, chúng ta đang thay đổi giá trị tại vùng bộ nhớ đó, điều này làm cho giá trị của value cũng thay đổi theo. Cuối cùng, chúng ta thấy con trỏ có thể được gán lại để trỏ đến một biến khác (anotherValue).

Giải tham chiếu với Con trỏ và Mảng:

Mảng trong C++ thường liên quan chặt chẽ đến con trỏ và giải tham chiếu. Tên của mảng (khi đứng một mình) thường suy biến thành một con trỏ tới phần tử đầu tiên của mảng.

#include <iostream>

int main() {
    using namespace std;
    int arr[] = {10, 20, 30, 40, 50};
    int* p = arr;

    cout << "Gia tri phan tu dau tien qua con tro (*p): " << *p << endl;

    p++;

    cout << "Gia tri phan tu tiep theo qua con tro (*p sau khi tang p): " << *p << endl;

    cout << "Gia tri phan tu thu 3 qua giai tham chieu (*(arr + 2)): " << *(arr + 2) << endl;
    cout << "Gia tri phan tu thu 3 qua chi so mang (arr[2]): " << arr[2] << endl;

    *(arr + 3) = 400;
    cout << "Vector sau khi thay doi phan tu thu 4 qua *(arr + 3): ";
    for (int i = 0; i < 5; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}

Output:

Gia tri phan tu dau tien qua con tro (*p): 10
Gia tri phan tu tiep theo qua con tro (*p sau khi tang p): 20
Gia tri phan tu thu 3 qua giai tham chieu (*(arr + 2)): 30
Gia tri phan tu thu 3 qua chi so mang (arr[2]): 30
Vector sau khi thay doi phan tu thu 4 qua *(arr + 3): 10 20 30 400 50

Giải thích: Tên mảng numbers tự nó là một con trỏ int* trỏ đến phần tử numbers[0]. Do đó, chúng ta có thể gán p = numbers;. Sau đó, *p truy cập giá trị của numbers[0]. Khi chúng ta tăng con trỏ p++, nó không tăng giá trị của p lên 1, mà tăng lên theo kích thước của kiểu dữ liệu mà nó trỏ tới (ở đây là int, thường là 4 byte). Do đó, p bây giờ trỏ tới phần tử numbers[1]. Cú pháp *(numbers + i) là cách khác để truy cập phần tử i của mảng thông qua con trỏ, tương đương với numbers[i]. numbers + i là địa chỉ của phần tử thứ i, và * giải tham chiếu địa chỉ đó để lấy giá trị.

Lưu ý khi Giải tham chiếu:
  • Cẩn thận với nullptr: Giải tham chiếu một con trỏ nullptr sẽ gây ra lỗi chương trình nghiêm trọng (thường là segmentation fault) và là một trong những lỗi phổ biến nhất khi làm việc với con trỏ. Luôn kiểm tra xem con trỏ có khác nullptr trước khi giải tham chiếu nếu có khả năng nó bị null.
  • Con trỏ "lơ lửng" (Dangling Pointers): Xảy ra khi con trỏ trỏ tới một vùng nhớ đã bị giải phóng hoặc không còn hợp lệ. Giải tham chiếu con trỏ lơ lửng cũng dẫn đến hành vi không xác định.

🤔 So sánh nhanh: Tham chiếu vs. Con trỏ

Mặc dù cả tham chiếu và con trỏ đều cho phép bạn làm việc gián tiếp với dữ liệu, chúng có những khác biệt quan trọng:

Đặc điểm Tham chiếu (References) Con trỏ (Pointers)
Khai báo Sử dụng & Sử dụng *
Khởi tạo Bắt buộc phải khởi tạo khi khai báo Có thể khai báo mà chưa cần khởi tạo (nhưng nên gán nullptr)
Null Không thể là null Có thể là nullptr
Gán lại Không thể gán lại để tham chiếu biến khác Có thể gán lại để trỏ đến biến khác
Sử dụng Dùng tên tham chiếu như biến gốc Cần dùng * để giải tham chiếu lấy giá trị
Địa chỉ Lấy địa chỉ của biến gốc (&tham_chiếu) Lưu trữ địa chỉ (con_trỏ)
Độ an toàn An toàn hơn (không null, không lơ lửng dễ dàng) Ít an toàn hơn, dễ gây lỗi nếu không cẩn thận

Khi nào sử dụng cái gì?

  • Sử dụng tham chiếu khi bạn muốn một bí danh cho một biến đã tồn tại và bạn đảm bảo nó luôn trỏ đến một đối tượng hợp lệ. Rất tốt cho việc truyền tham số cho hàm để sửa đổi biến gốc hoặc tránh sao chép đối tượng lớn (const& cho việc tránh sao chép mà không sửa đổi).
  • Sử dụng con trỏ khi bạn cần quản lý bộ nhớ linh hoạt hơn: làm việc với bộ nhớ heap (cấp phát động), khi con trỏ có thể không trỏ đến đâu (là nullptr), hoặc khi bạn cần các kỹ thuật như con trỏ đến con trỏ, con trỏ hàm, v.v. Con trỏ mang lại sức mạnh nhưng đòi hỏi trách nhiệm cao hơn trong việc quản lý.

Bài tập ví dụ: C++ Bài 15.A2: Người say rượu

Người say rượu

FullHouse Dev rất buồn sau khi biết tin về một sự kiện không may. Anh ấy cố gắng giải sầu bằng rượu và trở nên say xỉn. Bây giờ anh ấy muốn về nhà nhưng không thể đi thẳng được. Cứ mỗi 3 bước tiến về phía trước, anh ấy lại lùi lại 1 bước.

Cụ thể, trong giây thứ nhất anh ấy tiến 3 bước, giây thứ hai lùi 1 bước, giây thứ ba tiến 3 bước, giây thứ tư lùi 1 bước, và cứ tiếp tục như vậy.

FullHouse Dev sẽ ở cách vị trí ban đầu bao xa sau k giây? Giả sử vị trí ban đầu của FullHouse Dev là 0.

Đề bài

Input:
  • Dòng đầu tiên chứa một số nguyên T - số lượng test case.
  • Mỗi test case gồm một dòng chứa số nguyên k - số giây đã trôi qua.
Output:
  • Với mỗi test case, in ra một dòng chứa một số nguyên - vị trí của FullHouse Dev sau k giây.
Ràng buộc:
  • 1 ≤ T ≤ 100000
  • 0 ≤ k ≤ 100000
  • Tổng của k trong tất cả các test case không vượt quá 1000000

Ví dụ:

Input:
3
5
11
23
Output:
7
13
25

Giải thích:

Test case 1:

  • Giây 1: tiến 3 bước, đến vị trí 3
  • Giây 2: lùi 1 bước, đến vị trí 2
  • Giây 3: tiến 3 bước, đến vị trí 5
  • Giây 4: lùi 1 bước, đến vị trí 4
  • Giây 5: tiến 3 bước, đến vị trí 7 Tuyệt vời, đây là hướng dẫn giải bài toán "Người say rượu" bằng C++ theo yêu cầu của bạn:

Bài toán yêu cầu tính vị trí của người say rượu sau k giây, biết rằng trong giây thứ i, anh ấy tiến 3 bước nếu i lẻ, và lùi 1 bước nếu i chẵn. Vị trí ban đầu là 0.

Phân tích quy luật di chuyển:

  1. Giây 1: Tiến 3 bước. Vị trí: 0 + 3 = 3.
  2. Giây 2: Lùi 1 bước. Vị trí: 3 - 1 = 2.
  3. Giây 3: Tiến 3 bước. Vị trí: 2 + 3 = 5.
  4. Giây 4: Lùi 1 bước. Vị trí: 5 - 1 = 4.
  5. Giây 5: Tiến 3 bước. Vị trí: 4 + 3 = 7.

Quan sát trong 2 giây liên tiếp (một giây lẻ, một giây chẵn):

  • Trong 2 giây đầu (1 và 2): Tiến 3, Lùi 1. Tổng dịch chuyển: +3 - 1 = +2. Vị trí sau 2 giây là 2.
  • Trong 2 giây tiếp theo (3 và 4): Tiến 3, Lùi 1. Tổng dịch chuyển: +3 - 1 = +2. Vị trí sau 4 giây là vị trí sau 2 giây (+2) = 2 + 2 = 4.
  • Trong 2 giây tiếp theo (5 và 6 - nếu có): Tiến 3, Lùi 1. Tổng dịch chuyển: +3 - 1 = +2. Vị trí sau 6 giây sẽ là vị trí sau 4 giây (+2) = 4 + 2 = 6.

Nhận thấy: Cứ sau mỗi cặp 2 giây (một giây lẻ, một giây chẵn), người say rượu dịch chuyển tịnh tiến thêm 2 bước.

Xây dựng công thức:

  • Sau k giây, có bao nhiêu cặp 2 giây hoàn chỉnh? Số cặp này chính là k / 2 (phép chia số nguyên).
  • Mỗi cặp 2 giây dịch chuyển +2 bước. Tổng dịch chuyển từ các cặp 2 giây là (k / 2) * 2.

  • Sau khi kết thúc các cặp 2 giây hoàn chỉnh, còn lại k % 2 giây.

    • Nếu k % 2 == 0: Không còn giây nào lẻ sau các cặp hoàn chỉnh. Vị trí cuối cùng chỉ do các cặp 2 giây quyết định.
    • Nếu k % 2 == 1: Còn 1 giây lẻ sau các cặp hoàn chỉnh. Giây lẻ này sẽ là giây thứ (k/2)*2 + 1 (tức là giây thứ k). Theo quy luật, ở giây lẻ, anh ta tiến 3 bước.

Vậy, công thức cho vị trí cuối cùng sẽ là:

  • Số lượng dịch chuyển từ các cặp 2 giây: (k / 2) * 2
  • Số lượng dịch chuyển từ giây cuối cùng (nếu k lẻ): Nếu k % 2 == 1, thêm 3. Nếu k % 2 == 0, thêm 0. Điều này có thể viết gọn là (k % 2) * 3.

Vị trí cuối cùng = (k / 2) * 2 + (k % 2) * 3

Hoặc gọn hơn:

  • Nếu k chẵn (k % 2 == 0): Vị trí = (k / 2) * 2 + 0 * 3 = k.
  • Nếu k lẻ (k % 2 == 1): Vị trí = (k / 2) * 2 + 1 * 3 = (k / 2) * 2 + 3.
    • Ví dụ k=5: (5/2)*2 + 3 = 2*2 + 3 = 4 + 3 = 7.
    • Ví dụ k=11: (11/2)*2 + 3 = 5*2 + 3 = 10 + 3 = 13.
    • Ví dụ k=23: (23/2)*2 + 3 = 11*2 + 3 = 22 + 3 = 25.

Công thức (k / 2) * 2 + (k % 2) * 3 hoạt động cho cả k chẵn và lẻ.

Một cách viết gọn khác, dựa trên quan sát khi k lẻ vị trí là k+2:

  • Nếu k chẵn, vị trí là k.
  • Nếu k lẻ, vị trí là k + 2. Điều này có thể viết là k + 2 * (k % 2)k % 2 là 0 khi k chẵn và 1 khi k lẻ.

Vị trí cuối cùng = k + 2 * (k % 2)

Triển khai C++:

  1. Sử dụng thư viện iostream để nhập/xuất dữ liệu chuẩn (cin, cout).
  2. Đọc số lượng test case T.
  3. Sử dụng vòng lặp while hoặc for để xử lý từng test case T lần.
  4. Trong mỗi lần lặp:
    • Đọc giá trị k.
    • Tính toán kết quả bằng công thức k + 2 * (k % 2).
    • In kết quả ra màn hình, theo sau là ký tự xuống dòng (endl hoặc '\n').
  5. Để xử lý nhanh với số lượng test case lớn (T lên đến 100000) và tổng k lớn (lên đến 1000000), bạn nên tối ưu hóa luồng nhập xuất bằng cách thêm dòng ios_base::sync_with_stdio(false); cin.tie(NULL); ở đầu hàm main.

Lưu ý:

  • Sử dụng kiểu dữ liệu int cho Tk là đủ vì các giá trị này nằm trong giới hạn của int. Vị trí cuối cùng cũng sẽ nằm trong giới hạn của int.
  • Đảm bảo in kết quả cho mỗi test case trên một dòng riêng biệt.

Hướng giải này tập trung vào việc tìm ra quy luật toán học của bài toán thay vì mô phỏng từng bước di chuyển, giúp giải quyết bài toán hiệu quả ngay cả với k lớn.

#include <iostream>

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int t;
    cin >> t;
    while (t--) {
        int k;
        cin >> k;
        int kq = k + 2 * (k % 2);
        cout << kq << '\n';
    }
    return 0;
}

Output (với Input mẫu):

7
13
25

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.