Bài 6.2: Kỹ thuật vẽ hình bằng vòng lặp trong C++

Chào mừng bạn quay trở lại với series học lập trình C++ cùng FullhouseDev!

Trong các bài trước, chúng ta đã làm quen với những khái niệm cơ bản như biến, kiểu dữ liệu và các cấu trúc điều khiển luồng như if/else. Hôm nay, chúng ta sẽ đào sâu vào một trong những công cụ mạnh mẽ nhất của lập trình: vòng lặp (loops). Và để thấy được sức mạnh "thực tế" của chúng, chúng ta sẽ sử dụng vòng lặp để... vẽ hình! Nghe có vẻ lạ lẫm khi vẽ hình bằng các kí tự trên màn hình console, nhưng đây là một kỹ thuật cổ điển, vô cùng hiệu quả để giúp bạn hiểu sâu sắc cách vòng lặp hoạt động, đặc biệt là vòng lặp lồng nhau (nested loops).

Bạn đã bao giờ tự hỏi làm thế nào những chương trình có thể tạo ra các mẫu hình hay thậm chí là đồ họa đơn giản chỉ bằng những dòng lệnh văn bản chưa? Câu trả lời nằm ở khả năng tự động hóa các tác vụ lặp đi lặp lại. Và đó chính là lúc vòng lặp tỏa sáng!

Tại sao lại dùng vòng lặp để vẽ?

Hãy tưởng tượng bạn muốn in ra 100 dấu * trên một dòng. Bạn có thể viết cout << "*"; 100 lần. Nhàm chánkhông hiệu quả, đúng không? Vòng lặp cho phép chúng ta chỉ định: "Lặp lại hành động này (in dấu *) đúng 100 lần".

Khi chúng ta nói về việc vẽ các hình dạng phức tạp hơn như hình chữ nhật hay tam giác bằng kí tự, chúng ta cần lặp lại việc in kí tự (và đôi khi cả khoảng trắng) theo một mẫu nhất định qua nhiều dòng. Đây chính là lúc vòng lặp lồng nhau trở thành người bạn đồng hành không thể thiếu. Một vòng lặp ngoài để xử lý các dòng (rows), và một hoặc nhiều vòng lặp bên trong để xử lý các cột (columns) trong mỗi dòng đó.

Hãy cùng đi vào các ví dụ cụ thể để thấy rõ điều này! Chúng ta sẽ sử dụng kí tự * làm "bút vẽ" chính.

1. Vẽ đường thẳng (Ngang và Dọc)

Đây là hình dạng đơn giản nhất, chỉ cần một vòng lặp đơn.

Ví dụ 1.1: Đường thẳng ngang

Để vẽ một đường thẳng ngang dài N kí tự, chúng ta chỉ cần lặp N lần hành động in một kí tự.

#include <iostream>

int main() {
    int length = 15; // Chiều dài của đường thẳng

    // Vòng lặp chạy từ 0 đến length - 1 (tổng cộng length lần)
    for (int i = 0; i < length; ++i) {
        cout << "*"; // In một dấu '*'
    }

    // Sau khi in xong tất cả '*', xuống dòng để các nội dung sau không bị dính
    cout << endl;

    return 0;
}

Giải thích: Vòng lặp for (int i = 0; i < length; ++i) sẽ thực hiện khối lệnh bên trong nó (cout << "*";) length lần. Biến i chỉ đơn giản là bộ đếm để đảm bảo vòng lặp chạy đúng số lần mong muốn. Sau khi vòng lặp kết thúc, cout << endl; đưa con trỏ xuống dòng tiếp theo.

Ví dụ 1.2: Đường thẳng dọc

Để vẽ một đường thẳng dọc cao N kí tự, chúng ta cần lặp N lần hành động in một kí tự xuống dòng ngay sau đó.

#include <iostream>

