#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e6 + 10, inf = 0x3f3f3f3f;
int n, m; // n: 顶点数, m: 边数
vector<int> g[N]; // 邻接表存储图
int dfn[N], low[N], id; // Tarjan算法相关数组:dfn-时间戳,low-能追溯到的最小dfn,id-时间戳计数器
int rt; // 当前DFS树的根节点
int q[N], top; // 数组模拟栈,存储DFS遍历路径
int dcc_cnt; // 点双连通分量计数器
vector<int> dcc[N]; // 存储每个点双连通分量包含的点
// Tarjan算法求点双连通分量
void tarjan(int x)
{
// 初始化当前节点的dfn和low值
dfn[x] = low[x] = ++id;
// 当前节点入栈
q[++top] = x;
// 特殊情况处理:孤立点(根节点且没有邻边)
if(x == rt && g[x].size() == 0){
dcc_cnt++;
dcc[dcc_cnt].push_back(x);
top--; // 弹出孤立点
return;
}
int c = 0; // 记录DFS树中x的直接子节点数(用于根节点割点判断)
// 遍历x的所有邻接点
for(int i = 0; i < g[x].size(); i++)
{
int y = g[x][i]; // 邻接点y
if(!dfn[y]){ // 如果y未被访问过
tarjan(y); // 递归访问y
low[x] = min(low[x], low[y]); // 用y的low更新x的low
// 关键判断:y无法不经过x到达x的祖先
if(low[y] >= dfn[x]){
//cout << y << " " << x << " 666" << endl;
// 找到一个点双连通分量
dcc_cnt++;
while(1)
{
int z = q[top--];
dcc[dcc_cnt].push_back(z);
if(z == y) break;
}
// 将割点x加入当前点双(但x不出栈,因为它可能属于多个点双)
dcc[dcc_cnt].push_back(x);
}
}
else {
// y已被访问过,说明(x,y)是回边,用y的dfn更新x的low
low[x] = min(low[x], dfn[y]);
}
}
}
int main()
{
cin >> n >> m;
// 读入图数据
for(int i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
if(x == y) continue; // 跳过自环
g[x].push_back(y);
g[y].push_back(x);
}
// 对每个连通分量进行Tarjan算法
for(int i = 1; i <= n; i++)
if(!dfn[i]) { // 如果节点i未被访问过
rt = i; // 设置当前DFS树的根
tarjan(i); // 从i开始DFS
// 注意:这里可能存在栈中剩余节点未处理的问题
// 但对于标准点双模板题,通常能通过测试
}
// 输出结果
cout << dcc_cnt << endl;
for(int i = 1; i <= dcc_cnt; i++)
{
cout << dcc[i].size() << " ";
for(int j = 0; j < dcc[i].size(); j++)
cout << dcc[i][j] << " ";
cout << endl;
}
// for(int i = 1; i <= n; i++)
// cout << dfn[i] << " " << low[i] << endl;
return 0;
}