#include "bits/stdc++.h"
using namespace std;

//#define int long long
#define DBG 1
#define dump(o) if(DBG){cerr<<#o<<" "<<(o)<<" ";}
#define dumpl(o) if(DBG){cerr<<#o<<" "<<(o)<<endl;}
#define dumpc(o) if(DBG){cerr<<#o; for(auto &e:(o))cerr<<" "<<e;cerr<<endl;}
#define rep(i,a,b) for(int i=(a);i<(b);i++)
#define rrep(i,a,b) for(int i=(b)-1;i>=(a);i--)
#define all(c) begin(c),end(c)
const int INF = sizeof(int) == sizeof(long long) ? 0x3f3f3f3f3f3f3f3fLL : 0x3f3f3f3f;
const int MOD = (int)(1e9 + 7);
template<class T> bool chmax(T &a, const T &b) { if (a < b) { a = b; return true; } return false; }
template<class T> bool chmin(T &a, const T &b) { if (a > b) { a = b; return true; } return false; }

using Weight = int;
class Edge {
public:
	int s; //source
	int d; //destination
	Weight w; //weight
	Edge(int s = 0, int d = 0, Weight w = INF) : s(s), d(d), w(w) {};
	bool operator<(const Edge &e)const { return w < e.w; }
};
using Edges = vector<Edge>;
using Graph = vector<Edges>;
using Array = vector<Weight>;
using Matrix = vector<Array>;
//辺の挿入
//G[s].emplace_back(s, d, w);
//G[d].emplace_back(d, s, w); 無向木は更に

//未探索, 探索中, 探索済
enum { WHITE, GRAY, BLACK };
//単一始点最短路(負閉路なし) Single Source Shortest Path
//Dijkstra O((E+V)logV)
//dist: 始点から各頂点までの最短距離 distance
//戻り値: 最短路木の親頂点(根は-1)
vector<int> dijkstra(const Graph &g, int s, vector<Weight> &dist) {
	using State = tuple<Weight, int, int>; //始点からの最小コスト 子 親
	priority_queue<State, vector<State>, greater<State>> PQ;
	int n = g.size();
	vector<int> color(n, WHITE);
	vector<int> prev(n, -1);
	dist.assign(n, INF);
	dist[s] = 0; PQ.emplace(0, s, -1); color[s] = GRAY; //始点
	while (PQ.size()) {
		Weight d; int v, u;
		tie(d, v, u) = PQ.top(); PQ.pop(); //distが最小となるStateを取り出し
		if (dist[v] < d)continue; //すでに最短でなければ無視
		color[v] = BLACK;
		prev[v] = u;
		for (auto &e : g[v]) { //隣接する頂点が存在する限り		
			if (color[e.d] == BLACK)continue; //探索済は無視
			if (dist[e.d] > dist[v] + e.w) {
				dist[e.d] = dist[v] + e.w;
				PQ.emplace(dist[e.d], e.d, v);
				color[e.d] = GRAY;
			}
		}
	}
	return prev;
}

//単一始点最短路(負閉路あり) Single Source Shortest Path
//Bellman Ford O(VE)
//dist: 始点から各頂点までの最短距離
//戻り値: 最短路木の親頂点, 負閉路なし:true あり:false
pair<vector<int>, bool> bellman_ford(const Graph &g, int s, vector<Weight> &dist) {
	int n = g.size();
	vector<int> prev(n, -1);
	Edges es; for (int i = 0; i < n; i++) for (auto &e : g[i]) es.emplace_back(e);
	dist.assign(n, INF); dist[s] = 0;
	bool negative_cycle = false;
	for (int i = 0;; i++) {
		bool update = false;
		for (auto &e : es) {
			if (dist[e.s] != INF && dist[e.d] > dist[e.s] + e.w) {
				dist[e.d] = dist[e.s] + e.w;
				prev[e.d] = e.s;
				update = true;
			}
		}
		if (!update) break;
		if (i > n) { negative_cycle = true; break; }
	}
	return make_pair(prev, !negative_cycle);
}

//経路復元
//始点から終点までの経路を取得
//終点から親を再帰的に辿り始点に着くまでの経路を反転している
//存在しない場合の戻り値: vector<int>()
vector<int> get_path(int s, int g, vector<int> prev) {
	vector<int> path;
	path.emplace_back(g);
	for (int i = g; i != s; ) {
		if (i == -1)return vector<int>();
		path.emplace_back(i = prev[i]);
	}
	reverse(path.begin(), path.end());
	return path;
}
////経路復元(Warshall Floyd)
////存在しない場合の戻り値: ?
//vector<int> get_path(int s, int g, vector<vector<int>> next) {
//	vector<int> path;
//	for (int i = s; i != g; i = next[i][g])
//		path.emplace_back(i);
//	path.emplace_back(g);
//	return path;
//}
//
////全点対間最短経路 All Pairs Shortest Path バグあり
////Warshall Floyd O(V^3)
////戻り値:
//// next[i][j]: iからjへの最短経路におけるiの1つ後の点 辞書順最小
//// 負閉路なし:true あり:false
//pair<vector<vector<int>>, bool> warshall_floyd(const Graph &g, Matrix &dist) {
//	int n = g.size();
//	dist.assign(n, Array(n, INF));
//	for (int i = 0; i < n; i++) dist[i][i] = 0;
//	for (int i = 0; i < n; i++)
//		for (auto e : g[i])
//			dist[e.s][e.d] = min(dist[e.s][e.d], e.w);
//	vector<vector<int>> next(n, vector<int>(n));
//	for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)next[i][j] = j;
//	for (int k = 0; k < n; k++)
//		for (int i = 0; i < n; i++)
//			for (int j = 0; j < n; j++) {
//				if (dist[i][k] == INF || dist[k][j] == INF)continue;
//				if (dist[i][j] > dist[i][k] + dist[k][j]) {
//					dist[i][j] = dist[i][k] + dist[k][j];
//					next[i][j] = next[i][k];
//				}
//				else if (k != i && dist[i][k] + dist[k][j] == dist[i][j])
//					next[i][j] = min(next[i][j], next[i][k]); //辞書順最小
//			}
//	bool negative_cycle = false;
//	for (int i = 0; i < n; i++)
//		if (dist[i][i] != 0)
//			negative_cycle = true;
//	return make_pair(next, !negative_cycle);
//}