int main() {
    int height = 7; // Chiều cao của đường thẳng

    // Vòng lặp chạy từ 0 đến height - 1 (tổng cộng height lần)
    for (int i = 0; i < height; ++i) {
        // In một dấu '*' VÀ xuống dòng ngay lập tức
        cout << "*" << endl;
    }

    return 0;
}

Giải thích: Trong vòng lặp này, mỗi lần lặp, chúng ta không chỉ in * mà còn in endl. Điều này khiến mỗi dấu * được in trên một dòng riêng biệt, tạo thành đường thẳng dọc.

2. Vẽ hình chữ nhật/hình vuông (Sử dụng vòng lặp lồng nhau)

Đây là lúc vòng lặp lồng nhau phát huy tác dụng. Một hình chữ nhật có nhiều hàng và nhiều cột. Chúng ta sẽ dùng vòng lặp ngoài để duyệt qua các hàng, và vòng lặp trong để in các kí tự cho từng cột trong hàng hiện tại.

Ví dụ 2.1: Hình chữ nhật đặc

Để vẽ một hình chữ nhật đặc kích thước rows x cols, chúng ta cần:

  1. Vòng lặp ngoài chạy rows lần (đại diện cho mỗi hàng).
  2. Bên trong vòng lặp ngoài, chúng ta cần một vòng lặp nữa chạy cols lần (đại diện cho mỗi cột trong hàng đó).
  3. Sau khi vòng lặp trong (duyệt cột) hoàn thành cho một hàng, chúng ta xuống dòng để chuẩn bị in hàng tiếp theo.
#include <iostream>

int main() {
    int rows = 5; // Số hàng
    int cols = 10; // Số cột

    // Vòng lặp ngoài: Duyệt qua từng hàng (từ 0 đến rows-1)
    for (int i = 0; i < rows; ++i) {
        // Vòng lặp trong: Duyệt qua từng cột trong hàng hiện tại (từ 0 đến cols-1)
        for (int j = 0; j < cols; ++j) {
            cout << "*"; // In một dấu '*' tại vị trí cột hiện tại
        }
        // Kết thúc vòng lặp trong (đã in xong tất cả các cột cho hàng i), xuống dòng
        cout << endl;
    }

    return 0;
}

Giải thích: Vòng lặp ngoài với biến i kiểm soát số lượng hàng. Với mỗi giá trị của i (tức là mỗi hàng), vòng lặp trong với biến j sẽ chạy toàn bộ từ đầu đến cuối (cols lần). Vòng lặp trong này in ra các kí tự cho hàng đó. Sau khi vòng lặp trong kết thúc, cout << endl; di chuyển con trỏ xuống đầu hàng tiếp theo, và vòng lặp ngoài tiếp tục với giá trị i kế tiếp. Quá trình này lặp lại cho đến khi tất cả các hàng được in.

3. Vẽ hình tam giác vuông (Sử dụng vòng lặp lồng nhau với điều kiện thay đổi)

Hình tam giác thú vị hơn vì số lượng kí tự trên mỗi dòng không giống nhau. Điều này có nghĩa là điều kiện của vòng lặp trong (hoặc những gì chúng ta in ra trong vòng lặp trong) cần phải phụ thuộc vào vòng lặp ngoài (số thứ tự của hàng).

Ví dụ 3.1: Tam giác vuông cân (góc vuông dưới bên trái)

Trong hình tam giác này, hàng thứ 0 có 1 kí tự, hàng thứ 1 có 2 kí tự, hàng thứ 2 có 3 kí tự, v.v. Hàng thứ i sẽ có i+1 kí tự.

#include <iostream>

int main() {
    int height = 6; // Chiều cao của tam giác

    // Vòng lặp ngoài: Duyệt qua từng hàng (từ 0 đến height-1)
    for (int i = 0; i < height; ++i) {
        // Vòng lặp trong: Duyệt qua các cột. Số lượng cột bằng i + 1.
        // Chú ý điều kiện j <= i hoặc j < i + 1
        for (int j = 0; j <= i; ++j) {
            cout << "*"; // In một dấu '*'
        }
        // Kết thúc hàng, xuống dòng
        cout << endl;
    }

    return 0;
}

