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

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ọng và thường xuyên được sử dụng trong C++: Tham chiếu (References) và 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 soGoc
và thamChieuSo
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 main
là x
), và b
là tham chiếu đến biến thứ hai (y
). Mọi thao tác trên a
và b
trong hàm đều trực tiếp ảnh hưởng đến x
và y
ở ngoài hàm. Kết quả là sau khi gọi swapValues(x, y)
, giá trị của x
và y
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ácnullptr
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:
- Giây 1: Tiến 3 bước. Vị trí: 0 + 3 = 3.
- Giây 2: Lùi 1 bước. Vị trí: 3 - 1 = 2.
- Giây 3: Tiến 3 bước. Vị trí: 2 + 3 = 5.
- Giây 4: Lùi 1 bước. Vị trí: 5 - 1 = 4.
- 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.
- Nếu
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ếuk % 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
.
- Ví dụ
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)
vì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++:
- Sử dụng thư viện
iostream
để nhập/xuất dữ liệu chuẩn (cin
,cout
). - Đọc số lượng test case
T
. - Sử dụng vòng lặp
while
hoặcfor
để xử lý từng test caseT
lần. - 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'
).
- Đọc giá trị
- Để xử lý nhanh với số lượng test case lớn (
T
lên đến 100000) và tổngk
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òngios_base::sync_with_stdio(false); cin.tie(NULL);
ở đầu hàmmain
.
Lưu ý:
- Sử dụng kiểu dữ liệu
int
choT
vàk
là đủ vì các giá trị này nằm trong giới hạn củaint
. Vị trí cuối cùng cũng sẽ nằm trong giới hạn củaint
. - Đả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
Comments