// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // BlockLanczos.hpp // GF(2) 上の線形代数: Gauss 消去 + Block Lanczos // SIQS / GNFS の共通インフラ // // 参考文献: // P.L. Montgomery, "A Block Lanczos Algorithm for Finding Dependencies // over GF(2)", Eurocrypt 1995, LNCS 921, pp.106-120 #pragma once #include #include #include #include #include #include #include namespace calx { namespace gf2_linalg { // ================================================================ // GF(2) 疎行列: CSR (Compressed Sparse Row) 形式 // ================================================================ // Block Lanczos は CSR 行列を使用して行列×ベクトル積を高速化する。 // 現在は将来の Block Lanczos 実装のための基盤として提供。 struct SparseMatrixGF2 { int nrows; int ncols; std::vector row_ptr; // row_ptr[i] = 行 i の開始インデックス (size = nrows+1) std::vector col_idx; // col_idx[k] = k 番目の非零要素の列番号 SparseMatrixGF2() : nrows(0), ncols(0) {} // dense 行列 (vector>) から構築 void buildFromDense(const std::vector>& mat, int nr, int nc) { nrows = nr; ncols = nc; row_ptr.resize(nr + 1); col_idx.clear(); row_ptr[0] = 0; for (int i = 0; i < nr; ++i) { for (int j = 0; j < nc; ++j) { if (mat[i][j]) col_idx.push_back(j); } row_ptr[i + 1] = static_cast(col_idx.size()); } } // y = A * x (64 ベクトル同時) void mulVec(const uint64_t* x, uint64_t* y) const { std::memset(y, 0, nrows * sizeof(uint64_t)); for (int i = 0; i < nrows; ++i) { uint64_t acc = 0; for (int k = row_ptr[i]; k < row_ptr[i + 1]; ++k) acc ^= x[col_idx[k]]; y[i] = acc; } } // y = A^T * x (64 ベクトル同時) void mulVecTranspose(const uint64_t* x, uint64_t* y) const { std::memset(y, 0, ncols * sizeof(uint64_t)); for (int i = 0; i < nrows; ++i) { uint64_t xi = x[i]; if (xi == 0) continue; for (int k = row_ptr[i]; k < row_ptr[i + 1]; ++k) y[col_idx[k]] ^= xi; } } // 非零要素数 int nnz() const { return static_cast(col_idx.size()); } }; // ================================================================ // 64×64 GF(2) 行列演算 (Block Lanczos のブロック演算用) // ================================================================ struct Mat64 { uint64_t m[64]; Mat64() { std::memset(m, 0, sizeof(m)); } static Mat64 identity() { Mat64 r; for (int i = 0; i < 64; ++i) r.m[i] = 1ULL << i; return r; } // C = A * B static Mat64 mul(const Mat64& a, const Mat64& b) { Mat64 c; for (int i = 0; i < 64; ++i) { uint64_t row = a.m[i], result = 0; while (row) { result ^= b.m[_tzcnt_u64(row)]; row &= row - 1; } c.m[i] = result; } return c; } // B = A^T static Mat64 transpose(const Mat64& a) { Mat64 r; for (int i = 0; i < 64; ++i) { uint64_t col = 0; for (int j = 0; j < 64; ++j) if (a.m[j] & (1ULL << i)) col |= 1ULL << j; r.m[i] = col; } return r; } // 逆行列 (存在する場合)。成功なら true。 static bool invert(const Mat64& a, Mat64& result) { Mat64 work; std::memcpy(work.m, a.m, sizeof(work.m)); result = identity(); for (int col = 0; col < 64; ++col) { uint64_t bit = 1ULL << col; int pivot = -1; for (int row = col; row < 64; ++row) if (work.m[row] & bit) { pivot = row; break; } if (pivot < 0) return false; if (pivot != col) { std::swap(work.m[col], work.m[pivot]); std::swap(result.m[col], result.m[pivot]); } for (int row = 0; row < 64; ++row) { if (row != col && (work.m[row] & bit)) { work.m[row] ^= work.m[col]; result.m[row] ^= result.m[col]; } } } return true; } }; // v^T * w → 64×64 行列 inline Mat64 dotProduct(const uint64_t* v, const uint64_t* w, int n) { Mat64 result; for (int i = 0; i < n; ++i) { uint64_t vi = v[i], wi = w[i]; while (vi) { result.m[_tzcnt_u64(vi)] ^= wi; vi &= vi - 1; } } return result; } // ================================================================ // Block Lanczos ヘルパー関数 // ================================================================ // 64-bit ワードを 64×64 行列の行として右から乗算 // v のビット k が立っている → M.m[k] を XOR inline uint64_t applyRow(uint64_t v, const Mat64& M) { uint64_t result = 0; while (v) { result ^= M.m[_tzcnt_u64(v)]; v &= v - 1; } return result; } // D の部分空間 S (列マスク) と S 上の擬似逆行列を計算 // D = v_i^T * B * v_i (64×64) // S = D の線形独立な列のビットマスク // D_inv = S に制限した D の逆行列 (S 外は 0) inline void computeSubspace(const Mat64& D, uint64_t& S, Mat64& D_inv) { Mat64 work; std::memcpy(work.m, D.m, sizeof(work.m)); Mat64 inv = Mat64::identity(); S = 0; for (int col = 0; col < 64; ++col) { uint64_t bit = 1ULL << col; int pivot = -1; for (int row = col; row < 64; ++row) if (work.m[row] & bit) { pivot = row; break; } if (pivot < 0) continue; S |= bit; if (pivot != col) { std::swap(work.m[col], work.m[pivot]); std::swap(inv.m[col], inv.m[pivot]); } for (int row = 0; row < 64; ++row) { if (row != col && (work.m[row] & bit)) { work.m[row] ^= work.m[col]; inv.m[row] ^= inv.m[col]; } } } // S 外の行・列をゼロクリア D_inv = inv; for (int k = 0; k < 64; ++k) { if (!(S & (1ULL << k))) { D_inv.m[k] = 0; } // S 外の列をクリア D_inv.m[k] &= S; } } // ================================================================ // Block Lanczos 反復 (Montgomery 1995, Algorithm 2) // ================================================================ // GF(2) 上の nrows×ncols 行列 A に対して、 // B = A·A^T (nrows×nrows 対称行列) の核を求める。 // nrows > ncols (SIQS/GNFS の典型) の場合、rank(M) ≤ ncols なので // dim ker(M M^T) ≥ nrows - ncols。 // M が列フルランクなら ker(M M^T) = ker(M^T)。 // 結果は行インデックスのリスト (= gaussianEliminationGF2 と同じ形式)。 // // ブロック幅 64 (マシンワード): 64 本のベクトルを同時に処理。 // 計算量: O(w · N / 64 · iter) where w=非零要素数, iter≈N/64 inline std::vector> blockLanczosGF2( const SparseMatrixGF2& A, uint64_t seed = 42) { int nrows = A.nrows; int ncols = A.ncols; int N = nrows; // B = A·A^T は nrows×nrows 対称行列 // バッファ確保 std::vector v(N, 0), v_prev(N, 0); std::vector Bv(N, 0), Bv_prev(N, 0), tmp(ncols, 0); std::vector vnext(N, 0); std::vector y(N, 0); std::vector x_sol(N, 0); // ランダム初期ベクトル x_0 (nrows 次元) std::vector x0(N, 0); std::mt19937_64 rng(seed); for (int j = 0; j < N; ++j) x0[j] = rng(); // y = B * x_0 = A * (A^T * x_0) A.mulVecTranspose(x0.data(), tmp.data()); A.mulVec(tmp.data(), y.data()); // v_0 = y std::memcpy(v.data(), y.data(), N * sizeof(uint64_t)); Mat64 D_inv_prev; uint64_t S_prev = 0; int max_iter = N / 64 + 128; for (int iter = 0; iter < max_iter; ++iter) { // 1. Bv = B * v = A * (A^T * v) A.mulVecTranspose(v.data(), tmp.data()); A.mulVec(tmp.data(), Bv.data()); // 2. D_i = v^T * Bv Mat64 D = dotProduct(v.data(), Bv.data(), N); // 3. 部分空間 S_i と D_i^{-1} uint64_t S; Mat64 D_inv; computeSubspace(D, S, D_inv); // 4. 収束判定: S が空 → 終了 if (S == 0) break; // 5. 解の蓄積: x_sol ^= v * (D_inv * (v^T * y)) Mat64 vTy = dotProduct(v.data(), y.data(), N); Mat64 coeff = Mat64::mul(D_inv, vTy); for (int j = 0; j < N; ++j) x_sol[j] ^= applyRow(v[j], coeff); // 6. 三項再帰の係数 (Montgomery 1995) // E_i = D_i^{-1} · V_i^T · B² · V_i (B-orthogonality with v_i) Mat64 E = Mat64::mul(D_inv, dotProduct(Bv.data(), Bv.data(), N)); // F_i = D_{i-1}^{-1} · V_{i-1}^T · B² · V_i (B-orthogonality with v_{i-1}) // V_{i-1}^T · B² · V_i = (B·V_{i-1})^T · (B·V_i) Mat64 F; if (iter > 0) { F = Mat64::mul(D_inv_prev, dotProduct(Bv_prev.data(), Bv.data(), N)); } // 7. 三項再帰: v_{i+1} = S_i · (Bv ⊕ v·E ⊕ v_prev·F) // S_i は全体に適用 (Montgomery Algorithm 2 step vi) for (int j = 0; j < N; ++j) { uint64_t val = Bv[j] ^ applyRow(v[j], E); if (iter > 0) val ^= applyRow(v_prev[j], F); vnext[j] = val & S; } // 8. シフト (Bv_prev も保存) std::swap(v_prev, v); std::swap(v, vnext); std::swap(Bv_prev, Bv); D_inv_prev = D_inv; S_prev = S; } // x = x_0 ⊕ Σ v_i D_i^{-1} v_i^T y (Montgomery's solution) // x ∈ ker(B) = ker(A·A^T) ⊃ ker(A^T) for (int j = 0; j < N; ++j) x_sol[j] ^= x0[j]; // 検証: A^T · x_sol = 0 なら x_sol は行の線形従属を表す std::vector ATx(ncols, 0); A.mulVecTranspose(x_sol.data(), ATx.data()); uint64_t nonzero_bits = 0; for (int i = 0; i < ncols; ++i) nonzero_bits |= ATx[i]; // 有効なビット b について、x_sol[j] のビット b が立っている行 j の集合が // 行従属関係を構成する (= Gauss の出力と同形式) std::vector> results; for (int b = 0; b < 64; ++b) { if (nonzero_bits & (1ULL << b)) continue; std::vector row_indices; for (int j = 0; j < N; ++j) if (x_sol[j] & (1ULL << b)) row_indices.push_back(j); if (row_indices.size() >= 2) results.push_back(std::move(row_indices)); } return results; } // ================================================================ // Gauss 消去 (64-bit ワードパッキング) // ================================================================ // SIQS / GNFS 共通。既存の gaussianEliminationGF2 / gnfsGaussGF2 を統合。 inline std::vector> gaussianEliminationGF2( std::vector>& matrix, int nrows, int ncols) { int col_words = (ncols + 63) / 64; int hist_words = (nrows + 63) / 64; std::vector> packed(nrows, std::vector(col_words, 0)); std::vector> history(nrows, std::vector(hist_words, 0)); for (int i = 0; i < nrows; ++i) { for (int c = 0; c < ncols; ++c) if (matrix[i][c]) packed[i][c / 64] |= 1ULL << (c % 64); history[i][i / 64] = 1ULL << (i % 64); } int pivot_row = 0; for (int col = 0; col < ncols && pivot_row < nrows; ++col) { int wi = col / 64; uint64_t bm = 1ULL << (col % 64); int found = -1; for (int row = pivot_row; row < nrows; ++row) if (packed[row][wi] & bm) { found = row; break; } if (found < 0) continue; if (found != pivot_row) { std::swap(packed[found], packed[pivot_row]); std::swap(history[found], history[pivot_row]); } for (int row = 0; row < nrows; ++row) { if (row == pivot_row || !(packed[row][wi] & bm)) continue; for (int w = 0; w < col_words; ++w) packed[row][w] ^= packed[pivot_row][w]; for (int w = 0; w < hist_words; ++w) history[row][w] ^= history[pivot_row][w]; } ++pivot_row; } std::vector> ns; for (int row = 0; row < nrows; ++row) { bool zero = true; for (int w = 0; w < col_words; ++w) if (packed[row][w]) { zero = false; break; } if (zero) { std::vector indices; for (int w = 0; w < hist_words; ++w) { uint64_t bits = history[row][w]; while (bits) { unsigned long idx; _BitScanForward64(&idx, bits); indices.push_back(w * 64 + static_cast(idx)); bits &= bits - 1; } } if (!indices.empty()) ns.push_back(indices); } } return ns; } // ================================================================ // 統合インタフェース // ================================================================ // 関係数に応じて Gauss 消去 / Block Lanczos をディスパッチ。 inline std::vector> findNullSpaceGF2( std::vector>& matrix, int nrows, int ncols) { // Block Lanczos は疎行列に強く、大きな行列で Gauss O(N³/64) より高速 constexpr int BLOCK_LANCZOS_THRESHOLD = 400; if (nrows >= BLOCK_LANCZOS_THRESHOLD && ncols >= BLOCK_LANCZOS_THRESHOLD) { SparseMatrixGF2 A; A.buildFromDense(matrix, nrows, ncols); // 複数シードでリトライ // ★ 最低 10 ベクトル必要。少なすぎたら Gauss にフォールバック constexpr int MIN_LANCZOS_VECTORS = 10; for (uint64_t seed = 42; seed < 42 + 5; ++seed) { auto result = blockLanczosGF2(A, seed); if ((int)result.size() >= MIN_LANCZOS_VECTORS) return result; } // フォールバック: Block Lanczos が十分な解を見つけられなかった場合 } return gaussianEliminationGF2(matrix, nrows, ncols); } } // namespace gf2_linalg } // namespace calx