Giải thích: Vòng lặp ngoài (với i) vẫn duyệt qua các hàng. Điểm khác biệt là vòng lặp trong (với j). Điều kiện j <= i làm cho số lần lặp của vòng lặp trong phụ thuộc vào giá trị hiện tại của i. Khi i = 0, j chạy từ 0 đến 0 (1 lần); khi i = 1, j chạy từ 0 đến 1 (2 lần); khi i = height - 1, j chạy từ 0 đến height - 1 (height lần). Điều này tạo ra số lượng kí tự tăng dần trên mỗi dòng, tạo nên hình tam giác.

Ví dụ 3.2: Tam giác vuông cân (góc vuông trên bên trái)

Trong hình tam giác này, hàng thứ 0 có height kí tự, hàng thứ 1 có height - 1 kí tự, v.v. Hàng thứ i sẽ có height - i kí tự.

#include <iostream>

int main() {
    int height = 6; // Chiều cao của tam giác

    // Vòng lặp ngoài: Duyệt qua từng hàng (từ 0 đến height-1)
    for (int i = 0; i < height; ++i) {
        // Vòng lặp trong: Duyệt qua các cột. Số lượng cột bằng height - i.
        // Chú ý điều kiện j < height - i
        for (int j = 0; j < height - i; ++j) {
            cout << "*"; // In một dấu '*'
        }
        // Kết thúc hàng, xuống dòng
        cout << endl;
    }

    return 0;
}

Giải thích: Tương tự như tam giác trước, vòng lặp ngoài là cho hàng. Vòng lặp trong có điều kiện j < height - i. Khi i = 0, j chạy từ 0 đến height - 1 (height lần); khi i = 1, j chạy từ 0 đến height - 2 (height - 1 lần); và cứ thế giảm dần. Điều này tạo ra số lượng kí tự giảm dần trên mỗi dòng.

4. Vẽ hình rỗng (Sử dụng điều kiện if bên trong vòng lặp lồng nhau)

Để vẽ các hình rỗng (chỉ có đường viền), chúng ta vẫn dùng vòng lặp lồng nhau để duyệt qua tất cả các vị trí (hàng x cột). Tuy nhiên, bên trong vòng lặp trong, chúng ta sẽ sử dụng một câu lệnh if để quyết định có in kí tự * hay một khoảng trắng tại vị trí đó.

Điều kiện để in * là vị trí đó phải nằm trên đường viền của hình. Đối với hình chữ nhật, điều này có nghĩa là:

  • Nó là hàng đầu tiên (i == 0).
  • Nó là hàng cuối cùng (i == rows - 1).
  • Nó là cột đầu tiên (j == 0).
  • Nó là cột cuối cùng (j == cols - 1).

Chúng ta sẽ kết hợp các điều kiện này bằng toán tử || (HOẶC).

Ví dụ 4.1: Hình chữ nhật rỗng
#include <iostream>

int main() {
    int rows = 6; // Số hàng
    int cols = 12; // Số cột

    // Vòng lặp ngoài: Duyệt qua từng hàng
    for (int i = 0; i < rows; ++i) {
        // Vòng lặp trong: Duyệt qua từng cột trong hàng hiện tại
        for (int j = 0; j < cols; ++j) {
            // Kiểm tra xem vị trí (i, j) có nằm trên đường viền hay không
            if (i == 0 ||       // Hàng đầu tiên
                i == rows - 1 || // Hàng cuối cùng
                j == 0 ||       // Cột đầu tiên
                j == cols - 1) { // Cột cuối cùng
                cout << "*"; // Nếu là đường viền, in '*'
            } else {
                cout << " "; // Nếu không phải đường viền, in khoảng trắng ' '
            }
        }
        // Kết thúc hàng, xuống dòng
        cout << endl;
    }

    return 0;
}

