#![allow(unused_imports)]
#![allow(dead_code)]
#![allow(unused_mut)]
// #![allow(non_camel_case_types)]
use crate::rust::libs::util::{io::*,util::*,lower_bound::*,memoize::*};
use crate::rust::libs::math::{mod_int::*,prime_factor::*};
use crate::rust::libs::data_structure::{union_find::UF,segment_tree::*};
use crate::rust::libs::algebraic_structure::monoid::{plus::*,max::*,min::*};
use std::ops::Range;
use  std::collections::*;
use std::cmp::*;
const MOD: u32 = 1_000_000_007;
const INF:usize=1usize<<60;
const_mod!(P,MOD);

fn main(){
    input!{
        n:usize,m:usize,
        v:[(usize1,usize1);m],
    }
    let mut ans=0;
    for i in 0..m{
        let mut uf=UF::new(n);
        for j in 0..m{
            if i!=j{
                uf.unite(v[j].0, v[j].1);
            }
        }
        ans+=btoi!(!uf.same(v[i].0,v[i].1));
    }
    println!("{}",ans);
}




pub mod rust {

pub mod libs {

pub mod algebraic_structure {

pub mod monoid {

pub mod max {

use crate::rust::libs::algebraic_structure::monoid::monoid::Monoid;
use std::option::Option;
#[derive(Debug,Copy,Clone)]
pub struct Max<T>{
    val:Option<T>,
}
impl<T:Ord+Copy> Monoid for Max<T>{
    type MonoidType=T;
    fn new()->Self{
        Self{
            val:None,
        }
    }
    fn make(t:T)->Self{
        Self{
            val:Some(t),
        }
    }
    fn f(&self,t:&Self)->Self{
        if self.val.is_none() {*t}
        else if t.val.is_none() {*self}
        else{
            Self{
                val:Some(T::max(self.val.unwrap(),t.val.unwrap())),
            }
        }
    }
    fn get(self)->Option<T>{
        self.val
    }
}

}  // mod max

pub mod min {

use crate::rust::libs::algebraic_structure::monoid::monoid::Monoid;
use std::option::Option;
#[derive(Debug,Copy,Clone)]
pub struct Min<T>{
    val:Option<T>,
}
impl<T:Ord+Copy> Monoid for Min<T>{
    type MonoidType=T;
    fn new()->Self{
        Self{
            val:None,
        }
    }
    fn make(t:T)->Self{
        Self{
            val:Some(t),
        }
    }
    fn f(&self,t:&Self)->Self{
        if self.val.is_none() {*t}
        else if t.val.is_none() {*self}
        else{
            Self{
                val:Some(T::min(self.val.unwrap(),t.val.unwrap())),
            }
        }
    }
    fn get(self)->Option<T>{
        self.val
    }
}

}  // mod min

pub mod monoid {

pub trait Monoid{
    type MonoidType;
    fn new()->Self;
    fn make(t:Self::MonoidType)->Self;
    fn get(self)->Option<Self::MonoidType>;
    fn f(&self,t:&Self)->Self;
}

}  // mod monoid

pub mod plus {

use crate::rust::libs::algebraic_structure::monoid::monoid::Monoid;
use std::option::Option;
use std::ops::Add;
#[derive(Debug,Copy,Clone)]
pub struct Plus<T>{
    val:Option<T>,
}
impl<T:Add<T,Output=T>+Copy> Monoid for Plus<T>{
    type MonoidType=T;
    fn new()->Self{
        Self{
            val:None,
        }
    }
    fn make(t:T)->Self{
        Self{
            val:Some(t),
        }
    }
    fn f(&self,t:&Self)->Self{
        if self.val.is_none() {*t}
        else if t.val.is_none() {*self}
        else{
            Self{
                val:Some(self.val.unwrap()+t.val.unwrap()),
            }
        }
    }
    fn get(self)->Option<T>{
        self.val
    }
}

}  // mod plus

}  // mod monoid

}  // mod algebraic_structure

pub mod data_structure {

pub mod segment_tree {

use crate::rust::libs::algebraic_structure::monoid::monoid::Monoid;
use std::ops::Range;
pub struct SegmentTree<T>{
    n:usize,
    v:Vec<T>,
}

impl<T:Monoid+Copy+std::fmt::Debug> SegmentTree<T>where T::MonoidType:Copy{
    pub fn new(n:usize)->Self{
        Self{
            n:n,
            v:vec![T::new();n*2],
        }
    }
    pub fn make(v:&[T::MonoidType])->Self{
        let n=v.len();
        let mut tmp=vec![T::new();n*2];
        for i in 0..n{
            tmp[v.len()+i]=T::make(v[i]);
        }
        for i in (1..n).rev(){
            tmp[i]=tmp[i*2].f(&tmp[i*2+1]);
        }
        Self{
            n:n,
            v:tmp,
        }
    }
    pub fn get(&self,ran:Range<usize>)->Option<T::MonoidType>{
        let mut l=ran.start+self.n;
        let mut r=ran.end+self.n;
        let mut s=T::new();
        let mut t=T::new();
        while l<r{
            if (l&1)==1{s=s.f(&self.v[l]);l+=1;}
            if (r&1)==1{r-=1;t=self.v[r].f(&t);}
            l>>=1;r>>=1;
        }
        s.f(&t).get()
    }
    pub fn set(&mut self,t:usize,val:T::MonoidType){
        let mut tp=t+self.n;
        self.v[tp]=self.v[tp].f(&T::make(val));
        while tp>1{
            tp=tp>>1;
            self.v[tp]=self.v[tp*2].f(&self.v[tp*2+1]);
        }
    }
    pub fn out(self){
        println!("{:?}",self.v);
    }
}

}  // mod segment_tree

pub mod union_find {


pub struct UF{
    par:Vec<usize>,
    rank:Vec<usize>,
}
impl UF{
    pub fn new(n:usize)->UF{
        let mut v=vec![0;n];
        for i in 0..n{
            v[i]=i;
        }
        UF{
            par:v,
            rank:vec![1;n],
        }
    }
    pub fn root(&mut self,x:usize)->usize{
        if x==self.par[x]{
            x
        }else{
            let par=self.par[x];
            let res=self.root(par);
            self.par[x]=res;
            res
        }
    }
    pub fn same(&mut self,a:usize,b:usize)->bool{
        self.root(a)==self.root(b)
    }
    pub fn unite(&mut self,a:usize,b:usize)->bool{
        let ap=self.root(a);
        let bp=self.root(b);
        if ap==bp{
            return false;
        }
        if self.rank[ap]<self.rank[bp]{
            self.par[bp]=ap;
            self.rank[ap]+=self.rank[bp];
        }else{
            self.par[ap]=bp;
            self.rank[bp]+=self.rank[ap];
        }
        return true;
    }
    pub fn size(&mut self,a:usize)->usize{
        let ap=self.root(a);
        self.rank[ap]
    }
}

}  // mod union_find

}  // mod data_structure

pub mod math {

pub mod mod_int {

use std::ops::{Add,AddAssign,Sub,SubAssign,Mul,MulAssign,Div,DivAssign,Neg};
use std::str::FromStr;
use std::num::ParseIntError;
use std::iter::{Sum,Product};

pub fn inv_mod(a:u64,m:u64)->u64{
    let m=m as i64;
    let mut a=a as i64;
    let mut b=m as i64;
    let mut u=1i64;
    let mut v=0i64;
    while b>0 {
        let t=a/b;
        a-=t*b;
        u-=t*v;
        std::mem::swap(&mut a,&mut b);
        std::mem::swap(&mut u,&mut v);
    }
    let ans =(if u>=0{u%m}else{m+(u%m)%m})as u64;
    ans
}

pub trait Mod:Sized{
    fn m()->u32;
    fn m64()->u64;
    fn mi64()->i64;
}
#[macro_export]
macro_rules! const_mod{
    ($st:ident,$m:expr)=>{
        struct $st{}
        impl Mod for $st {
            fn m()->u32{$m}
            fn m64()->u64{$m as u64}
            fn mi64()->i64{$m as i64}
        }
        //const MAX=10000000;
        type Fp=ModInt<P>;
        //const fact_table:[Fp;MAX]=(0..MAX)
    }
}
pub struct ModInt<M:Mod>{a:u32,_p:std::marker::PhantomData<M>}

impl<M: Mod> ModInt<M>{
    pub fn new(a:u32)->Self{
        ModInt{a,_p:std::marker::PhantomData}
    }
    pub fn newu64(a:u64)->Self{
        ModInt{a:(a%M::m64())as u32,_p:std::marker::PhantomData}
    }
    pub fn newi64(a:i64)->Self{
        ModInt{a:((a%M::mi64()+M::mi64())%M::mi64()) as u32,_p:std::marker::PhantomData}
    }
    pub fn value(&self)->u32{self.a}
    pub fn pow(&self,p:u64)->Self{
        let mut exp=p;
        let mut now=*self;
        let mut ans=ModInt::new(1);
        while exp>0{
            if (exp&1)==1{ans*=now;}
            now*=now;
            exp>>=1;
        }
        ans
    }
    // pub fn comb(k:i32)->Self{
    //     Self::new(k)
    // }
    pub fn inv(&self)->Self{Self::new(inv_mod(self.a as u64,M::m64())as u32)}
}

impl<M:Mod>Clone for ModInt<M>{
    fn clone(&self)->Self{ModInt::new(self.a)}
}
impl<M:Mod>Copy for ModInt<M>{}
impl<M:Mod>From<i64>for ModInt<M>{
    fn from(i:i64)->Self{Self::newi64(i)}
}
impl<M:Mod>From<i32>for ModInt<M>{
    fn from(i:i32)->Self{Self::newi64(i as i64)}
}

impl<M:Mod>FromStr for ModInt<M>{
    type Err = ParseIntError;
    fn from_str(i:&str)->Result<Self, Self::Err>{
        let res=i.parse::<i64>()?;
        Ok(Self::newi64(res))
    }
}
impl<M:Mod>Sum for ModInt<M>{
    fn sum<I>(iter: I) -> Self where I: Iterator<Item = Self>,{
        iter.fold(Self::new(0), |b, i| b + i)
    }
}

impl<M:Mod>Product for ModInt<M>{
    fn product<I>(iter: I) -> Self where I: Iterator<Item = Self>,{
        iter.fold(Self::new(0), |b, i| b * i)
    }
}
impl<M:Mod>Add for ModInt<M>{
    type Output=Self;
    fn add(self,rhs:Self)->Self{
        let a=self.a+rhs.a;
        ModInt::new(if a>=M::m(){a-M::m()}else{a})
    }
}
impl<M:Mod>Sub for ModInt<M>{
    type Output=Self;
    fn sub(self,rhs:Self)->Self{
        ModInt::new(if self.a<rhs.a{M::m()+self.a-rhs.a}else{self.a-rhs.a})
    }
}
impl<M:Mod>Mul for ModInt<M>{
    type Output=Self;
    fn mul(self,rhs:Self)->Self{
        ModInt::newu64(self.a as u64*rhs.a as u64)
    }
}
impl<M:Mod>Div for ModInt<M>{
    type Output=Self;
    fn div(self,rhs:Self)->Self{
        self*rhs.inv()
    }
}
impl<M:Mod>Neg for ModInt<M> {
    type Output = Self;
    fn neg(self) -> Self {
        ModInt::new(M::m()-self.value())
    }
}
impl<M:Mod>AddAssign for ModInt<M>{fn add_assign(&mut self,rhs:Self){*self=*self+rhs;}}
impl<M:Mod>SubAssign for ModInt<M>{fn sub_assign(&mut self,rhs:Self){*self=*self-rhs;}}
impl<M:Mod>MulAssign for ModInt<M>{fn mul_assign(&mut self,rhs:Self){*self=*self*rhs;}}
impl<M:Mod>DivAssign for ModInt<M>{fn div_assign(&mut self,rhs:Self){*self=*self/rhs;}}

impl<M:Mod>std::fmt::Debug for ModInt<M>{
    fn fmt(&self,f:&mut std::fmt::Formatter)->Result<(),std::fmt::Error>{
        write!(f,"M{}",self.a)
    }
}
impl<M:Mod> std::fmt::Display for ModInt<M> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        self.value().fmt(f)
    }
}

// impl<M:Mod> Sum for ModInt<M>{
//     fn sum<I>(iter: I) -> ModInt<M>
//     where
//         I: Iterator<Item = ModInt<M>>,
//     {
//         iter.fold(Self::new(0), |b, i| b + i)
//     }
// }

}  // mod mod_int

pub mod prime_factor {

use std::collections::BTreeMap;
pub fn prime_factor(n:usize)->BTreeMap<usize,usize>{
    let mut ret:BTreeMap<usize,usize>=BTreeMap::<usize,usize>::new();
    let mut m=n;
    let mut i=2;
    while i*i<=m {
        while m%i==0{
            let tmp=ret.entry(i).or_insert(0);
            *tmp+=1;
            m/=i;
        }
        i+=1;
    }
    ret
}

}  // mod prime_factor

}  // mod math

