#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5 + 10, inf = 0x3f3f3f3f;
// 边的结构体:v-目标节点,id-边的编号(用于区分重边)
struct node{
int v, id;
};
vector<node> g[N]; // 邻接表存储图
int n, m; // n: 节点数, m: 边数
int id; // 边的编号计数器,用于给每条边一个唯一编号
// Tarjan算法核心数组
int dfn[N], low[N], cnt; // dfn:时间戳,low:能回溯到的最早时间戳,cnt:时间戳计数器
int inq[N]; // 标记节点是否在栈中(这里其实用不到,但保留了)
// 栈相关
int s[N], top; // 数组模拟栈,存储DFS路径上的节点
// 边双连通分量相关
int sccnt; // 边双连通分量计数器
vector<int> Scc[N]; // 存储每个边双连通分量包含的节点
/**
* Tarjan算法求边双连通分量
* @param x 当前访问的节点
* @param bh 进入当前节点的边的编号(用于防止走反边,即父边)
*/
void tarjan(int x, int bh)
{
// 初始化当前节点的时间戳和low值
dfn[x] = low[x] = ++cnt;
inq[x] = 1; // 标记节点在栈中
s[++top] = x; // 节点入栈
// 遍历当前节点的所有邻接边
for(int i = 0; i < g[x].size(); i++)
{
int y = g[x][i].v; // 邻接节点
int Id = g[x][i].id; // 当前边的编号
// 如果当前边是进入x的边(父边),则跳过,防止走反边
// 注意:这里处理了重边的情况,因为重边有不同的id
if(bh == Id) continue;
if(!dfn[y]){ // 如果y未被访问过(树边)
tarjan(y, Id); // 递归访问y
// 回溯时用子节点的low值更新当前节点的low值
low[x] = min(low[x], low[y]);
}
else { // y已被访问过(回边或横叉边)
// 用y的时间戳更新当前节点的low值
// 注意:这里不需要判断y是否在栈中,因为无向图没有横叉边的问题
low[x] = min(low[x], dfn[y]);
}
}
// 关键判断:如果low[x] == dfn[x],说明x是当前边双的根节点
// 这意味着从x出发无法通过回边到达任何祖先节点
// 此时栈中从x到栈顶的所有节点构成一个边双连通分量
if(low[x] == dfn[x])
{
sccnt++; // 新的边双分量
while(1)
{
int y = s[top--]; // 弹出栈顶节点
Scc[sccnt].push_back(y); // 将该节点加入当前边双
if(y == x) break; // 弹到x时停止(x是根)
}
}
}
/**
* 调试函数:打印每个节点的dfn和low值
*/
void print()
{
for(int i = 1; i <= n; i++)
cout << dfn[i] << " " << low[i] << " " << i << endl;
}
int main()
{
cin >> n >> m;
id = 0; // 边编号从0开始
// 读入所有边
for(int i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
if(x == y){
// 自环处理:只加一条边,但用不同的id
// 自环不会影响边双的连通性,但需要存储
g[x].push_back({y, id});
id++;
}
else{
// 无向图:加双向边,使用相同的id表示这是同一条边
g[x].push_back({y, id});
g[y].push_back({x, id});
id++;
}
}
// 对每个未被访问的节点执行Tarjan算法
// 图可能不连通,所以需要遍历所有节点
for(int i = 1; i <= n; i++)
if(!dfn[i])
tarjan(i, -1); // -1表示没有进入边(根节点)
// 输出结果
cout << sccnt << endl; // 先输出边双连通分量的个数
for(int i = 1; i <= sccnt; i++)
{
cout << Scc[i].size() << " "; // 输出当前分量的节点数
for(int j = 0; j < Scc[i].size(); j++)
cout << Scc[i][j] << " "; // 输出当前分量的所有节点
cout << endl;
}
// print(); // 调试用,可以取消注释查看dfn和low值
return 0;
}