#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <stack>
#include <string>
#include <utility>
#include <vector>
#define FOR(i,a,b) for (int i=(a),for_##i##_max=(b);i<=(for_##i##_max);++i)
#define RFOR(i,a,b) for (int i=(a),rfor_##i##_min=(b);i>=(rfor_##i##_min);--i)
#define REP(i,n) for (int i=0,rep_##i##_len=(n);i<(rep_##i##_len);++i)
#define RREP(i,n) for (int i=(n)-1;i>=0;--i)

namespace { // Basics

template<typename T>
void chmin(T &x, T y) {
  x = std::min(x, y);
}

template<typename T>
void chmax(T &x, T y) {
  x = std::max(x, y);
}

using lli = long long int;
using ld = long double;

} // namespace

namespace { // Graph Basics

template<typename WeightT>
struct Edge {
  int from, to;
  WeightT weight;
  Edge(int s, int d, WeightT w): from(s), to(d), weight(w) {}
};

template<typename WeightT>
bool
operator< (Edge<WeightT> const &lhs, Edge<WeightT> const &rhs) {
  if (lhs.weight != rhs.weight) {
    return lhs.weight < rhs.weight;
  } else if (lhs.from != rhs.from) {
    return lhs.from < rhs.from;
  }
  return lhs.to < rhs.to;
}

template<typename WeightT>
using Edges = std::vector<Edge<WeightT> >;

template<typename WeightT>
using AdjacencyList = std::vector<Edges<WeightT> >;

template<typename WeightT>
void
add_undir_edge(AdjacencyList<WeightT> &graph, int u, int v, WeightT w) {
  graph[u].emplace_back(u, v, w);
  graph[v].emplace_back(v, u, w);
}

template<typename WeightT>
AdjacencyList<WeightT>
inverse_graph(AdjacencyList<WeightT> const &G) {
  size_t const n = G.size();
  AdjacencyList<WeightT> F(n);
  for (size_t i = 0; i < n; ++i) {
    for (auto &&e: G[i]) {
      F[e.to].emplace_back(e.to, e.from, e.weight);
    }
  }
  return F;
}

} // namespace

namespace { // Strongnly Connected Components

struct SccResult {
  std::vector<int> top_index;
  std::vector<std::vector<int> > components;
};

template<typename T>
SccResult
kosaraju_scc(AdjacencyList<T> const &G) {
  int const N = G.size();

  // 1st DFS
  std::stack<int> postord;
  std::vector<bool> used(N, false);
  for (int s = 0; s < N; ++s) {
    if (used[s]) { continue; }
    used[s] = true; postord.push(s);
    std::stack<int> stk; stk.push(s);
    while (!stk.empty()) {
      auto const u = stk.top(); stk.pop();
      if (used[u]) { continue; }
      used[u] = true; postord.push(u);
      for (auto &&e: G[u]) {
        stk.push(e.to);
      }
    }
  }

  // 2nd DFS
  auto const rG = inverse_graph(G);
  std::fill(begin(used), end(used), false);
  std::vector<int> order(N);
  std::vector<std::vector<int>> components;
  int top_index = 0;
  while (!postord.empty()) {
    auto const s = postord.top(); postord.pop();
    if (used[s]) { continue; }
    std::vector<int> scc;
    std::stack<int> stk; stk.push(s);
    while (!stk.empty()) {
      auto const u = stk.top(); stk.pop();
      if (used[u]) { continue; }
      used[u] = true;
      order[u] = top_index;
      scc.push_back(u);
      for (auto &&e: rG[u]) {
        stk.push(e.to);
      }
    }
    ++top_index;
    components.push_back(std::move(scc));
  }
  return { order, components };
}

} // namespace

using namespace std;

using Graph = AdjacencyList<int>;

void solve(Graph const &G) {
  auto const scc = kosaraju_scc(G);
  cout << scc.components.size() << endl;
}

int main() {
  // cout << fixed << setprecision(10);

  int N, M;
  cin >> N >> M;
  Graph G(N);
  REP(i, M) {
    int X, Y, Z;
    cin >> X >> Y >> Z;
    add_undir_edge(G, --X, --Y, Z);
  }
  solve(G);
  return 0;
}


Measured memory_bytes: 23548