Bài 7.5: Bài tập thực hành vẽ hình học trong C++

Chào mừng bạn trở lại với chuỗi bài học C++ của FullhouseDev! Sau khi đã làm quen với các loại vòng lặp (for, while, do-while) và cấu trúc điều khiển (if, else, switch), đã đến lúc chúng ta đặt những kiến thức này vào một bài tập thực hành cực kỳ thú vị và có tính trực quan cao: vẽ hình học bằng ký tự trên console!

Nghe có vẻ đơn giản, nhưng bài tập này sẽ giúp bạn rèn luyện khả năng suy luận logic, phân tích vấn đề thành các bước nhỏ và đặc biệt là làm chủ kỹ thuật sử dụng vòng lặp lồng nhau (nested loops)cấu trúc điều khiển một cách khéo léo. Chúng ta sẽ không sử dụng các thư viện đồ họa phức tạp, mà chỉ dùng những ký tự "text" quen thuộc như *, #, hay thậm chí là khoảng trắng () để tạo ra các hình dạng trên màn hình console đen trắng huyền thoại!

Hãy cùng bắt tay vào khám phá sức mạnh của code để "vẽ" nào!

Khái niệm cơ bản: Console là một lưới (Grid)

Hãy hình dung màn hình console như một lưới gồm các ô vuông nhỏ (tưởng tượng như một bảng Excel khổng lồ), mỗi ô có thể chứa một ký tự. Khi chúng ta in một ký tự ra console, nó sẽ hiển thị tại vị trí hiện tại của con trỏ và con trỏ sẽ di chuyển sang ô tiếp theo. Lệnh cout << "\n"; sẽ di chuyển con trỏ xuống đầu dòng tiếp theo.

Để vẽ các hình 2D, chúng ta cần điều khiển việc in ký tự tại từng "ô" trong lưới này. Cách tự nhiên nhất để làm việc với một lưới 2D là sử dụng hai vòng lặp lồng nhau:

  1. Vòng lặp ngoài sẽ điều khiển việc đi qua các hàng (rows).
  2. Vòng lặp bên trong (lồng vào vòng lặp ngoài) sẽ điều khiển việc đi qua các cột (columns) trong mỗi hàng đó.

Bằng cách này, chúng ta có thể truy cập và xử lý từng vị trí (ô) trên màn hình console theo tọa độ (hàng, cột).

#include <iostream> // Cần thư viện này để sử dụng cout và cin

int main() {
    int so_hang = 5;
    int so_cot = 10;

    // Vòng lặp ngoài: Đi qua từng hàng
    for (int i = 0; i < so_hang; ++i) {
        // Vòng lặp bên trong: Đi qua từng cột trong hàng hiện tại
        for (int j = 0; j < so_cot; ++j) {
            // Ở đây, chúng ta sẽ quyết định in ký tự gì tại vị trí (i, j)
            // Ví dụ: in ra một ký tự '*' cho mọi vị trí
            cout << "*";
        }
        // Kết thúc một hàng, xuống dòng để chuẩn bị in hàng tiếp theo
        cout << "\n";
    }

    return 0;
}

Giải thích:

  • Chúng ta khai báo hai biến so_hangso_cot để xác định kích thước của "khu vực vẽ".
  • Vòng lặp for (int i = 0; i < so_hang; ++i) chạy từ 0 đến so_hang - 1, đại diện cho các chỉ số hàng.
  • Bên trong, vòng lặp for (int j = 0; j < so_cot; ++j) chạy từ 0 đến so_cot - 1, đại diện cho các chỉ số cột trong hàng i hiện tại.
  • Câu lệnh cout << "*"; in ký tự * tại vị trí (i, j) hiện tại.
  • Sau khi vòng lặp cột hoàn thành (đã in xong tất cả các ký tự trong một hàng), cout << "\n"; sẽ xuống dòng, chuẩn bị cho vòng lặp ngoài in hàng tiếp theo.

Nếu chạy đoạn code trên, bạn sẽ thấy một hình chữ nhật đặc bằng ký tự *:

**********
**********
**********
**********
**********

