[0]zero-index clytie0x/zero-index

7.2 แถว คอลัมน์ และเมทริกซ์

พอเก็บตารางได้แล้ว งานที่เจอบ่อยที่สุดคือรวมตัวเลขตามแนวต่างๆ คะแนนรวมของนักเรียนแต่ละคนคือผลรวมตามแถว คะแนนรวมของแต่ละวิชาคือผลรวมตามคอลัมน์ หน้านี้ฝึกเดินตารางตามแถว ตามคอลัมน์ และตามเส้นทแยง แล้วต่อด้วยงานของ เมทริกซ์ (matrix) ซึ่งคือชื่อในวิชาคณิตศาสตร์ของตารางตัวเลข ได้แก่ สลับแถวเป็นคอลัมน์ บวก และคูณ

ผลรวมแต่ละแถวและแต่ละคอลัมน์

ใช้ตารางคะแนนจากหน้า 7.1 แต่คราวนี้รับจากข้อมูลเข้า บรรทัดแรกคือจำนวนนักเรียนกับจำนวนวิชา

#include <stdio.h>

int score[100][100];

int main()
{
    int students, subjects;
    scanf("%d %d", &students, &subjects);
    for (int r = 0; r < students; r++) {
        for (int c = 0; c < subjects; c++) {
            scanf("%d", &score[r][c]);
        }
    }
    for (int r = 0; r < students; r++) {
        int total = 0; // (1)!
        for (int c = 0; c < subjects; c++) {
            total += score[r][c];
        }
        printf("student %d: %d\n", r + 1, total);
    }
    for (int c = 0; c < subjects; c++) { // (2)!
        int total = 0;
        for (int r = 0; r < students; r++) {
            total += score[r][c];
        }
        printf("subject %d: %d\n", c + 1, total);
    }
    return 0;
}
  1. ตั้ง total เป็น 0 ใหม่ทุกครั้งที่เริ่มแถวใหม่ ถ้าตั้งไว้นอกลูปนอก ผลรวมจะทบต่อจากแถวก่อน ดูในส่วนลองทำให้พัง
  2. ผลรวมตามคอลัมน์ สลับให้ลูปนอกเดินคอลัมน์ ลูปในเดินแถว ส่วน score[r][c] ยังเขียนแบบเดิม แถวมาก่อนคอลัมน์เสมอ
4 3
78 65 90
82 91 70
55 88 73
95 60 84
student 1: 233
student 2: 243
student 3: 216
student 4: 239
subject 1: 310
subject 2: 304
subject 3: 317

สองส่วนนี้ต่างกันแค่ว่าลูปไหนอยู่นอก ลูปนอกคือสิ่งที่เราอยากได้คำตอบทีละตัว อยากได้คำตอบต่อนักเรียนหนึ่งคน ลูปนอกก็คือแถว อยากได้คำตอบต่อวิชา ลูปนอกก็คือคอลัมน์

เส้นทแยง

ตารางที่จำนวนแถวเท่ากับจำนวนคอลัมน์เรียกว่า ตารางจัตุรัส (square matrix) ขนาด N × N มีเส้นทแยงสองเส้น เส้นทแยงหลัก ลากจากมุมบนซ้ายลงไปมุมล่างขวา และ เส้นทแยงรอง ลากจากมุมบนขวาลงไปมุมล่างซ้าย

0 1 2 3 0 0,0 0,3 1 1,1 1,2 2 2,1 2,2 3 3,0 3,3 แถว = คอลัมน์ a[i][i] แถว + คอลัมน์ = N - 1 a[i][N - 1 - i]

ดูเลขแถวกับเลขคอลัมน์ของแต่ละช่องแล้วจะเห็นกฎ

  • เส้นทแยงหลัก เลขแถวเท่ากับเลขคอลัมน์ ช่องในแถว i คือ a[i][i]
  • เส้นทแยงรอง เลขแถวบวกเลขคอลัมน์ได้ N - 1 เสมอ ช่องในแถว i จึงอยู่คอลัมน์ N - 1 - i คือ a[i][N - 1 - i]

