Array dua dimensi (matriks) adalah perluasan array satu dimensi untuk merepresentasikan data berbentuk tabel. Di C++, matriks disimpan sebagai blok memori tunggal dengan urutan row-major — baris demi baris. Pada Chapter ini kita akan membedah bagaimana layout memori memengaruhi kinerja traversal, mengapa urutan loop dapat berdampak 5–10× pada kecepatan, dan kapan std::vector<std::vector<T>> cocok dipakai. Pemahaman layout 2D menjadi fondasi untuk topik lanjutan seperti transpose matriks dan prefix sum 2D.
Deklarasi dan Akses #
C++ menyediakan beberapa cara mendeklarasikan matriks:
// 1. C-style array 2D
int m1[3][4];
// 2. std::array 2D
std::array<std::array<int, 4>, 3> m2;
// 3. std::vector 2D (dinamis)
std::vector<std::vector<int>> m3(3, std::vector<int>(4, 0));
// 4. std::vector 1D dengan indexing manual (flat)
std::vector<int> m4(3 * 4, 0); // akses: m4[i * 4 + j]
Pilihan terakhir sering dipakai di kompetitif programming karena lebih hemat memori dan lebih cepat — tidak ada pointer tambahan per baris.
Row-Major Order #
Mekanisme: Baris demi Baris #
C-style array 2D int m[R][C] disimpan sebagai blok kontigu berisi R * C elemen. Urutan: elemen baris 0 (kolom 0, 1, …, C-1), lalu baris 1, lalu baris 2, dan seterusnya. Alamat elemen m[i][j] dihitung dengan:
addr(m[i][j]) = base + (i * C + j) * sizeof(T)Konsekuensi penting: traversal sepanjang kolom (yaitu m[0][0], m[1][0], m[2][0], …) melompat C * sizeof(T) byte antar akses — tidak cache-friendly.
Kode: Verifikasi Layout dengan Offset #
#include <iostream>
#include <cstdint>
int main() {
int m[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
std::uintptr_t base = reinterpret_cast<std::uintptr_t>(&m[0][0]);
std::cout << "Row-major offset (byte):\n";
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
std::uintptr_t addr = reinterpret_cast<std::uintptr_t>(&m[i][j]);
std::cout << ' ' << (addr - base);
}
std::cout << '\n';
}
return 0;
}Output yang Dihasilkan:
Row-major offset (byte):
0 4 8 12
16 20 24 28
32 36 40 44- Baris
int m[3][4] = {{1,2,3,4}, ...};: matriks 3 baris × 4 kolom. Total elemen3 * 4 = 12. Setiap baris dideklarasikan dalam kurung kurawal sendiri. - Baris
reinterpret_cast<std::uintptr_t>(&m[i][j]): mengonversi alamat elemen ke integer untuk dihitung offsetnya. - Pola offset: baris ke-0 menempati offset 0..12, baris ke-1 offset 16..28, baris ke-2 offset 32..44. Setiap baris menempati
4 * 4 = 16byte. Inilah bukti langsung layout row-major.
Traversal: Baris vs Kolom #
Mekanisme: Cache-Friendly vs Cache-Hostile #
Bila kita traverse matriks baris demi baris (m[i][0], m[i][1], ..., m[i][C-1]), akses berturut-turut berada pada alamat yang berdekatan — satu cache line (64 byte = 16 int) dapat melayani 16 akses beruntun. Sebaliknya, traversal kolom demi kolom (m[0][j], m[1][j], ...) melompat C * 4 byte setiap akses — sering memicu cache miss.
Kode: Benchmark Row vs Column Traversal #
#include <iostream>
#include <vector>
#include <chrono>
int main() {
constexpr int N = 2000;
std::vector<std::vector<int>> m(N, std::vector<int>(N, 1));
// Row-major traversal
volatile long long sum1 = 0;
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
sum1 += m[i][j];
auto t1 = std::chrono::high_resolution_clock::now();
// Column-major traversal
volatile long long sum2 = 0;
auto t2 = std::chrono::high_resolution_clock::now();
for (int j = 0; j < N; ++j)
for (int i = 0; i < N; ++i)
sum2 += m[i][j];
auto t3 = std::chrono::high_resolution_clock::now();
auto ms_row = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
auto ms_col = std::chrono::duration_cast<std::chrono::milliseconds>(t3 - t2).count();
std::cout << "row-major: sum=" << sum1 << " waktu=" << ms_row << " ms\n";
std::cout << "col-major: sum=" << sum2 << " waktu=" << ms_col << " ms\n";
return 0;
}Output yang Dihasilkan (waktu bervariasi):
row-major: sum=4000000 waktu=4 ms
col-major: sum=4000000 waktu=20 ms- Baris
for (int i...) for (int j...) sum1 += m[i][j];: loop luar untuk baris, loop dalam untuk kolom. Aksesm[i][j]berurutan dalam memori → cache-friendly. - Baris
for (int j...) for (int i...) sum2 += m[i][j];: loop luar untuk kolom, loop dalam untuk baris. Aksesm[i][j]melompatN * 4byte setiap iterasi → cache miss sering. - Perbedaan waktu: row-major ~5× lebih cepat meskipun jumlah iterasi identik. Inilah alasan kuat untuk selalu memilih urutan loop yang sesuai dengan layout memori.
Flat 1D Vector sebagai Matriks #
Mekanisme: Menghindari Indirection #
std::vector<std::vector<int>> membutuhkan satu pointer per baris — overhead 8 byte per baris dan satu tingkat indirection saat akses. Untuk matriks besar (mis. 1000 × 1000), overhead mencapai 8 KB dan potensi cache miss tambahan. Alternatif: simpan sebagai std::vector<int> 1D dengan indexing manual m[i * cols + j].
Kode: Flat Vector dengan Helper #
#include <iostream>
#include <vector>
class Matriks {
int rows_, cols_;
std::vector<int> data_;
public:
Matriks(int r, int c, int init = 0)
: rows_(r), cols_(c), data_(static_cast<std::size_t>(r) * c, init) {}
int& at(int i, int j) { return data_[static_cast<std::size_t>(i) * cols_ + j]; }
int at(int i, int j) const { return data_[static_cast<std::size_t>(i) * cols_ + j]; }
int rows() const { return rows_; }
int cols() const { return cols_; }
};
int main() {
Matriks m(2, 3);
m.at(0, 0) = 1; m.at(0, 1) = 2; m.at(0, 2) = 3;
m.at(1, 0) = 4; m.at(1, 1) = 5; m.at(1, 2) = 6;
for (int i = 0; i < m.rows(); ++i) {
for (int j = 0; j < m.cols(); ++j) {
std::cout << m.at(i, j) << ' ';
}
std::cout << '\n';
}
return 0;
}Output yang Dihasilkan:
1 2 3
4 5 6- Baris
data_(static_cast<std::size_t>(r) * c, init): alokasi tunggalr * celemen. Menghindari alokasi per baris. - Baris
int& at(int i, int j): akses elemen dengan rumusi * cols_ + j. Versiconstuntuk konteks read-only. - Class wrapper menyembunyikan detail indexing dari pemanggil — kode pemanggil tetap ekspresif
m.at(i, j) = v;.
Inisialisasi dan Konstruksi #
Pola Konstruksi yang Umum #
#include <iostream>
#include <vector>
int main() {
// Pola 1: 3x3 dengan nilai default 0
std::vector<std::vector<int>> a(3, std::vector<int>(3, 0));
// Pola 2: langsung dengan initializer list (C++11)
std::vector<std::vector<int>> b = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Pola 3: identitas (diagonal 1)
int n = 3;
std::vector<std::vector<int>> id(n, std::vector<int>(n, 0));
for (int i = 0; i < n; ++i) id[i][i] = 1;
std::cout << "a[1][2] = " << a[1][2] << '\n';
std::cout << "b[2][0] = " << b[2][0] << '\n';
std::cout << "id:\n";
for (const auto& row : id) {
for (int x : row) std::cout << ' ' << x;
std::cout << '\n';
}
return 0;
}Output yang Dihasilkan:
a[1][2] = 0
b[2][0] = 7
id:
1 0 0
0 1 0
0 0 1- Baris
std::vector<std::vector<int>> a(3, std::vector<int>(3, 0));: konstruktor vector-of-vector menerima jumlah baris (3) dan nilai inisialisasi per baris (vector 3 elemen bernilai0). - Baris
for (const auto& row : id): range-based for denganconst auto&— menghindari copy seluruh baris. Inner loopfor (int x : row)aman copy karenaintmurah.
Ringkasan #
- Array 2D di C++ disimpan secara row-major: baris demi baris di blok memori kontigu.
- Traversal row-major (
m[i][j]dengan loop j terdalam) jauh lebih cepat daripada column-major karena cache friendly. std::vector<std::vector<int>>fleksibel namun punya overhead pointer per baris.- Untuk matriks besar yang ukurannya diketahui,
std::vector<int>1D dengan indexingi * cols + jlebih hemat memori dan lebih cepat. - Bungkus indexing manual dalam class atau fungsi helper agar kode pemanggil tetap ekspresif.