Đây là nền tảng cơ bản nhất. Bây giờ, hãy nâng cao độ phức tạp một chút!

Vẽ Hình Chữ Nhật Rỗng

Thay vì in ký tự ở mọi vị trí, chúng ta chỉ muốn in ký tự ở đường viền của hình chữ nhật, còn bên trong là khoảng trắng. Đây là lúc cấu trúc điều khiển if/else phát huy tác dụng!

Chúng ta cần kiểm tra xem vị trí (hàng i, cột j) hiện tại có nằm trên đường viền hay không. Một vị trí nằm trên đường viền nếu:

  • Nó là hàng đầu tiên (i == 0)
  • Hoặc nó là hàng cuối cùng (i == so_hang - 1)
  • Hoặc nó là cột đầu tiên (j == 0)
  • Hoặc nó là cột cuối cùng (j == so_cot - 1)

Nếu bất kỳ điều kiện nào trong số này đúng, chúng ta in ký tự vẽ (ví dụ: *). Ngược lại, chúng ta in khoảng trắng ().

#include <iostream>

int main() {
    int so_hang, so_cot;

    cout << "Nhap so hang cho hinh chu nhat rong: ";
    cin >> so_hang; // Lấy kích thước từ người dùng
    cout << "Nhap so cot cho hinh chu nhat rong: ";
    cin >> so_cot; // Lấy kích thước từ người dùng

    cout << "Ve hinh chu nhat rong:\n";

    for (int i = 0; i < so_hang; ++i) { // Vòng lặp hàng
        for (int j = 0; j < so_cot; ++j) { // Vòng lặp cột
            // Kiểm tra xem vị trí (i, j) có nằm trên đường viền không
            if (i == 0 || i == so_hang - 1 || j == 0 || j == so_cot - 1) {
                cout << "*"; // Nếu ở viền, in '*'
            } else {
                cout << " "; // Nếu không ở viền, in khoảng trắng
            }
        }
        cout << "\n"; // Xuống dòng sau mỗi hàng
    }

    return 0;
}

Giải thích:

  • Chúng ta thêm phần nhập liệu để người dùng có thể tự xác định kích thước hình chữ nhật họ muốn vẽ.
  • Điểm mấu chốt là câu lệnh if (i == 0 || i == so_hang - 1 || j == 0 || j == so_cot - 1). Toán tử || (hoặc) kiểm tra xem có ít nhất một trong các điều kiện về vị trí hàng/cột đầu/cuối có đúng không.
  • Nếu điều kiện if đúng, cout << "*"; được thực thi.
  • Nếu điều kiện if sai (nghĩa là vị trí hiện tại không phải là đường viền), khối else được thực thi, in ra một khoảng trắng cout << " ";.

Với so_hang = 5so_cot = 10, kết quả sẽ trông như thế này:

**********
*        *
*        *
*        *
**********

(Chú ý: khoảng trắng bên trong là thật)

Mẹo nhỏ: Khi vẽ hình rỗng, bạn cần đảm bảo khoảng trắng bên trong có chiều rộng tương đương với ký tự bạn dùng cho đường viền để hình không bị lệch. Ký tự * thường có chiều rộng khác nhau trên các console khác nhau, nhưng với các bài tập cơ bản thì sự khác biệt này thường không quá ảnh hưởng.

Vẽ Tam Giác Vuông

Vẽ tam giác vuông đòi hỏi một sự thay đổi trong cách thức hoạt động của vòng lặp cột. Thay vì chạy một số lần cố định cho mỗi hàng, số lần chạy của vòng lặp cột sẽ phụ thuộc vào chỉ số của vòng lặp hàng!

Hãy xem xét một tam giác vuông góc dưới bên trái, có chiều cao h.

  • Hàng 0 (đầu tiên): có 1 ký tự.
  • Hàng 1: có 2 ký tự.
  • Hàng 2: có 3 ký tự.
  • ...
  • Hàng i: có i + 1 ký tự.
#include <iostream>