แต่ละแถวมีช่องบนเส้นทแยงแค่ช่องเดียว จึงใช้ลูปชั้นเดียวก็พอ ไม่ต้องไล่ทั้งตารางแล้วเช็กว่า r == c

#include <stdio.h>

int a[100][100];

int main()
{
    int n;
    scanf("%d", &n);
    for (int r = 0; r < n; r++) {
        for (int c = 0; c < n; c++) {
            scanf("%d", &a[r][c]);
        }
    }
    int main_sum = 0, anti_sum = 0;
    for (int i = 0; i < n; i++) {
        main_sum += a[i][i];
        anti_sum += a[i][n - 1 - i];
    }
    printf("main diagonal = %d\n", main_sum);
    printf("anti diagonal = %d\n", anti_sum);
    return 0;
}
4
3 8 1 6
7 2 9 4
5 0 6 1
2 7 3 8
main diagonal = 19
anti diagonal = 17

3 + 2 + 6 + 8 = 19 และ 6 + 9 + 0 + 2 = 17 ถ้า N เป็นเลขคี่ ช่องตรงกลางพอดีจะอยู่บนเส้นทแยงทั้งสองเส้น โจทย์ที่ให้รวมสองเส้นเข้าด้วยกันจึงต้องระวังนับช่องนี้ซ้ำ

สลับแถวเป็นคอลัมน์ (transpose)

การสลับให้แถวที่ 1 กลายเป็นคอลัมน์ที่ 1 แถวที่ 2 กลายเป็นคอลัมน์ที่ 2 ไปเรื่อยๆ เรียกว่า transpose ตาราง R × C จะกลายเป็น C × R เช่น ตารางคะแนนที่แถวเป็นนักเรียน พอ transpose แล้วแถวจะเป็นวิชาแทน

a 2 x 3 1 2 3 4 5 6 t 3 x 2 1 4 2 5 3 6

ช่อง a[r][c] ย้ายไปอยู่ที่ t[c][r] แค่สลับเลขสองตัว

#include <stdio.h>

int a[100][100], t[100][100];

int main()
{
    int rows, cols;
    scanf("%d %d", &rows, &cols);
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            scanf("%d", &a[r][c]);
            t[c][r] = a[r][c];
        }
    }
    for (int r = 0; r < cols; r++) {
        for (int c = 0; c < rows; c++) {
            printf("%d", t[r][c]);
            if (c < rows - 1) {
                printf(" ");
            }
        }
        printf("\n");
    }
    return 0;
}
2 3
1 2 3
4 5 6
1 4
2 5
3 6

ตาราง t มี cols แถว และ rows คอลัมน์ ลูปพิมพ์จึงสลับขอบเขตกับลูปอ่าน ถ้าแค่ต้องการพิมพ์ ไม่ได้เอา t ไปใช้ต่อ ไม่ต้องมี t ก็ได้ ให้ลูปนอกเดินคอลัมน์ของ a ลูปในเดินแถว แล้วพิมพ์ a[r][c] แบบเดียวกับผลรวมตามคอลัมน์

บวกเมทริกซ์

ตารางสองตารางขนาดเท่ากัน บวกกันได้โดยบวกช่องที่ตำแหน่งตรงกัน เช่น คะแนนกลางภาคกับคะแนนปลายภาคของนักเรียนคนเดียวกันวิชาเดียวกัน

#include <stdio.h>

int midterm[100][100], final_exam[100][100];

int main()
{
    int rows, cols;
    scanf("%d %d", &rows, &cols);
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            scanf("%d", &midterm[r][c]);
        }
    }
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            scanf("%d", &final_exam[r][c]);
        }
    }
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            printf("%d", midterm[r][c] + final_exam[r][c]);
            if (c < cols - 1) {
                printf(" ");
            }
        }
        printf("\n");
    }
    return 0;
}
2 3
30 25 40
35 20 28
45 38 41
50 29 33
75 63 81
85 49 61

ลบเมทริกซ์ก็ทำแบบเดียวกัน แค่เปลี่ยนบวกเป็นลบ

