#include <algorithm>
#include <cassert>
#include <cctype>
#include <climits>
#include <cmath>
#include <complex>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <numeric>
#include <queue>
#include <random>
#include <set>
#include <stack>
#include <string>
#include <tuple>
#include <vector>
#define rep(i, n) for (int i = 0; i < (int)(n); ++i)
//#define cerr if(false) cerr
#ifdef DEBUG
#define show(...) cerr << #__VA_ARGS__ << " = ", debug(__VA_ARGS__);
#else
#define show(...) 42
#endif
using namespace std;
using ll = long long;
using pii = pair<int, int>;

class UnionFind{
    private:
    int sz;
    vector<int> par,size_;
    public:
    UnionFind(){}
    UnionFind(int node_size):sz(node_size),par(sz),size_(sz,1){
        iota(par.begin(),par.end(),0);
    }
    int find(int x){
        if(par[x]==x)return x;
        else return par[x] = find(par[x]);
    }
    void unite(int x,int y){
        x = find(x),y = find(y);
        if(x==y)return;
        if(size_[x]<size_[y])swap(x,y);
        par[y] = x;
        size_[x] += size_[y];
    }
    int size(int x){
        x = find(x);
        return size_[x];
    }
};

template <typename T, typename S>
ostream& operator<<(ostream& os, pair<T, S> a) {
    os << '(' << a.first << ',' << a.second << ')';
    return os;
}
template <typename T>
ostream& operator<<(ostream& os, vector<T> v) {
    for (auto x : v) os << x << ' ';
    return os;
}
void debug() {
    cerr << '\n';
}
template <typename H, typename... T>
void debug(H a, T... b) {
    cerr << a;
    if (sizeof...(b)) cerr << ", ";
    debug(b...);
}
template<typename T> class dinic{
private:
    int V;
    vector<int> level, iter;
    void bfs(int s){
        fill(level.begin(), level.end(),-1);
        queue<int> que;
        level[s] = 0;
        que.push(s);
        while(!que.empty()){
            int v = que.front();
            que.pop();
            for(auto &e : G[v]){
                if(e.cap > 0 and level[e.to] < 0){
                    level[e.to] = level[v] + 1;
                    que.push(e.to);
                }
            }
        }
    }
    T dfs(int v, int t , T f){
        if(v == t)return f;
        for(int& i = iter[v]; i < (int) G[v].size(); i++){
            edge&e = G[v][i];
            if(e.cap > 0 and level[v] < level[e.to]){
                T d = dfs(e.to, t, min(f,e.cap));
                if(d > 0){
                    e.cap -= d;
                    G[e.to][e.rev].cap += d;
                    return d;
                }
            }
        }
        return 0;
    }
public:
    struct edge{
        int to;
        T cap;
        int rev;
    };
    vector<vector<edge>>G;
    dinic(int node_size) : V(node_size), level(V), iter(V), G(V){}
    void add_edge(int from, int to, T cap){
        G[from].push_back((edge){to,cap,(int)G[to].size()});
        G[to].push_back((edge){from,0,(int)G[from].size()-1});
    }
    T solve(int s, int t){
        T flow = 0;
        for(;;){
            bfs(s);
            if(level[t] < 0)return flow;
            fill(iter.begin(),iter.end(),0);
            T f;
            while((f = dfs(s,t,numeric_limits<T>::max())) > 0){
                flow += f;
            }
        }
    }
};
int main(){
    int N,M;
    cin>>N>>M;
    int a,b;
    dinic<int> d(2 * N + 2);
    int s = 2 * N;
    int t = 2 * N + 1;
    UnionFind uf(2 * N);
    int INF = 1 << 30;
    for(int i=0;i<M;i++){
        cin >> a >> b; a--;b--;
        d.add_edge(a, b + N, 1);
        uf.unite(a, b + N);
    }
    for(int i=0;i<N;i++){
        d.add_edge(s, i, 1);
        d.add_edge(i + N, t, 1);
    }
    set<int> st;
    for(int i=0;i<2*N;i++){
        if(uf.size(i) > 1){
            st.insert(uf.find(i));
        }
    }
    int res1 = M - d.solve(s, t);
    int res2 = M - st.size();
    cout << res1 <<" "<<res2<<endl;
}


Measured memory_bytes: 3320