Vui học - Bài 1 - Vẽ vời bằng sao (1)

2021, Dec 05    

Giới thiệu series “Vui học”

Bài gốc mình đã đăng đầu tiên tại DNH, nay mình đăng lại lên blog cá nhân của mình.

Series Vui học được xây dựng từ những ý tưởng của tác giả mỗi khi nhìn vào một topic gì đó có vẻ vui vui trên DNH :grin:


Dạo qua DNH, không hiếm topic có dạng vẽ hình bằng * (sao) như thế này.

https://daynhauhoc.com/t/hoi-ve-thuat-toan-ve-hinh-trai-tim-bang-dau-trong-java/36204

https://daynhauhoc.com/t/xin-cach-ve-hinh-trong-c/71291

https://daynhauhoc.com/t/ve-hinh-trong-c/592/

https://daynhauhoc.com/t/viet-chuong-trinh-ve-mot-tam-giac-can-bang-dau/30492

https://daynhauhoc.com/t/xin-giup-do-su-dung-ngon-ngu-java-in-ra-man-hinh-1-tam-giac-nhu-trong-hinh/39260/

Ngoài ra còn có các kiểu vẽ hình thoi, hình dấu +, các chữ số 0-9 như trên bảng điện tử,…

Để khởi động, ta sẽ làm 1 bài đơn giản trước.

Đề

Vẽ

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

và phải dùng for.

Cách vẽ

Niềm vui toé loe ra từ đây ^^

Ta đánh số các cách theo thứ tự tăng dần của mức độ học để thấy mức độ hay và kì cục của mỗi cách.

Cách không có học

Làm con ngoan trò giỏi, thầy bảo gì làm nấy.

printf("  * * * * * *\n");
printf(" * * * * * *\n");
printf("* * * * * *\n");

Ơ kìa, thiếu for mất rồi. Nâng cấp lên một chút nào:

int i;
for (i = 0; i < 1; i++) {
    printf("  * * * * * *\n");
    printf(" * * * * * *\n");
    printf("* * * * * *\n");
}

Một số kiểu for khác vui hơn:

for (;;) { 
    /* lắp 3 dòng printf vào */
    break;
}
#include <climits>

for (int i = 0; i < INT_MAX; i++); // nhìn cho thật kĩ vào ^^
// 3 cái printf vào đây
for (int i = 0; !i; i ^= 1) // bitwise cho máu

Cách ít học

Khi đã học hơn một chút, ta biết 3 cái dòng này cứ na ná giống nhau, lắp for vào cho khoẻ. Nhưng ta không biết lắp thế nào cho vừa.

Ta nhanh mắt thấy

  • Dòng 1 có 2 dấu cách, rồi có * * * * * *.
  • Dòng 2 có 1 dấu cách, rồi có * * * * * *.
  • Dòng 3 có 0 dấu cách, rồi có * * * * * *.

Ta code thật nhanh tay:

int i, j;
for (i = 2; i >= 0; i--) { // số dấu cách đầu dòng
    for (j = 0; j < i; j++) printf(" "); // in ra dấu cách đầu dòng
    printf("* * * * * *\n");
}

Có vẻ ổn rồi chứ nhỉ?

Cách có học

Nếu như một ngày đẹp trời nào đó, thầy lỡ tay gõ nhầm, thay vì bắt bạn in ra 1 dòng 6 sao thì 1 dòng phải in ra hàng trăm sao, hàng nghìn sao,… thì ta gõ tay từng sao trong printf làm sao đây, gõ đến hết giờ cũng không xong :sob:

Bạn khóc đến sưng húp cả 2 mắt :eyes: mà chả thấy Bụt nào hiện lên hỏi cả :innocent: Rồi trong lúc chùi nước mắt, bạn lại thấy sao cái dãy sao này có vẻ như lặp lại như 3 cái dòng vừa nãy. Bạn lao ngay vào code:

int i, j;
for (i = 2; i >= 0; i--) { // vẫn là số dấu cách đầu dòng
    for (j = 0; j < i; j++)
        printf(" "); // vẫn in ra dấu cách đầu dòng

    // phép màu bắt đầu từ đây
    for (j = 0; j < 6; j++)
        printf("* ");

    printf("\n"); // bình tĩnh kết thúc cái dòng sao của nợ này
}

Rồi bạn vô tình nhận ra trong lúc in sao có lỡ thừa ra dấu cách ở cuối dòng, bạn đành tặc lưỡi kệ. Sao bảo có học mà chả thấy hay ho hơn ít học gì sất :unamused:

Cách bác học

Một cách super có học, gọi là bác học. Nó super có học hơn vì nó vứt đi được cái dấu cách thừa thãi ở cuối đáng ghét kia :x

Dấu cách đứng bên phải dấu sao mà thấy thừa dấu cách thì cho nó đứng bên trái cho đỡ thừa.

int i, j;
for (i = 2; i >= 0; i--) {
    for (j = 0; j < i; j++)
        printf(" ");

    for (j = 0; j < 6; j++)
        printf(" *");

    printf("\n");
}

Output:

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

Vấn đề xảy ra, tự dưng lòi đâu cái dấu cách ở đầu mỗi dòng, trông đáng ghét quá :unamused: Vậy vứt đi bằng cách nào?

Có sửa chỗ vòng for in dấu cách đầu dòng cũng không ổn. Vẫn thừa dấu cách ở dòng cuối :’(

  * * * * * *
 * * * * * *
 * * * * * * // cái dòng đáng ghét -_- 

Đến đây ta áp dụng chút kiến thức toán học cùi ta còn rớt lại.

Ném dòng sao kia lên trục toạ độ, mỗi kí tự là 1 điểm, có đánh số đàng hoàng.

| `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` |
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
|  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  | 10  |

Coi dấu * đầu tiên là gốc toạ độ luôn cho dễ.

Còn những dấu cách đằng trước, ta coi nó đặt trên các điểm âm. Ta thích thì in nó ra, không thích thì thôi.

Lắp code vào:

int i, j;
for (i = -2; i <= 0; i++) { // i là điểm đầu
    for (j = i; j < 10; j++) {
        if (j >= 0 && j % 2 == 0)
            printf("*");
        else
            printf(" ");
    }
    printf("\n");
}

Lạ nhỉ, sao code này đúng?

Ta thử vẽ lại trên hệ trục toạ độ như hình trên:

  • Dòng 1:
| ` ` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` |
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| -2  | -1  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  | 10  |
  • Dòng 2:
| ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` |
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| -1  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  | 10  |
  • Dòng 3:
| `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` | ` ` | `*` |
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
|  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  | 10  |

Vì ta biết dòng cuối có dấu * ở đầu nên ta coi dấu * đầu tiên là gốc luôn. Trên mỗi dòng, vị trí các kí tự đều phụ thuộc vào dấu * đầu tiên của mỗi dòng hết.

Khó hiểu quá, đúng không? Code bác học mà :smile:

Tổng quát hoá

Thay vì chỉ có 3 dòng, mỗi dòng 6 sao thì ta upgrade lên m dòng, mỗi dòng n sao. Cách làm vẫn tương tự. Không khuyến khích 2 cách đầu tiên, quá khổ sở :sweat:

Kết luận

Mời mọi người vào xem comment trong đề gốc, có lẽ sẽ có cái cao học hơn cách mình nói nữa đó ;)