### BUGGY CODE ###

#!/usr/bin/env python3
import sys
sys.setrecursionlimit(10 ** 7)


def solve(N: int, M: int, X: "List[int]", Y: "List[int]", Z: "List[int]"):
    # G = {i: [] for i in range(N)}
    nodes = set()
    G = {}
    for x, y in zip(X, Y):
        x -= 1
        y -= 1
        # print(x, y)
        G.setdefault(x, [])
        G.setdefault(y, [])
        G[x].append(y)
        G[y].append(x)
        nodes |= set([x, y])
    # print(G)
    n = N - len(nodes)
    # print(f'nodes: {nodes}')
    # print(f'n: {n}')

    def dfs(v, p):
        ret = set([v])
        for nv in G[v]:
            if nv in p:
                continue
            else:
                ret |= dfs(nv, p + [v])
        return ret

    # print('あ')

    # n = 0
    while True:
        if not len(G):
            break
        # print(G.keys())
        for node in dfs(list(G.keys())[0], []):
            # print(node)
            del G[node]
        # print(n)
        n += 1

    print(n)

    return


# Generated by 1.1.5 https://github.com/kyuridenamida/atcoder-tools
# (tips: You use the default template now. You can remove this line by using your custom template)
def main():
    def iterate_tokens():
        for line in sys.stdin:
            for word in line.split():
                yield word
    tokens = iterate_tokens()
    N = int(next(tokens))  # type: int
    M = int(next(tokens))  # type: int
    X = [int()] * (M)  # type: "List[int]"
    Y = [int()] * (M)  # type: "List[int]"
    Z = [int()] * (M)  # type: "List[int]"
    for i in range(M):
        X[i] = int(next(tokens))
        Y[i] = int(next(tokens))
        Z[i] = int(next(tokens))
    solve(N, M, X, Y, Z)


if __name__ == '__main__':
    main()


### FIXED CODE ###

#!/usr/bin/env python3
import sys
# from typing import List
sys.setrecursionlimit(10 ** 7)


class UnionFind(object):
    """docstring for UnionFind"""

    def __init__(self, n):
        super(UnionFind, self).__init__()
        self.v = [-1 for _ in range(n)]

    def find_root(self, x):
        if self.v[x] < 0:
            return x
        else:
            self.v[x] = self.find_root(self.v[x])
            return self.v[x]

    def unite(self, x, y):
        x = self.find_root(x)
        y = self.find_root(y)
        if x == y:
            return
        if -self.v[x] < -self.v[y]:
            x, y = y, x
        self.v[x] += self.v[y]
        self.v[y] = x

    def is_root(self, x):
        return self.v[x] < 0

    def has_same_root(self, x, y):
        return self.find_root(x) == self.find_root(y)


def solve(N: int, M: int, X: "List[int]", Y: "List[int]", Z: "List[int]"):
    uf = UnionFind(N)
    for x, y, z in zip(X, Y, Z):
        uf.unite(x - 1, y - 1)

    print(sum(1 for i in range(N) if uf.is_root(i)))

    return


# Generated by 1.1.5 https://github.com/kyuridenamida/atcoder-tools
# (tips: You use the default template now. You can remove this line by using your custom template)
def main():
    def iterate_tokens():
        for line in sys.stdin:
            for word in line.split():
                yield word
    tokens = iterate_tokens()
    N = int(next(tokens))  # type: int
    M = int(next(tokens))  # type: int
    X = [int()] * (M)  # type: "List[int]"
    Y = [int()] * (M)  # type: "List[int]"
    Z = [int()] * (M)  # type: "List[int]"
    for i in range(M):
        X[i] = int(next(tokens))
        Y[i] = int(next(tokens))
        Z[i] = int(next(tokens))
    solve(N, M, X, Y, Z)


if __name__ == '__main__':
    main()