//全点対間最短経路 All Pairs Shortest Path
//Warshall Floyd O(V^3)
//戻り値: 負閉路なし:true あり:false
bool warshall_floyd(const Graph &g, Matrix &dist) {
	int n = g.size();
	dist.assign(n, Array(n, INF));
	for (int i = 0; i < n; i++) dist[i][i] = 0;
	for (int i = 0; i < n; i++)
		for (auto e : g[i])
			dist[e.s][e.d] = min(dist[e.s][e.d], e.w);
	for (int k = 0; k < n; k++)
		for (int i = 0; i < n; i++)
			for (int j = 0; j < n; j++) {
				if (dist[i][k] == INF || dist[k][j] == INF)continue;
				dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
			}
	bool negative_cycle = false;
	for (int i = 0; i < n; i++)
		if (dist[i][i] != 0)
			negative_cycle = true;
	return !negative_cycle;
}

//トポロジカルソート O(E+V)
//入次数が0の点と辺を取り除きながらretに突っ込む
vector<int> topological_sort(const Graph &g) {
	int n = g.size(), k = 0;
	vector<int> ord(n), indeg(n); //入次数
	for (auto &es : g) for (auto &e : es) indeg[e.d]++;
	queue<int> Q;
	for (int i = 0; i < n; i++) if (indeg[i] == 0) Q.push(i);
	while (!Q.empty()) {
		int v = Q.front(); Q.pop(); ord[k++] = v;
		for (auto &e : g[v]) if (--indeg[e.d] == 0) Q.push(e.d);
	}
	return *max_element(indeg.begin(), indeg.end()) == 0 ? ord : vector<int>();
}

//木の直径
Edge diameter(const Graph &g, int s = 0) {
	vector<Weight> dist;
	dijkstra(g, s, dist);
	int n = g.size(), u = -1, v = -1;
	for (int i = 0; i < n; i++) if (dist[i] != INF && (u == -1 || dist[i] > dist[u])) u = i;
	dijkstra(g, u, dist);
	for (int i = 0; i < n; i++) if (dist[i] != INF && (v == -1 || dist[i] > dist[v])) v = i;
	Weight d = dist[v];
	if (u > v) swap(u, v);
	return Edge(u, v, d);
}

//#include "UnionFind.cpp"
////最小全域木
////Kruskal O(|E|log|E|)
//pair<Weight, Edges> kruskal(const Graph &g) {
//	UnionFind UF(g.size());
//	Edges es;
//	for (int i = 0; i < g.size(); i++) for (auto &e : g[i]) es.emplace_back(e);
//	sort(es.begin(), es.end());
//	Weight total = 0;
//	Edges T;
//	for (auto &e : es) if (!UF.same(e.s, e.d)) {
//		T.push_back(e); total += e.w; UF.unite(e.s, e.d);
//	}
//	return make_pair(total, T);
//}
//
////最小全域木
////Prim O(ElogV)
////rから到達可能な頂点が対象
////Edgeを bool operator<(const Edge &e)const { return w > e.w; } にする必要がある
//pair<Weight, Edges> prim(const Graph &g, int r = 0) {
//	Edges T; Weight total = 0; vector<int> v(g.size());
//	priority_queue <Edge> q;
//	q.emplace(-1, r, 0);
//	while (q.size()) {
//		Edge e = q.top(); q.pop();
//		if (v[e.d]) continue;
//		v[e.d] = true;
//		total += e.w; if (e.s != -1) T.emplace_back(e);
//		for (auto &f : g[e.d]) if (!v[f.d]) q.emplace(f);
//	}
//	return make_pair(total, T);
//}

signed main() {
	int N, M; cin >> N >> M;
	Graph G(N);
	rep(i, 0, M) {
		int a, b, c; cin >> a >> b >> c; a--, b--;
		G[a].emplace_back(a, b, c);
		G[b].emplace_back(b, a, c);
	}
	set<pair<int, int>> st;

	rep(i, 0, N) {
		vector<Weight> dist;
		vector<int> prev = dijkstra(G, i, dist);
		rep(j, 1, N) {
			vector<int> path = get_path(i, j, prev);
			rep(k, 1, path.size()) {
				int s = path[k], d = path[k - 1];
				if (s > d)swap(s, d);
				st.insert(make_pair(s, d));
			}
		}
	}
	cout << M - st.size() << endl;
	return 0;
}

Measured memory_bytes: 256