首先我们要明确并查集的作用:快速判断两个数是否同一集合与快速合并两个集合成一个集合并求出一些节点之间的关系,根据的就是树的特点:每个孩子节点有且仅有一个父节点。这样就用数组记录父节点就还(根就记录自己),合并操作就是合并两个根节点,这儿有个优化就是启发式合并:根记录孩子个数,并把个数少的并到个数大的上面。 

不过我们有其他大招:路径压缩,即我们每次查询的时候都把一条线上的所有节点连接到祖先节点,这样每次查找都很快(并查集在路径压缩之后的时间复杂度是阿克曼函数)。依据就是我们只需要知道多个孩子节点的祖先是否一致就能判断是否一个集合,不需要知道树上的结构。我们的权值则是一般记录此节点与父节点的关系,只要满足这个关系可以传递我们就可以模仿矢量计算来处理权值。 

  这儿我们要明确是有三种关系的:两者同类,吃父节点,被父节点吃,所以权值可以用0,1,2表示 

  注意有个关键就是当我们知道x与祖先x1的关系,y与祖先y1的关系,x与y的关系时,求x1与y1的关系时,使用矢量 计算

/* ***********************************************
Author :PeterBishop
Created Time :Sun 17 Feb 2019 21:43:16 CST
File Name :t.cpp
Origin :P O J 1182
************************************************ */

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <string>
#include <cmath>
#include <cstdlib>
#include <vector>
#include <queue>
#include <set>
#include <map>

using namespace std;
const int maxn=50001;
struct node{
int f;
int s;
}A[maxn];

void init(int x){
for(int i=1;i<=x;i++){
A[i].f=i;
A[i].s=0;
}
}

int find(int x){
if(x==A[x].f)
return A[x].f;
int y=find(A[x].f);
A[x].s=(A[x].s+A[A[x].f].s)%3;
return A[x].f=y;
}

int join(int t,int a,int b)
{
int r1=find(a);
int r2=find(b);
if(r1==r2){
if((A[a].s-A[b].s+3)%3==t-1)
return 0;
return 1;
}
A[r1].f=r2;
A[r1].s=(-A[a].s+t-1+A[b].s+3)%3;
return 0;
}


int main()
{
freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int n,k,ans;
int typ,smt1,smt2;
scanf("%d %d",&n,&k);
init(n);
ans=0;
for(int i=0; i<k; i++)
{
scanf("%d %d %d",&typ,&smt1,&smt2);
if(smt1==smt2&&typ==2)
ans++;
else if(smt1>n||smt2>n)
ans++;
else
ans+=join(typ,smt1,smt2);
}
printf("%d\n",ans);
return 0;
}