int main() {
    int chieu_cao;

    cout << "Nhap chieu cao tam giac vuong goc duoi trai: ";
    cin >> chieu_cao;

    cout << "Ve tam giac vuong goc duoi trai:\n";

    // Vòng lặp hàng
    for (int i = 0; i < chieu_cao; ++i) {
        // Vòng lặp cột: Số ký tự in ra phụ thuộc vào chỉ số hàng (i)
        for (int j = 0; j <= i; ++j) { // Chú ý điều kiện j <= i
            cout << "*";
        }
        cout << "\n"; // Xuống dòng sau mỗi hàng
    }

    return 0;
}

Giải thích:

  • Vòng lặp hàng vẫn chạy từ 0 đến chieu_cao - 1.
  • Vòng lặp cột for (int j = 0; j <= i; ++j) là điểm khác biệt. Điều kiện j <= i đảm bảo rằng:
    • Khi i = 0, j chạy từ 0 đến 0 (1 lần) -> in 1 *.
    • Khi i = 1, j chạy từ 0 đến 1 (2 lần) -> in 2 *.
    • Khi i = 2, j chạy từ 0 đến 2 (3 lần) -> in 3 *.
    • Và cứ thế cho đến hàng cuối cùng.

Kết quả với chieu_cao = 5:

*
**
***
****
*****

Bạn có thể dễ dàng biến tấu để vẽ các kiểu tam giác vuông khác bằng cách thay đổi điều kiện của vòng lặp cột hoặc kết hợp thêm việc in khoảng trắng dẫn đầu:

  • Tam giác vuông góc trên bên trái: Số ký tự trên hàng i (0-indexed) là chieu_cao - i.

    #include <iostream>
    
    int main() {
        int chieu_cao;
        cout << "Nhap chieu cao tam giac vuong goc tren trai: ";
        cin >> chieu_cao;
    
        cout << "Ve tam giac vuong goc tren trai:\n";
        for (int i = 0; i < chieu_cao; ++i) {
            // Vòng lặp cột: in height - i ký tự
            for (int j = 0; j < chieu_cao - i; ++j) {
                cout << "*";
            }
            cout << "\n";
        }
        return 0;
    }
    

    Kết quả với chieu_cao = 5:

    *****
    ****
    ***
    **
    *
  • Tam giác vuông góc dưới bên phải: Cần in khoảng trắng trước khi in ký tự. Số khoảng trắng trên hàng ichieu_cao - 1 - i. Số ký tự là i + 1.

    #include <iostream>
    
    int main() {
        int chieu_cao;
        cout << "Nhap chieu cao tam giac vuong goc duoi phai: ";
        cin >> chieu_cao;
    
        cout << "Ve tam giac vuong goc duoi phai:\n";
        for (int i = 0; i < chieu_cao; ++i) {
            // In khoảng trắng dẫn đầu
            for (int j = 0; j < chieu_cao - 1 - i; ++j) {
                cout << " ";
            }
            // In ký tự
            for (int k = 0; k <= i; ++k) {
                cout << "*";
            }
            cout << "\n";
        }
        return 0;
    }
    

    Kết quả với chieu_cao = 5:

        *
       **
      ***
     ****
    *****

    Giải thích: Ta dùng hai vòng lặp con bên trong vòng lặp hàng: một vòng để in số lượng khoảng trắng cần thiết để "đẩy" hình sang phải, và một vòng để in số lượng ký tự cho hàng đó. Số khoảng trắng giảm dần, số ký tự tăng dần.

  • Tam giác vuông góc trên bên phải: Tương tự, cần in khoảng trắng trước ký tự. Số khoảng trắng trên hàng ii. Số ký tự là chieu_cao - i.

    #include <iostream>
    
    int main() {
        int chieu_cao;
        cout << "Nhap chieu cao tam giac vuong goc tren phai: ";
        cin >> chieu_cao;
    
        cout << "Ve tam giac vuong goc tren phai:\n";
        for (int i = 0; i < chieu_cao; ++i) {
            // In khoảng trắng dẫn đầu
            for (int j = 0; j < i; ++j) {
                cout << " ";
            }
            // In ký tự
            for (int k = 0; k < chieu_cao - i; ++k) {
                cout << "*";
            }
            cout << "\n";
        }
        return 0;
    }
    

    Kết quả với chieu_cao = 5:

    *****
     ****
      ***
       **
        *

    Giải thích: Ở đây, số khoảng trắng tăng dần theo hàng, và số ký tự giảm dần.