คูณเมทริกซ์

การคูณเมทริกซ์ไม่ได้คูณช่องที่ตรงกันแบบการบวก ดูจากเรื่องนี้ก่อน ลูกค้า 2 คนจะซื้อของ 3 อย่าง ตาราง qty บอกว่าลูกค้าแต่ละคนซื้ออะไรกี่ชิ้น ส่วนตาราง price บอกราคาของแต่ละอย่างที่ร้าน 2 ร้าน อยากรู้ว่าลูกค้าแต่ละคนจะจ่ายเท่าไรถ้าซื้อที่แต่ละร้าน

qty 2 x 3 2 1 3 0 4 1 price 3 x 2 10 12 25 20 8 9 69 71 108 89 0x10 + 4x25 + 1x8 = 108 total 2 x 2

ลูกค้าคนที่มีเลข 1 ไปร้านที่มีเลข 0 จ่าย 0 × 10 + 4 × 25 + 1 × 8 = 108 คือเอา แถว 1 ของ qty มาคูณกับ คอลัมน์ 0 ของ price ทีละคู่ แล้วบวกกันทั้งหมด เขียนเป็นสูตรได้ว่า

total[i][j] = qty[i][0] * price[0][j] + qty[i][1] * price[1][j] + ... + qty[i][M-1] * price[M-1][j]

คูณได้เมื่อ จำนวนคอลัมน์ของตารางซ้ายเท่ากับจำนวนแถวของตารางขวา ในที่นี้คือจำนวนชนิดของสินค้า M = 3 ตาราง N × M คูณกับ M × P ได้ N × P โปรแกรมต้องมีลูปสามชั้น สองชั้นนอกเลือกช่อง [i][j] ของคำตอบ ชั้นในสุดเดิน k ไปตามแถวของตารางซ้ายพร้อมกับคอลัมน์ของตารางขวา

#include <stdio.h>

int qty[100][100], price[100][100];
long long total[100][100];

int main()
{
    int customers, items, shops;
    scanf("%d %d %d", &customers, &items, &shops);
    for (int i = 0; i < customers; i++) {
        for (int k = 0; k < items; k++) {
            scanf("%d", &qty[i][k]);
        }
    }
    for (int k = 0; k < items; k++) {
        for (int j = 0; j < shops; j++) {
            scanf("%d", &price[k][j]);
        }
    }
    for (int i = 0; i < customers; i++) {
        for (int j = 0; j < shops; j++) {
            total[i][j] = 0;
            for (int k = 0; k < items; k++) {
                total[i][j] += (long long)qty[i][k] * price[k][j]; // (1)!
            }
        }
    }
    for (int i = 0; i < customers; i++) {
        for (int j = 0; j < shops; j++) {
            printf("%lld", total[i][j]);
            if (j < shops - 1) {
                printf(" ");
            }
        }
        printf("\n");
    }
    return 0;
}
  1. ถ้าโจทย์ให้ค่าแต่ละช่องถึง 104 ผลคูณหนึ่งคู่ถึง 108 พอบวกกัน 100 คู่ได้ถึง 1010 เกิน int จึงเก็บคำตอบใน long long และ cast ก่อนคูณ ตามกับดัก int คูณ int ในหน้า 1.3
2 3 2
2 1 3
0 4 1
10 12
25 20
8 9
69 71
108 89

ลูกค้าคนแรกควรซื้อร้านแรก จ่าย 69 ส่วนคนที่สองควรซื้อร้านที่สอง จ่าย 89 ตัวนับในโปรแกรมนี้ใช้ i j k แทน r กับ c เพราะมีสามตาราง k เป็นคอลัมน์ของ qty แต่เป็นแถวของ price ตั้งชื่อตามตารางใดตารางหนึ่งจะชวนสับสน

ลูปสามชั้นทำงาน N × M × P รอบ ถ้าทั้งสามค่าเป็น 100 คือหนึ่งล้านรอบ เร็วมาก แต่ถ้าเป็น 1000 จะเป็นพันล้านรอบ ซึ่งเริ่มช้า เรื่องประมาณเวลาจากขอบเขตโจทย์จะเรียนในบทที่ 18

