#include #include using namespace std; using namespace atcoder; // using mint = modint1000000007; // const int mod = 1000000007; using mint = modint998244353; const int mod = 998244353; const int INF = 1e9; // const long long LINF = 1e18; #define rep(i, n) for (int i = 0; i < (n); ++i) #define rep2(i, l, r) for (int i = (l); i < (r); ++i) #define rrep(i, n) for (int i = (n)-1; i >= 0; --i) #define rrep2(i, l, r) for (int i = (r)-1; i >= (l); --i) #define all(x) (x).begin(), (x).end() #define allR(x) (x).rbegin(), (x).rend() #define P pair template inline bool chmax(A& a, const B& b) { if (a < b) { a = b; return true; } return false; } template inline bool chmin(A& a, const B& b) { if (a > b) { a = b; return true; } return false; } /** * @file xor_shift.hpp * @brief Xorshift 乱数生成器 * @author kwm_t * @see https://github.com/kwm-t/cp-library/blob/main/utility/xor_shift.hpp */ #ifndef KWM_T_UTILITY_XORSHIFT_HPP #define KWM_T_UTILITY_XORSHIFT_HPP #include #include namespace kwm_t::utility { /** * @brief Xorshift 乱数生成器(軽量・高速) * * 典型用途: * - 競プロの乱択 * - テストケース生成 * * 計算量: * O(1) * * 注意: * - 暗号用途には不適 * - seed=0 は避ける * * verified: * - 競技プログラミング用途 */ class Xorshift { public: using result_type = uint32_t; explicit Xorshift(uint64_t seed = 1) : x(seed ? seed : 1) {} static constexpr result_type min() { return std::numeric_limits::min(); } static constexpr result_type max() { return std::numeric_limits::max(); } /** * @brief [0, 2^32) の乱数 */ result_type operator()() { x ^= x << 9; x ^= x >> 7; return static_cast(x); } /** * @brief [0, m) の整数乱数 */ int64_t operator()(int64_t m) { return (static_cast((*this)()) * m) >> 32; } /** * @brief [0, 1) の実数乱数 */ double uniform() { return static_cast((*this)()) / (static_cast(1) << 32); } private: uint64_t x; }; } // namespace kwm_t::utility #endif // KWM_T_UTILITY_XORSHIFT_HPP /** * @file gauss_jordan.hpp * @brief Gauss-Jordan 消去法 * @author kwm_t * @see https://github.com/kwm-t/cp-library/blob/main/math/matrix/gauss_jordan.hpp */ #ifndef KWM_T_MATH_MATRIX_GAUSS_JORDAN_HPP #define KWM_T_MATH_MATRIX_GAUSS_JORDAN_HPP #include #include namespace kwm_t::math::matrix { /** * @brief Gauss-Jordan elimination(掃き出し法) * * Ax = b を解く / ランク / kernel を求める * * 要件: * - T は体(+,-,*,/ が使える) * - 0 判定ができる(== 0) * * verified: * https://judge.yosupo.jp/submission/367294 */ template class GaussJordan { public: GaussJordan(std::vector> A, std::vector b) : A(std::move(A)), b(std::move(b)) {} /** * @brief 掃き出し(内部状態を書き換える) * @return rank */ int sweep() { int n = A.size(); int m = A[0].size(); rank = 0; for (int col = 0; col < m; ++col) { int pivot = -1; for (int row = rank; row < n; ++row) { if (A[row][col] != T(0)) { pivot = row; break; } } if (pivot == -1) continue; std::swap(A[rank], A[pivot]); std::swap(b[rank], b[pivot]); T inv = T(1) / A[rank][col]; for (int j = col; j < m; ++j) A[rank][j] *= inv; b[rank] *= inv; for (int row = 0; row < n; ++row) { if (row == rank) continue; if (A[row][col] == T(0)) continue; T factor = A[row][col]; for (int j = col; j < m; ++j) { A[row][j] -= factor * A[rank][j]; } b[row] -= factor * b[rank]; } ++rank; } return rank; } /** * @brief 解を1つ求める(存在すれば) */ bool solve(std::vector& x) { int n = A.size(); int m = A[0].size(); sweep(); x.assign(m, T(0)); for (int i = rank; i < n; ++i) { if (b[i] != T(0)) return false; } for (int i = 0; i < rank; ++i) { for (int j = 0; j < m; ++j) { if (A[i][j] != T(0)) { x[j] = b[i]; break; } } } return true; } /** * @brief kernel(解空間の基底) */ std::vector> kernel() { int n = A.size(); int m = A[0].size(); sweep(); std::vector pivot_col(m, -1); int row = 0; for (int col = 0; col < m && row < rank; ++col) { if (A[row][col] == T(1)) { pivot_col[col] = row; ++row; } } std::vector> res; for (int col = 0; col < m; ++col) { if (pivot_col[col] != -1) continue; std::vector v(m, T(0)); v[col] = T(1); for (int c = 0; c < m; ++c) { if (pivot_col[c] != -1) { v[c] = -A[pivot_col[c]][col]; } } res.push_back(v); } return res; } int rank = 0; private: std::vector> A; std::vector b; }; } // namespace kwm_t::math::matrix #endif /** * @file linear_matroid_parity.hpp * @brief 線形マトロイドパリティ * @author kwm_t * @see https://github.com/kwm-t/cp-library/blob/main/math/matroid/linear_matroid_parity.hpp */ #ifndef KWM_T_MATH_MATROID_LINEAR_MATROID_PARITY_HPP #define KWM_T_MATH_MATROID_LINEAR_MATROID_PARITY_HPP #include #include #include //#include "../matrix/gauss_jordan.hpp" //#include "../../utility/xor_shift.hpp" namespace kwm_t::math::matroid { namespace internal { /** * @brief 0 でないランダムな Mint を生成する */ template Mint random_nonzero(kwm_t::utility::Xorshift& rnd) { Mint x = Mint(rnd()); while (x == Mint(0)) { x = Mint(rnd()); } return x; } /** * @brief 行列のランクを求める */ template int rank(std::vector> mat) { int n = static_cast(mat.size()); if (n == 0) return 0; std::vector b(n, Mint(0)); kwm_t::math::matrix::GaussJordan gj( std::move(mat), std::move(b) ); return gj.sweep(); } /** * @brief 2 本の疎ベクトルの外積による歪対称行列への加算 * * mat += r * (p * q^T - q * p^T) */ template void add_pair( std::vector>& mat, const std::vector>& p, const std::vector>& q, Mint r ) { for (const auto& [i, x] : p) { for (const auto& [j, y] : q) { Mint v = r * x * y; mat[i][j] += v; mat[j][i] -= v; } } } } // namespace internal /** * @brief 線形マトロイドパリティ(dense) * * 各ペアは 2 本の固定されたベクトル (p, q) からなる。 * 各ペアに独立なランダム値 r を割り当て、 * * M += r * (p * q^T - q * p^T) * * によって歪対称行列を構成する。 * * 行列の rank / 2 を最大ペア数の確率的な推定値として返す。 * * @tparam Mint 有限体の型 * @tparam T ベクトルの要素型 * @param pairs ベクトルのペア * @param rnd 乱数生成器 * @param repeat ランダム化の試行回数 * @return 最大ペア数の確率的な推定値 * * 各ベクトルの長さはすべて等しいこと。 */ template int dense( const std::vector, std::vector>>& pairs, kwm_t::utility::Xorshift& rnd, int repeat = 2 ) { int n = 0; if (!pairs.empty()) { n = static_cast(pairs[0].first.size()); } int ans = 0; for (int rep = 0; rep < repeat; ++rep) { std::vector> mat(n, std::vector(n)); for (const auto& [p, q] : pairs) { std::vector> x, y; for (int i = 0; i < n; ++i) { if (p[i] != T(0)) { x.emplace_back(i, Mint(p[i])); } if (q[i] != T(0)) { y.emplace_back(i, Mint(q[i])); } } Mint r = Mint(rnd()); internal::add_pair(mat, x, y, r); } ans = std::max(ans, internal::rank(std::move(mat)) / 2); } return ans; } /** * @brief 線形マトロイドパリティ(sparse) * * 各ペアは 2 本の固定された疎ベクトルからなる。 * 各ペアに独立なランダム値 r を割り当て、 * * M += r * (p * q^T - q * p^T) * * によって歪対称行列を構成する。 * * 行列の rank / 2 を最大ペア数の確率的な推定値として返す。 * * @tparam Mint 有限体の型 * @tparam T ベクトルの要素型 * @param n ベクトルの次元 * @param pairs ベクトルのペア * @param rnd 乱数生成器 * @param repeat ランダム化の試行回数 * @return 最大ペア数の確率的な推定値 * * 各ベクトル内で座標は重複せず、0 <= 座標 < n を満たすこと。 */ template int sparse( int n, const std::vector< std::pair< std::vector>, std::vector> > >& pairs, kwm_t::utility::Xorshift& rnd, int repeat = 2 ) { int ans = 0; for (int rep = 0; rep < repeat; ++rep) { std::vector> mat(n, std::vector(n)); for (const auto& [p, q] : pairs) { std::vector> x, y; x.reserve(p.size()); y.reserve(q.size()); for (const auto& [i, val] : p) { if (val != T(0)) { x.emplace_back(i, Mint(val)); } } for (const auto& [i, val] : q) { if (val != T(0)) { y.emplace_back(i, Mint(val)); } } Mint r = Mint(rnd()); internal::add_pair(mat, x, y, r); } ans = std::max(ans, internal::rank(std::move(mat)) / 2); } return ans; } /** * @brief サポートから生成したベクトルによる線形マトロイドパリティ(dense) * * 各ペアは 2 本のベクトルのサポートで与える。 * 各ベクトルの非ゼロ座標には、独立なランダム値を割り当てる。 * * 入力の T は非ゼロ座標の判定にのみ使用する。 * 実際のベクトルの値は使用しない。 * * @tparam Mint 有限体の型 * @tparam T サポート判定に使用する要素型 * @param pairs ベクトルのペア * @param rnd 乱数生成器 * @param repeat ランダム化の試行回数 * @return 最大ペア数の確率的な推定値 * * 各ベクトルの長さはすべて等しいこと。 * この方式の正当性は、入力のサポートに応じた問題の帰着に依存する。 * * verified * https://atcoder.jp/contests/abc482/submissions/80547562 */ template int dense_support( const std::vector, std::vector>>& pairs, kwm_t::utility::Xorshift& rnd, int repeat = 2 ) { int n = 0; if (!pairs.empty()) { n = static_cast(pairs[0].first.size()); } int ans = 0; for (int rep = 0; rep < repeat; ++rep) { std::vector> mat(n, std::vector(n)); for (const auto& [p, q] : pairs) { std::vector> x, y; for (int i = 0; i < n; ++i) { if (p[i] != T(0)) { x.emplace_back(i, internal::random_nonzero(rnd)); } if (q[i] != T(0)) { y.emplace_back(i, internal::random_nonzero(rnd)); } } internal::add_pair(mat, x, y, Mint(1)); } ans = std::max(ans, internal::rank(std::move(mat)) / 2); } return ans; } /** * @brief サポートから生成したベクトルによる線形マトロイドパリティ(sparse) * * 各ペアは 2 本のベクトルのサポートで与える。 * 各ベクトルの非ゼロ座標には、独立なランダム値を割り当てる。 * * 入力の T は非ゼロ座標の判定にのみ使用する。 * 実際のベクトルの値は使用しない。 * * @tparam Mint 有限体の型 * @tparam T サポート判定に使用する要素型 * @param n ベクトルの次元 * @param pairs ベクトルのペア * @param rnd 乱数生成器 * @param repeat ランダム化の試行回数 * @return 最大ペア数の確率的な推定値 * * 各ベクトル内で座標は重複せず、0 <= 座標 < n を満たすこと。 * この方式の正当性は、入力のサポートに応じた問題の帰着に依存する。 * * verified * https://atcoder.jp/contests/abc482/submissions/80547619 */ template int sparse_support( int n, const std::vector< std::pair< std::vector>, std::vector> > >& pairs, kwm_t::utility::Xorshift& rnd, int repeat = 2 ) { int ans = 0; for (int rep = 0; rep < repeat; ++rep) { std::vector> mat(n, std::vector(n)); for (const auto& [p, q] : pairs) { std::vector> x, y; x.reserve(p.size()); y.reserve(q.size()); for (const auto& [i, val] : p) { if (val != T(0)) { x.emplace_back(i, internal::random_nonzero(rnd)); } } for (const auto& [i, val] : q) { if (val != T(0)) { y.emplace_back(i, internal::random_nonzero(rnd)); } } internal::add_pair(mat, x, y, Mint(1)); } ans = std::max(ans, internal::rank(std::move(mat)) / 2); } return ans; } } // namespace kwm_t::math::matroid #endif void solve() { int n, m; cin >> n >> m; kwm_t::utility::Xorshift rnd; { vector>, vector>>> pairs; rep(i, m) { int u, v, w; cin >> u >> v >> w; u--, v--, w--; pair>, vector>> tmp; tmp.first.emplace_back(u, 1); tmp.first.emplace_back(w, -1); tmp.second.emplace_back(v, 1); tmp.second.emplace_back(w, -1); pairs.push_back(tmp); } int ans = kwm_t::math::matroid::sparse(n, pairs, rnd); cout << ans << endl; } } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int t = 1; while (t--)solve(); return 0; }