Vẽ Tam Giác Cân (Hình Kim Tự Tháp)

Vẽ tam giác cân (hình kim tự tháp) đòi hỏi sự kết hợp của cả việc in khoảng trắng để căn giữa và in số lượng ký tự tăng dần theo một quy luật khác.

Với một tam giác cân có chiều cao h:

  • Hàng 0: h - 1 khoảng trắng, rồi 1 ký tự.
  • Hàng 1: h - 2 khoảng trắng, rồi 3 ký tự.
  • Hàng 2: h - 3 khoảng trắng, rồi 5 ký tự.
  • ...
  • Hàng i: h - 1 - i khoảng trắng, rồi 2 * i + 1 ký tự.
#include <iostream>

int main() {
    int chieu_cao;

    cout << "Nhap chieu cao tam giac can (kim tu thap): ";
    cin >> chieu_cao;

    cout << "Ve tam giac can:\n";

    // Vòng lặp hàng
    for (int i = 0; i < chieu_cao; ++i) {
        // Vòng lặp in khoảng trắng để căn giữa
        for (int j = 0; j < chieu_cao - 1 - i; ++j) {
            cout << " ";
        }

        // Vòng lặp in ký tự (số lượng tăng dần theo quy luật 1, 3, 5...)
        for (int k = 0; k < 2 * i + 1; ++k) {
            cout << "*";
        }

        cout << "\n"; // Xuống dòng sau mỗi hàng
    }

    return 0;
}

Giải thích:

  • Vòng lặp hàng i chạy từ 0 đến chieu_cao - 1.
  • Vòng lặp đầu tiên bên trong for (int j = 0; j < chieu_cao - 1 - i; ++j) in số lượng khoảng trắng cần thiết. Số khoảng trắng giảm dần khi i tăng.
  • Vòng lặp thứ hai bên trong for (int k = 0; k < 2 * i + 1; ++k) in số lượng ký tự. Công thức 2 * i + 1 tạo ra dãy số lẻ: khi i=0 là 1, khi i=1 là 3, khi i=2 là 5, v.v.
  • Sau khi in khoảng trắng và ký tự cho một hàng, cout << "\n"; xuống dòng.

Kết quả với chieu_cao = 5:

    *
   ***
  *****
 *******
*********

Lưu ý: Hình kim tự tháp này chỉ trông "cân" hoàn hảo khi tỷ lệ chiều rộng/chiều cao của ký tự trên console là 1:2 (tức là một ký tự rộng bằng nửa chiều cao). Trên nhiều console hiện đại, tỷ lệ này có thể khác, khiến hình trông hơi bè hoặc hơi cao. Đây là hạn chế của việc vẽ bằng ký tự text.

Thử Thách Thêm cho Bạn

Bây giờ bạn đã nắm vững cách sử dụng vòng lặp lồng nhau và điều kiện if/else để vẽ các hình cơ bản, hãy thử sức với những bài tập nâng cao hơn:

  1. Vẽ Hình Kim Cương (Diamond): Kết hợp hình kim tự tháp xuôi và ngược.
  2. Vẽ Hình Rỗng cho Tam Giác Vuông và Tam Giác Cân: Tương tự như cách vẽ hình chữ nhật rỗng, bạn chỉ in ký tự khi vị trí đó nằm trên đường viền của hình tam giác.
  3. Vẽ các hình khác: Hình tròn (xấp xỉ), hình thoi, hoặc kết hợp nhiều hình với nhau.
  4. Cho người dùng chọn hình muốn vẽ: Sử dụng cấu trúc switch hoặc if-else if để hỏi người dùng muốn vẽ hình gì (nhập 1 cho HCN đặc, 2 cho HCN rỗng, v.v.) rồi gọi đoạn code tương ứng.

Comments

There are no comments at the moment.