Giải thích: Cấu trúc vòng lặp lồng nhau vẫn như cũ. Điều quan trọng là khối if/else bên trong. Tại mỗi vị trí (i, j), chúng ta kiểm tra xem nó có thỏa mãn bất kỳ điều kiện nào của đường viền hay không. Nếu CÓ, in *. Nếu KHÔNG (nằm ở bên trong), in một khoảng trắng. Việc in khoảng trắng đúng vị trí là quan trọng để duy trì hình dạng của hình chữ nhật.

5. Vẽ hình tam giác cân (Kim tự tháp - Sử dụng nhiều vòng lặp trong)

Để vẽ các hình phức tạp hơn như kim tự tháp (tam giác cân), chúng ta cần kiểm soát cả việc in khoảng trắng ở đầu dòng để căn chỉnh tâm, và in kí tự * theo số lượng tăng dần. Điều này thường đòi hỏi nhiều hơn một vòng lặp bên trong vòng lặp hàng.

Ví dụ 5.1: Kim tự tháp đơn giản

Kim tự tháp có số lượng kí tự tăng theo quy luật lẻ (1, 3, 5, 7, ...). Hàng thứ i (bắt đầu từ 0) sẽ có 2 * i + 1 kí tự *. Số lượng khoảng trắng ở đầu mỗi dòng sẽ giảm dần. Hàng đầu tiên cần nhiều khoảng trắng nhất để căn giữa.

#include <iostream>

int main() {
    int height = 5; // Chiều cao của kim tự tháp

    // Vòng lặp ngoài: Duyệt qua từng hàng (từ 0 đến height-1)
    for (int i = 0; i < height; ++i) {
        // Vòng lặp 1 (trong): In khoảng trắng ở đầu dòng để căn chỉnh
        // Số lượng khoảng trắng giảm dần: height - 1 - i
        for (int j = 0; j < height - 1 - i; ++j) {
            cout << " ";
        }

        // Vòng lặp 2 (trong): In các dấu '*'
        // Số lượng dấu '*' tăng dần: 2 * i + 1
        for (int k = 0; k < 2 * i + 1; ++k) {
            cout << "*";
        }

        // Kết thúc hàng, xuống dòng
        cout << endl;
    }

    return 0;
}

Giải thích: Vòng lặp ngoài (với i) xác định đang xử lý hàng nào. Bên trong, chúng ta có hai vòng lặp for riêng biệt:

  1. Vòng lặp với j: Có điều kiện j < height - 1 - i. Điều này đảm bảo rằng ở hàng đầu tiên (i = 0), nó in height - 1 khoảng trắng. Hàng thứ hai (i = 1), nó in height - 2 khoảng trắng, và cứ thế giảm dần.
  2. Vòng lặp với k: Có điều kiện k < 2 * i + 1. Điều này đảm bảo ở hàng đầu tiên (i = 0), nó in 2 * 0 + 1 = 1 dấu *. Hàng thứ hai (i = 1), nó in 2 * 1 + 1 = 3 dấu *, v.v., tạo ra quy luật số lẻ. Thứ tự của hai vòng lặp trong là quan trọng: in khoảng trắng trước, sau đó mới in kí tự. Cuối cùng, endl để xuống dòng cho hàng tiếp theo.
Tóm lại

Qua các ví dụ trên, bạn đã thấy sức mạnh và sự linh hoạt của vòng lặp trong C++, đặc biệt là vòng lặp lồng nhau. Bằng cách kết hợp vòng lặp với các cấu trúc điều khiển khác như if/else và thay đổi điều kiện lặp dựa vào biến đếm của vòng lặp ngoài, chúng ta có thể tạo ra vô số các mẫu hình thú vị chỉ bằng các kí tự đơn giản.

Comments

There are no comments at the moment.