// グラフ

// 隣接リスト

// cycle を検出 ⇔ 強連結成分の個数をチェック

#include<vector>
#include<queue>
#include<functional>
#include<cfloat>
#include<tuple>
#include<set>
#include<stack>

const double INF = DBL_MAX / 3.0;

// Edge
class Edge {
public:
    Edge() {}
    Edge(const int f, const int t, const double w) :from_(f), to_(t), weight_(w) {}
    int from() const {return from_;}
    int to() const {return to_;}
    double weight() const {return weight_;}
    bool operator<(const Edge& e) const {
        if(from() != e.from()) return from() < e.from();
        if(to() != e.to()) return to() < e.to();
        return weight() < e.weight();
    }
private:
    int from_, to_;
    double weight_;
};

// Graph
class Graph {
public:
    explicit Graph(int s) :size_(s) {edge_.resize(s);}

    int size() const {return size_;}
    const std::vector<Edge>& edge(const int v) const {return edge_[v];}

    void add(const Edge& e);
    void add(const int from, const int to, const double weight);
    double Dijkstra(const int start, const int goal) const;
    double BellmanFord(const int start, const int goal) const;
    bool cut_vertex(const int v, const std::set<int>& visited) const;
    std::vector<std::vector<int>> Kosaraju() const;
private:
    void cut_vertex_reachability(const int from, std::set<int>& visited) const;
    void Kosaraju_dfs_postorder(const int from, std::vector<bool>& visited, std::stack<int>& order) const;
    std::vector<int> Kosaraju_dfs_decompose(const std::vector<std::vector<Edge>>& reverse_edge, const int from, std::vector<bool>& visited) const;

    int size_;
    std::vector<std::vector<Edge>> edge_;
};

// implements
void Graph::add(const Edge& e) {edge_[e.from()].push_back(e);}

void Graph::add(const int from, const int to, const double weight) {edge_[from].push_back(Edge(from, to, weight));}

double Graph::Dijkstra(const int start, const int goal) const {
    enum {COST, VERTEX};
    typedef std::tuple<double, int> Node;

    std::vector<int> visited(size(), 0);
    std::priority_queue<Node, std::vector<Node>, std::greater<Node>> q;
    q.push(Node(0, start));
    while(!q.empty()) {
        double cost = std::get<COST>(q.top());
        int from = std::get<VERTEX>(q.top());
        q.pop();
        if(from == goal) return cost;
        if(visited[from]) continue;
        visited[from] = 1;
        for(const auto& e: edge(from)) q.push(Node(cost + e.weight(), e.to()));
    }
    return INF;
}

double Graph::BellmanFord(const int start, const int goal) const {
    std::vector<double> distance(size(), INF);
    distance[start] = 0;
    for(int step = 0; step <= size(); ++step) {
        for(int from = 0; from < size(); ++from) {
            for(const auto& e: edge(from)) {
                if(distance[from] + e.weight() >= distance[e.to()]) continue;
                if(step == size()) return INF;
                distance[e.to()] = distance[from] + e.weight();
            }
        }
    }
    return distance[goal];
}

bool Graph::cut_vertex(const int v, const std::set<int>& visited = std::set<int>()) const {
    std::set<int> dup = visited;
    for(const auto& e: edge(v)) {
        if(dup.count(e.to())) continue;
        cut_vertex_reachability(e.to(), dup);
        return dup.size() != size();
    }
    return dup.size() != size();
}
void Graph::cut_vertex_reachability(const int from, std::set<int>& visited) const {
    visited.insert(from);
    for(const auto& e: edge(from)) if(!visited.count(e.to())) cut_vertex_reachability(e.to(), visited);
}

std::vector<std::vector<int>> Graph::Kosaraju() const {
    std::vector<std::vector<int>> result;

    std::vector<bool> visited(size(), false);
    std::stack<int> order;
    for(int v = 0; v < size(); ++v) if(!visited[v]) Kosaraju_dfs_postorder(v, visited, order);

    std::vector<std::vector<Edge>> reverse_edge(size(), std::vector<Edge>());
    for(int v = 0; v < size(); ++v) for(const auto& e: edge(v)) reverse_edge[e.to()].push_back(Edge(e.to(), e.from(), e.weight()));

    visited = std::vector<bool>(size(), false);
    while(!order.empty()) {
        int v = order.top();
        order.pop();
        if(visited[v]) continue;
        result.push_back(Kosaraju_dfs_decompose(reverse_edge, v, visited));
    }
    return result;
}
void Graph::Kosaraju_dfs_postorder(const int from, std::vector<bool>& visited, std::stack<int>& order) const {
    visited[from] = true;
    for(const auto& e: edge(from)) if(!visited[e.to()]) Kosaraju_dfs_postorder(e.to(), visited, order);
    order.push(from);
}
std::vector<int> Graph::Kosaraju_dfs_decompose(const std::vector<std::vector<Edge>>& reverse_edge, const int from, std::vector<bool>& visited) const {
    std::vector<int> result;
    result.push_back(from);
    visited[from] = true;
    for(const auto& e: reverse_edge[from]) {
        if(visited[e.to()]) continue;
        std::vector<int> add = Kosaraju_dfs_decompose(reverse_edge, e.to(), visited);
        result.insert(result.end(), add.begin(), add.end());
    }
    return result;
}

