#include <bits/stdc++.h>

#define REP(i, n) for(decltype(n) i = 0; i < n; i++)

#define ALL(c)   c.begin(), c.end()
#define SORT(c)  std::sort(ALL(c))
#define RSORT(c) std::sort(ALL(c), std::greater<decltype(c)::value_type>())

using namespace std;
using ll = long long;

const int MOD  = (int)1e9 + 7;
const int INF  = (int)1e9 + 1;
const ll  LINF = (long long)1e18 + 1;

class UnionFind {
    using ll = long long;
public:
    struct Node {
        ll parent, rank, weight;
        Node() = default;
        Node(const ll& _parent,
             const ll& _rank,
             const ll& _weight = 0) :
            parent(_parent),
            rank(_rank),
            weight(_weight) { }
    };

    std::vector<Node> node;

    UnionFind(const ll& num_node) :
        node(num_node) {
        for(ll i = 0; i < num_node; i++) {
            node[i].parent = i;
        }
    }

    ll searchAndZipRoot(const ll& x) {
        if(node[x].parent == x) {
            return x;
        }
        else {
            auto r = searchAndZipRoot(node[x].parent);
            node[x].weight += node[node[x].parent].weight;
            return node[x].parent = r;
        }
    }

    ll calcWeight(const ll& x) {
        searchAndZipRoot(x);
        return node[x].weight;
    }

    void unite(const ll& x,
               const ll& y,
               const ll& weight = 0) {
        auto rx = searchAndZipRoot(x);
        auto ry = searchAndZipRoot(y);
        if(rx != ry) {
            auto yw = weight + calcWeight(x) - calcWeight(y);
            if(node[rx].rank < node[ry].rank) {
                std::swap(rx, ry);
                yw *= -1;
            }
            node[ry].parent = rx;
            node[ry].weight = yw;
            if(node[rx].rank == node[ry].rank) {
                node[rx].rank++;
            }
        }
    }

    ll calcDiff(const ll& x,
                const ll& y) {
        return std::abs(calcWeight(x) - calcWeight(y));
    }

    template<class... A>
    bool checkWhetherRootIsSame(const A&... nodes) {
        auto node_list = std::initializer_list<ll>{nodes...};
        if(node_list.size() < 2) return false;
        auto v = searchAndZipRoot(*node_list.begin());
        for(size_t i = 1; i < node_list.size(); i++) {
            if(v != searchAndZipRoot(*(node_list.begin() + i))) return false;
        }
        return true;
    }
};

class CommonGraph {
    using ll = long long;
public:
    struct Edge {
        enum class Type { Directed, Undirected };
        int    src, dst;
        ll     cost, capacity, flow;
        size_t rev;
        Edge(const int&    _src,
             const int&    _dst,
             const ll&     _cost     = 0,
             const ll&     _capacity = 0,
             const ll&     _frow     = 0,
             const size_t& _rev      = 0) :
            src(_src),
            dst(_dst),
            cost(_cost),
            capacity(_capacity),
            flow(_frow),
            rev(_rev) { }
    };
    friend std::ostream& operator <<(std::ostream& os, const Edge& obj) {
        os << "src : "  << obj.src      << ", "
           << "dst : "  << obj.dst      << ", "
           << "cost : " << obj.cost     << ", "
           << "cap : "  << obj.capacity << ", "
           << "flow : " << obj.flow     << ", "
           << "rev : "  << obj.rev;
        return os;
    }

    struct Vertex {
        ll cost;
        Vertex(const ll& _cost) : cost(_cost) { }
    };

    enum class Type { Normal, Flow };

    CommonGraph(const int&        num_vertex,
                const Type&       type,
                const Edge::Type& edge_type) :
        vertex_(num_vertex, 0),
        edge_(num_vertex),
        total_cost_(0),
        type_(type),
        edge_type_(edge_type) { }

    ll getTotalCost() const {
        return total_cost_;
    }

    void addEdge(const int& src,
                 const int& dst,
                 const ll&  cost,
                 const ll&  capacity = 0) {
        switch(type_) {
            case Type::Normal: {
                switch(edge_type_) {
                    case Edge::Type::Directed: {
                        edge_[src].emplace_back(src, dst, cost);
                        break;
                    }
                    case Edge::Type::Undirected: {
                        edge_[src].emplace_back(src, dst, cost);
                        edge_[dst].emplace_back(dst, src, cost);
                        break;
                    }
                }
            }
            case Type::Flow: {
                switch(edge_type_) {
                    case Edge::Type::Directed: {
                        edge_[src].emplace_back(src, dst, cost,  capacity, 0, edge_[dst].size());
                        edge_[dst].emplace_back(dst, src, -cost,        0, 0, edge_[src].size() - 1);
                        break;
                    }
                    case Edge::Type::Undirected: {
                        edge_[src].emplace_back(src, dst, cost, capacity, 0, edge_[dst].size());
                        edge_[dst].emplace_back(dst, src, cost, capacity, 0, edge_[src].size() - 1);
                        break;
                    }
                }
            }
        }
        total_cost_ += cost;
    }