pub mod util {

pub mod io {

#[macro_export]
macro_rules! input {
    (source = $s:expr, $($r:tt)*) => {
        let mut iter = $s.split_whitespace();
        input_inner!{iter, $($r)*}
    };
    ($($r:tt)*) => {
        let mut s = {
            use std::io::Read;
            let mut s = String::new();
            std::io::stdin().read_to_string(&mut s).unwrap();
            s
        };
        let mut iter = s.split_whitespace();
        input_inner!{iter, $($r)*}
    };
}

#[macro_export]
macro_rules! input_inner {
    ($iter:expr) => {};
    ($iter:expr, ) => {};

    ($iter:expr, $var:ident : $t:tt $($r:tt)*) => {
        let $var = read_value!($iter, $t);
        input_inner!{$iter $($r)*}
    };
    ($iter:expr, mut $var:ident : $t:tt $($r:tt)*) => {
        let mut $var = read_value!($iter, $t);
        input_inner!{$iter $($r)*}
    };
}

#[macro_export] 
macro_rules! read_value {
    ($iter:expr, [tree;$len:expr]) => {
        {
            let mut g:Vec<Vec<usize>>=vec![Vec::new();$len];
            for (s,t) in read_value!($iter,[(usize1,usize1);$len-1]){
                g[s].push(t);
                g[t].push(s);
            }
            g
        }
    };
    ($iter:expr, [graph;$vertex:expr,$edge:expr]) => {
        {
            let mut g:Vec<Vec<usize>>=vec![Vec::new();$vertex];
            for (s,t) in read_value!($iter,[(usize1,usize1);$edge]){
                g[s].push(t);
                g[t].push(s);
            }
            g
        }
    };
    ($iter:expr, [directed;$vertex:expr,$edge:expr]) => {
        {
            let mut g:Vec<Vec<usize>>=vec![Vec::new();$vertex];
            for (s,t) in read_value!($iter,[(usize1,usize1);$edge]){
                g[s].push(t);
            }
            g
        }
    };
    ($iter:expr, ( $($t:tt),* )) => {
        ( $(read_value!($iter, $t)),* )
    };

    ($iter:expr, [ $t:tt ; $len:expr ]) => {
        (0..$len).map(|_| read_value!($iter, $t)).collect::<Vec<_>>()
    };

    ($iter:expr, chars) => {
        read_value!($iter, String).chars().collect::<Vec<char>>()
    };

    ($iter:expr, bytes) => {
        read_value!($iter, String).into_bytes()
    };

    ($iter:expr, usize1) => {
        read_value!($iter, usize) - 1
    };
    ($iter:expr, Fp) => {
        Fp::newi64(read_value!($iter,i64))
    };

    ($iter:expr, $t:ty) => {
        $iter.next().unwrap().parse::<$t>().expect("Parse error")
    };
}


}  // mod io

pub mod lower_bound {

pub trait LowerBound<T> {
    fn lower_bound(&self,v:T)->usize;
}

impl<T:Ord> LowerBound<T> for Vec<T>{
    fn lower_bound(&self,x:T)->usize{
        let mut l:usize=0;
        let mut r:usize=self.len()+1;
        while r-l>1{
            let m=(l+r)/2;
            if self[m-1]<x {l=m;}
            else {r=m;}
        }
        r-1
    }
}
pub trait UpperBound<T> {
    fn upper_bound(&self,v:T)->usize;
}

impl<T:Ord> UpperBound<T> for Vec<T>{
    fn upper_bound(&self,x:T)->usize{
        let mut l:usize=0;
        let mut r:usize=self.len()+1;
        while r-l>1{
            let m=(l+r)/2;
            if self[m-1]<=x {l=m;}
            else {r=m;}
        }
        r-1
    }
}

}  // mod lower_bound

pub mod memoize {

#[macro_export]
macro_rules! make_vec{
    ( $val:expr , $head:expr)=>{
        vec![$val;$head]
    };
    ( $val:expr , $head:expr , $($tail:expr),+ )=>{
        vec![make_vec!($val,$($tail),+);$head]
    };
}

#[macro_export]
macro_rules! access{
    ($v:expr,$tail:expr)=>{
        $v[$tail]
    };
    ($v:expr,$tail:expr,)=>{
        $v[$tail]
    };
    ($v:expr,$($r:expr),+,$tail:expr)=>{
        access!($v,$($r),+)[$tail]
    };
}
#[macro_export]
macro_rules! make_type_vec{
    ($v:ty,$tail:expr)=>{
        Vec<$v>
    };
    ($v:ty,$tail:expr,)=>{
        Vec<$v>
    };
    ($v:ty,$($r:expr),+,$tail:expr)=>{
        Vec<make_type_vec!($v,$($r),+)>
    };
}
#[macro_export]
macro_rules! access_local{
    ($v:expr,$($r:expr),+)=>{
        v.with()
    };
}
#[macro_export]
macro_rules! memoized{
    (fn $func:tt ($($val:tt [$r:expr]:$typ:ty),*)->$ret:ty{$($t:tt)*} ( $($st:expr),* );)=>{
        {
            use std::cell::RefCell;
            use std::borrow::BorrowMut;
            fn $func($($val : $typ),*)->$ret{
                thread_local! {
                    pub static MEMO :RefCell<make_type_vec!(Option<$ret>,$($r),*)>= RefCell::new(make_vec!(None,$($r),*));
                }                
                let __function__=|$($val : $typ),*|->$ret {$($t)*};
                
                if MEMO.with(|v|access!(v.borrow(),$($val),*).is_none()){
                    MEMO.with(|v|access!(v.borrow_mut(),$($val),*)=Some(__function__($($val),*)));
                }
                MEMO.with(|v|access!(v.borrow(),$($val),*).clone()).unwrap()
            }
            $func($($st),*)
        }
    };
    (fn $func:tt ($($val:tt [$r:expr]:$typ:ty),*)[$($cap_val:tt : $cap_typ:ty),*]->$ret:ty{$($t:tt)*} $func2:tt( $($st:expr),* );)=>{
        {
            use std::cell::RefCell;
            assert!(stringify!($func)==stringify!($func2));
            fn __function2__($($val : $typ),*,$($cap_val:$cap_typ),*)->$ret{
                thread_local! {
                    pub static MEMO :RefCell<make_type_vec!(Option<$ret>,$($r),*)>= RefCell::new(make_vec!(None,$($r),*));
                }
                let $func=|$($val : $typ),*|->$ret {__function2__($($val),*,$($cap_val),*)};
                let __function__=|$($val : $typ),*,$($cap_val:$cap_typ),*|->$ret {
                    $($t)*
                };
                if MEMO.with(|v|access!(v.borrow(),$($val),*).is_none()){
                    MEMO.with(|v|access!(v.borrow_mut(),$($val),*)=Some(__function__($($val),*,$($cap_val),*)));
                }
                MEMO.with(|v|access!(v.borrow(),$($val),*).clone()).unwrap()
            }
            __function2__($($st),*)
        }
    };
}

}  // mod memoize

pub mod util {

use std::ops::Add;
pub trait CumSum<T> {
    fn cumsum(&self)->Vec<T>;
}

impl<T:Add<Output=T>+Default+Copy> CumSum<T> for Vec<T>{
    fn cumsum(&self)->Vec<T>{
        let mut ret=Vec::new();
        ret.push(T::default());
        for i in 0..self.len(){
            ret.push(ret[i]+self[i]);
        }
        ret
    }
}


pub fn type_of<T>(_: T) -> String{
    let a = std::any::type_name::<T>();
    return a.to_string();
}

#[macro_export]
macro_rules! btoi{
    ( $val:expr)=>{
        if $val{1}else{0}
    };
}

pub trait Isomorphism<T> {
    fn isomorphism(&self)->Vec<T>;
}

use std::ops::{Rem,Div,Mul};
use std::default::Default;
pub fn gcd<T:Rem<Output=T>+Eq+Default+Copy>(a:T,b:T)->T{
    if b==T::default(){a}else{gcd(b, a % b)}
}
pub fn lcm<T:Rem<Output=T>+Eq+Default+Copy+Div<Output=T>+Mul<Output=T>>(a:T,b:T)->T{
    a/gcd(a,b)*b
}

use crate::rust::libs::util::lower_bound::*;
pub trait Lis<T> {
    fn lis(&self)->Vec<T>;
}

impl<T:Ord+Copy> Lis<T> for Vec<T>{
    fn lis(&self)->Vec<T>{
        let mut ret=Vec::<T>::new();
        for i in 0..self.len(){
            let t=ret.lower_bound(self[i]);
            if t==ret.len(){ret.push(self[i]);}
            else {ret[t]=self[i];}
        }
        ret
    }
}

use std::convert::TryInto;
pub trait Ac<T>{
    fn ac(&self,idx:T)->T;
}
impl<T> Ac<T> for Vec<T>where T:TryInto<usize>+Copy{
    fn ac(&self,idx:T)->T{
        match idx.try_into(){
            Ok(i)=>self[i],
            Err(_e)=>{
                panic!("cannot convert to usize");
            }
        }
    }
}

}  // mod util

}  // mod util

}  // mod libs

}  // mod rust



Measured memory_bytes: 2120