////////////////////////////////////////////////////////////////////////////////

// 無向木の最遠点対
//     ある頂点から最も遠い点を u とし, u から最も遠い点を v とすると (u, v) が最遠点対
// 有向だとダメ && cycle があると死ぬ
std::tuple<double, int> diameter_of_unoriented_tree_dfs(const Graph& g, const int previous, const int current) {
    std::tuple<double, int> result(0.0, current);
    for(const auto& e: g.edge(current)) {
        if(e.to() == previous) continue;
        double w; int v;
        std::tie(w, v) = diameter_of_unoriented_tree_dfs(g, current, e.to());
        w += e.weight();
        if(std::get<0>(result) < w) result = std::make_tuple(w, v);
    }
    return result;
}
double diameter_of_unoriented_tree(const Graph& g) {
    double w; int v;
    std::tie(w, v) = diameter_of_unoriented_tree_dfs(g, -1, 0);
    std::tie(w, v) = diameter_of_unoriented_tree_dfs(g, -1, v);
    return w;
}

// 連結グラフの cut vertex の集合を返す
//     Algorithmics: theory and practice, Gilles Brassard and Paul Bratley
void cut_vertex_dfs(const Graph& g, const int prev, const int current, int& timer, std::vector<int>& prenum, std::vector<int>& parent, std::vector<int>& lowest) {
    prenum[current] = timer;
    lowest[current] = timer;
    parent[current] = prev;
    for(const auto& e: g.edge(current)) {
        if(prenum[e.to()] == -1) {
            cut_vertex_dfs(g, current, e.to(), ++timer, prenum, parent, lowest);
            lowest[current] = std::min(lowest[current], lowest[e.to()]);
        } else if(e.to() != prev) {
            lowest[current] = std::min(lowest[current], prenum[e.to()]);
        }
    }
}
std::set<int> cut_vertex(const Graph& g) {
    std::vector<int> prenum(g.size(), -1);
    std::vector<int> parent(g.size(), -1);
    std::vector<int> lowest(g.size(), 1000000000);
    int timer = 0;
    cut_vertex_dfs(g, -1, 0, timer, prenum, parent, lowest);
    std::set<int> result;
    int count = 0;
    for(const auto& p: parent) if(p == 0) count++;
    if(count >= 2) result.insert(0);
    for(int v = 1; v < g.size(); ++v) {
        if(parent[v] == 0) continue;
        if(prenum[parent[v]] <= lowest[v]) result.insert(parent[v]);
    }
    return result;
}

// 無向グラフの bridge の集合を返す
void bridge_dfs(const Graph& g, const int prev, const int current, int& timer, std::vector<int>& prenum, std::vector<int>& parent, std::vector<int>& lowest) {
    prenum[current] = timer;
    lowest[current] = timer;
    parent[current] = prev;
    for(const auto& e: g.edge(current)) {
        if(prenum[e.to()] == -1) {
            cut_vertex_dfs(g, current, e.to(), ++timer, prenum, parent, lowest);
            lowest[current] = std::min(lowest[current], lowest[e.to()]);
        } else if(e.to() != prev) {
            lowest[current] = std::min(lowest[current], prenum[e.to()]);
        }
    }
}
#include<iostream>
using namespace std;
std::set<Edge> bridge(const Graph& g) {
    std::vector<int> prenum(g.size(), -1);
    std::vector<int> parent(g.size(), -1);
    std::vector<int> lowest(g.size(), 1000000000);
    int timer = 0;
    bridge_dfs(g, -1, 0, timer, prenum, parent, lowest);
    std::set<Edge> result;
    for(int v = 0; v < g.size(); ++v) {
        for(const auto& e: g.edge(v)) {
//             if(prenum[e.from()] < lowest[e.to()]) result.insert(e);
            if(prenum[e.from()] < lowest[e.to()]) result.insert(Edge(std::min(e.from(), e.to()), std::max(e.from(), e.to()), e.weight()));
        }
    }
    return result;
}

////////////////////////////////////////////////////////////////////////////////

#include<iostream>
using namespace std;

int main() {
    int V, E;
    cin >> V >> E;
    Graph g(V);
    for(int i = 0; i < E; ++i) {
        int s, t;
        cin >> s >> t;
        g.add(s, t, 1);
        g.add(t, s, 1);
    }
    for(const auto& e: bridge(g)) cout << e.from() << " " << e.to() << endl;
}

Measured memory_bytes: 4956