ลองทำให้พัง

แบบที่ 1 ตั้งผลรวมเป็นศูนย์ครั้งเดียว

ย้าย int total = 0; ออกไปไว้ก่อนลูปนอก ตัดส่วนผลรวมคอลัมน์ออกให้สั้นลง

#include <stdio.h>

int score[100][100];

int main()
{
    int students, subjects;
    scanf("%d %d", &students, &subjects);
    for (int r = 0; r < students; r++) {
        for (int c = 0; c < subjects; c++) {
            scanf("%d", &score[r][c]);
        }
    }
    int total = 0;
    for (int r = 0; r < students; r++) {
        for (int c = 0; c < subjects; c++) {
            total += score[r][c];
        }
        printf("student %d: %d\n", r + 1, total);
    }
    return 0;
}
4 3
78 65 90
82 91 70
55 88 73
95 60 84
student 1: 233
student 2: 476
student 3: 692
student 4: 931

คนแรกถูก คนต่อมาผิดหมด เพราะคะแนนของคนก่อนหน้าทบเข้ามาด้วย 476 คือ 233 + 243 อาการ "ตัวแรกถูก ตัวหลังโตขึ้นเรื่อยๆ" แบบนี้ ให้สงสัยไว้ก่อนเลยว่าลืมตั้งค่าเริ่มต้นใหม่ในลูปนอก

แบบที่ 2 transpose ในตารางเดิมด้วยการสลับทุกช่อง

ตารางจัตุรัส transpose ในตารางเดิมได้ ไม่ต้องมีตารางที่สอง โดยสลับค่า a[r][c] กับ a[c][r] หลายคนเขียนลูปเดินทั้งตารางแบบนี้

#include <stdio.h>

int main()
{
    int a[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++) {
            int temp = a[r][c];
            a[r][c] = a[c][r];
            a[c][r] = temp;
        }
    }
    for (int r = 0; r < 3; r++) {
        printf("%d %d %d\n", a[r][0], a[r][1], a[r][2]);
    }
    return 0;
}
1 2 3
4 5 6
7 8 9

ได้ตารางเดิมเป๊ะ เพราะทุกคู่ถูกสลับสองครั้ง ตอน r = 0, c = 1 สลับ 2 กับ 4 ไปแล้ว พอถึง r = 1, c = 0 ก็สลับคู่เดิมกลับคืน แก้โดยสลับเฉพาะช่องที่อยู่เหนือเส้นทแยงหลัก ให้ลูปในเริ่มที่ c = r + 1 แต่ละคู่จะถูกสลับครั้งเดียว

for (int r = 0; r < 3; r++) {
    for (int c = r + 1; c < 3; c++) {

แก้แล้วจะได้ 1 4 7 2 5 8 3 6 9 วิธีนี้ใช้ได้กับตารางจัตุรัสเท่านั้น ตาราง R × C ที่ R ไม่เท่ากับ C เปลี่ยนรูปร่างหลัง transpose จึงต้องใช้ตารางที่สองแบบในหัวข้อ transpose

สรุป

  • ผลรวมตามแถว ลูปนอกเดินแถว ผลรวมตามคอลัมน์ ลูปนอกเดินคอลัมน์ ตั้งผลรวมเป็น 0 ใหม่ทุกรอบของลูปนอก
  • เส้นทแยงหลักคือ a[i][i] เส้นทแยงรองคือ a[i][N - 1 - i] ใช้ลูปชั้นเดียว
  • transpose ย้าย a[r][c] ไป t[c][r] ตาราง R × C กลายเป็น C × R
  • บวกเมทริกซ์คือบวกช่องที่ตรงกัน ต้องขนาดเท่ากัน
  • คูณเมทริกซ์ N × M กับ M × P ได้ N × P แต่ละช่องคือแถวซ้ายคูณคอลัมน์ขวาทีละคู่แล้วบวก ใช้ลูปสามชั้น และระวังผลรวมล้น int