    void dumpEdgeInfo() {
        for(const auto& v : edge_) {
            for(const auto& e : v) {
                std::cout << "[" << e << "]" << std::endl;
            }
            std::cout << std::endl;
        }
    }

    // 最大流問題を解く
    // Dinic's blocking flow
    ll calcMaxFlow(const int& src_index,
                   const int& sink_index) {
        if(type_ != Type::Flow) {
            throw std::logic_error("Graph type is invalid");
        }
        std::vector<int> level(vertex_.size()), iter(vertex_.size());
        std::function<int(void)> levelize
            = [&]() {
                  level.assign(vertex_.size(), -1);
                  level[src_index] = 0;
                  queue<int> Q; Q.push(src_index);
                  while(!Q.empty()) {
                      int u = Q.front(); Q.pop();
                      if(u == sink_index) break;
                      for(const auto& e : edge_[u]) {
                          if(e.flow < e.capacity && level[e.dst] < 0) {
                              Q.push(e.dst);
                              level[e.dst] = level[u] + 1;
                          }
                      }
                  }
                  return level[sink_index];
              };
        function<ll(int, ll)> augment
            = [&](int u, ll cur) {
                  if(u == sink_index) return cur;
                  for(size_t i = iter[u]; i < edge_[u].size(); ++i) {
                      auto& e = edge_[u][i];
                      auto& r = edge_[e.dst][e.rev];
                      if(e.flow < e.capacity && level[u] < level[e.dst]) {
                          auto f = augment(e.dst, min(cur, e.capacity - e.flow));
                          if(0 < f) { e.flow += f; r.flow -= f; return f; }
                      }
                  }
                  return 0LL;
              };
        for(auto& v : edge_) for(auto& e : v) e.flow = 0;
        auto flow = 0LL;
        while(0 <= levelize()) {
            std::fill(iter.begin(), iter.end(), 0);
            for(ll f; 0 < (f = augment(src_index, INF)); flow += f);
        }
        return flow;
    }

    // 幅優先探索で最小コストを求める
    bool calcMinCostByBFS(ll&        min_cost,
                          const int& start_index,
                          const int& goal_index) {
        if(type_ != Type::Normal) {
            throw std::logic_error("Graph type is invalid");
        }
        std::fill(vertex_.begin(), vertex_.end(), std::numeric_limits<ll>::max());
        vertex_[start_index] = 0;
        queue<int> Q; Q.push(start_index);
        while(!Q.empty()) {
            const auto src = Q.front(); Q.pop();
            for(const auto& v : edge_[src]) {
                const auto& dst  = v.dst;
                const auto  cost = vertex_[src].cost + v.cost;
                if(cost < vertex_[dst].cost) {
                    vertex_[dst] = cost; Q.push(dst);
                }
            }
        }
        min_cost = vertex_[goal_index].cost;
        return (min_cost == std::numeric_limits<ll>::max()) ? false : true;
    }

    // 最小全域木を求める
    // Kruskal's algorithm
    CommonGraph generateMinimumSpanningTree() {
        if(type_ != Type::Normal && edge_type_ != Edge::Type::Undirected) {
            throw std::logic_error("Graph type is invalid");
        }
        CommonGraph mst(vertex_.size(), Type::Normal, Edge::Type::Undirected);
        std::vector<Edge> mst_e;
        for(const auto& v : edge_) {
            for(const auto& e : v) {
                mst_e.push_back(e);
            }
        }
        auto sort_rule = [](const Edge& l, const Edge& r) { return l.cost < r.cost; };
        sort(mst_e.begin(), mst_e.end(), sort_rule);
        UnionFind uf(vertex_.size());
        for(const auto& e : mst_e) {
            if(!uf.checkWhetherRootIsSame(e.src, e.dst)) {
                mst.addEdge(e.src, e.dst, e.cost);
                uf.unite(e.src, e.dst);
            }
        }
        return mst;
    }

private:
    std::vector<Vertex>            vertex_;
    std::vector<std::vector<Edge>> edge_;
    ll                             total_cost_;
    const Type                     type_;
    const Edge::Type               edge_type_;
};

int main() {
    try {
        cin.tie(0);
        ios::sync_with_stdio(false);

        int V, E;
        std::cin >> V >> E;
        CommonGraph g(V, CommonGraph::Type::Normal, CommonGraph::Edge::Type::Undirected);
        REP(i, E) {
            int s, t, w;
            std::cin >> s >> t >> w;
            g.addEdge(s, t, w);
        }
        auto mst = g.generateMinimumSpanningTree();
        std::cout << mst.getTotalCost() << std::endl;
    }
    catch(const std::exception& e) {
        std::cout << e.what() << std::endl;
        exit(1);
    }
    return 0;
}



Measured memory_bytes: 48944