2018 杭电多校 - 图论题目
这些题是真的硬核啊!
1. HDU - 6321 - Dynamic Graph Matching(状压 dp)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6321
题目来源:2018 HDU 多校第三场 - C 题
出题人:Claris(%%%)
1.1 题意
给你一个 n ( 1 ≤ n ≤ n , 2 ∣ n ) n(1 \le n \le n, 2|n) n(1≤n≤n,2∣n) 个点的无向图,最初无任何边。
一共有 m ( 1 ≤ m ≤ 3 ⋅ 1 0 4 ) m(1 \le m \le 3 \cdot 10^4) m(1≤m≤3⋅104) 次查询,查询共有两种:
- + u v \texttt{+ u v} + u v,表示增加一条无向边 ( u , v ) (u,v) (u,v),需要注意的是,本题支持添加无向边;
- - u v \texttt{- u v} - u v,表示删除一条无向边 ( u , v ) (u,v) (u,v),题目保证删掉的是已经存在的边。
每次操作之后,输出 n 2 \frac{n}{2} 2n 个数 a n s k ( 1 ≤ k ≤ n 2 ) ans_k(1 \le k \le \frac{n}{2}) ansk(1≤k≤2n),表示有 k k k 条边的独立集有多少。
本题有多组输入,共有 T ( 1 ≤ T ≤ 10 ) T(1 \le T \le 10) T(1≤T≤10) 组输入。
1.2 解题过程
通过 1 ≤ n ≤ 10 1 \le n \le 10 1≤n≤10 这个范围,应该立刻想到状态压缩!
将这 n n n 个点用二进制数 S S S 表示, 1 1 1 表示这个点被匹配, 0 0 0 表示这个点未被匹配。
设 d p [ S ] dp[S] dp[S] 表示在当前的边状态下,点的匹配状态为 S S S 的方案数。
先考虑加边的情况:
加边 ( u , v ) (u,v) (u,v) 时,将 d p [ 2 u − 1 + 2 v − 1 ] dp[2 ^ {u-1} + 2 ^ {v-1}] dp[2u−1+2v−1] 加 1 1 1,之后从小到大跑一遍状压 dp 即可得到所有状态的 dp 值。
之后开始统计答案,设 c n t ( S ) cnt(S) cnt(S) 为 S S S 中 1 1 1 的数量,则答案 a n s k = ∑ c n t ( S ) = 2 ⋅ k d p [ S ] ans_k=\sum_{cnt(S) = 2 \cdot k} dp[S] ansk=∑cnt(S)=2⋅kdp[S]。
删边时,考虑时光倒流即可。
时间复杂度: O ( T ⋅ 2 n ⋅ m ) O(T \cdot 2 ^ n \cdot m) O(T⋅2n⋅m)。
1.3 代码
ll dp[1 << 11];
ll ans[10];
int cnt[1 << 11];
void init()
{
memset(dp, 0, sizeof(dp));
}
int main()
{
int t, n, m;
int u, v;
char op[2];
for(int i = 0; i < (1 << 10); i++)
{
int S = i;
while(S)
{
cnt[i] += (S % 2);
S /= 2;
}
}
scanf("%d", &t);
while(t--)
{
scanf("%d%d", &n, &m);
init();
while(m--)
{
scanf("%s", op);
scanf("%d%d", &u, &v);
if(op[0] == '+')
{
int S = 0;
S |= (1 << (u - 1));
S |= (1 << (v - 1));
dp[S] = (dp[S] + 1) % Mod;
for(int i = 0; i < (1 << n); i++)
{
if((i >> (u - 1)) & 1) continue;
if((i >> (v - 1)) & 1) continue;
S = i;
S |= (1 << (u - 1));
S |= (1 << (v - 1));
dp[S] = (dp[S] + dp[i]) % Mod;
}
}
else
{
int S;
for(int i = 0; i < (1 << n); i++)
{
if((i >> (u - 1)) & 1) continue;
if((i >> (v - 1)) & 1) continue;
S = i;
S |= (1 << (u - 1));
S |= (1 << (v - 1));
dp[S] = (dp[S] - dp[i] + Mod) % Mod;
}
S = 0;
S |= (1 << (u - 1));
S |= (1 << (v - 1));
dp[S] = (dp[S] - 1 + Mod) % Mod;
}
memset(ans, 0, sizeof(ans));
for(int i = 0; i < (1 << n); i++)
{
if(cnt[i] & 1) continue;
ans[cnt[i] / 2] = (ans[cnt[i] / 2] + dp[i]) % Mod;
}
for(int k = 1; k <= n / 2; k++)
{
if(k > 1) printf(" ");
printf("%lld", ans[k]);
}
printf("\n");
}
}
return 0;
}
2. HDU - 6331 - Walking Plan(最短路 + 分块)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6331
题目来源:2018 HDU 多校第三场 - M 题
出题人:Claris(%%%)
2.1 题意
给你一张 n ( 2 ≤ n ≤ 50 ) n(2 \le n \le 50) n(2≤n≤50) 个点和 m ( 1 ≤ m ≤ 10000 ) m(1 \le m \le 10000) m(1≤m≤10000) 条边的无向图,每条边都有权值 w i ( 1 ≤ w i ≤ 10000 ) w_i(1 \le w_i \le 10000) wi(1≤wi≤10000)。
有 q ( 1 ≤ q ≤ 1 0 5 ) q(1 \le q \le 10^5) q(1≤q≤105) 次询问,每次询问给出 s i , t i , k i ( 1 ≤ s i , t i ≤ n , 1 ≤ k i ≤ 10000 ) s_i,t_i,k_i(1\leq s_i,t_i\leq n,1\leq k_i\leq 10000) si,ti,ki(1≤si,ti≤n,1≤ki≤10000),表示询问从 s i s_i si 到 t i t_i ti 至少经过 k i k_i ki 条边的最短路。
题目有 T ( 1 ≤ T ≤ 10 ) T(1 \le T \le 10) T(1≤T≤10) 组读入。
2.2 解题过程
首先回忆 Floyd 算法求恰好经过 m m m 条边的最短路的过程,
一般地,设矩阵
A
m
A^m
Am 表示任意两点之间恰好经过
m
m
m 条边的最短路,则
A
1
A^1
A1 矩阵相当于初始的邻接矩阵,我们有转移
(
A
r
+
m
)
[
i
]
[
j
]
=
min
1
≤
k
≤
n
{
(
A
r
)
[
i
]
[
k
]
+
(
A
m
)
[
k
]
[
j
]
}
(A^{r+m})[i][j]=\min_{1 \le k \le n} \{ (A^r)[i][k] + (A^m)[k][j]\}
(Ar+m)[i][j]=1≤k≤nmin{(Ar)[i][k]+(Am)[k][j]}
我们注意到,这其实是对矩阵乘法的改造,因此求解恰好经过
m
m
m 条边的最短路时,如果采用矩阵快速幂的话,时间复杂度为
O
(
n
3
log
m
)
O(n^3\log m)
O(n3logm)。
那么最终的答案为
min
1
≤
k
≤
n
{
A
m
[
s
i
]
[
k
]
+
G
[
k
]
[
t
i
]
}
\min_{1\le k \le n} \{ A^m[s_i][k] + G[k][t_i] \}
1≤k≤nmin{Am[si][k]+G[k][ti]}
其中
G
[
i
]
[
j
]
G[i][j]
G[i][j] 表示传统 Floyd 算法计算出的从
i
i
i 到
j
j
j 的最短路长度。
这样做,对于 1 0 5 10^5 105 组查询,显然会超时!
所以,我们必须对算法进行优化。
注意到, 10 0 2 = 10000 100^2=10000 1002=10000,所以我们可以对上述矩阵进行分块:
- 设 d p 0 [ k ] [ i ] [ j ] dp_0[k][i][j] dp0[k][i][j] 表示 i i i 和 j j j 之间恰好经过 k k k 条边的最短路;
- 设 d p 1 [ k ] [ i ] [ j ] dp_1[k][i][j] dp1[k][i][j] 表示 i i i 和 j j j 之间恰好经过 100 ⋅ k 100 \cdot k 100⋅k 条边的最短路;
- 设 d p 2 [ k ] [ i ] [ j ] dp_2[k][i][j] dp2[k][i][j] 表示 i i i 和 j j j 之间至少经过 k k k 条边的最短路。
- 设 G [ i ] [ j ] G[i][j] G[i][j] 表示传统 Floyd 算法计算出的从 i i i 到 j j j 的最短路长度。
所以有转移
d
p
0
[
k
]
[
i
]
[
j
]
=
min
1
≤
l
≤
100
{
d
p
0
[
k
−
1
]
[
i
]
[
l
]
+
d
p
0
[
1
]
[
l
]
[
j
]
}
d
p
1
[
k
]
[
i
]
[
j
]
=
min
1
≤
l
≤
100
{
d
p
1
[
k
−
1
]
[
i
]
[
l
]
+
d
p
0
[
100
]
[
l
]
[
j
]
}
d
p
2
[
k
]
[
i
]
[
j
]
=
min
0
≤
l
≤
100
{
d
p
0
[
k
]
[
i
]
[
l
]
+
G
[
l
]
[
j
]
}
dp_0[k][i][j] = \min_{1\le l \le 100} \{dp_0[k - 1][i][l] + dp_0[1][l][j] \}\\ dp_1[k][i][j] = \min_{1\le l \le 100} \{dp_1[k - 1][i][l] + dp_0[100][l][j] \}\\ dp_2[k][i][j] = \min_{0\le l \le 100} \{dp_0[k][i][l] + G[l][j] \}\\
dp0[k][i][j]=1≤l≤100min{dp0[k−1][i][l]+dp0[1][l][j]}dp1[k][i][j]=1≤l≤100min{dp1[k−1][i][l]+dp0[100][l][j]}dp2[k][i][j]=0≤l≤100min{dp0[k][i][l]+G[l][j]}
转移时注意边界条件的设置,例如在处理 d p 0 dp_0 dp0 和 d p 1 dp_1 dp1 时,自身和自身的距离应该为无穷大;但是在处理 G G G 时,自身和自身的距离应该为 0 0 0。
设查询为
s
i
,
t
i
,
k
i
s_i,t_i,k_i
si,ti,ki,则当前查询的答案为
min
1
≤
j
≤
n
{
d
p
1
[
⌊
k
i
100
⌋
]
[
s
i
]
[
j
]
+
d
p
2
[
k
mod
100
]
[
j
]
[
t
i
]
}
\min_{1 \le j \le n} \{ dp_1[\lfloor \frac{k_i}{100} \rfloor][s_i][j] + dp_2[k \text{ mod } 100][j][t_i] \}
1≤j≤nmin{dp1[⌊100ki⌋][si][j]+dp2[k mod 100][j][ti]}
总的时间复杂度为 O ( T ⋅ ( 100 ⋅ n 3 + q ⋅ n ) ) O(T \cdot (100 \cdot n^3 + q \cdot n)) O(T⋅(100⋅n3+q⋅n))。
2.3 代码
ll G[55][55];
ll dp[3][105][55][55];
int main()
{
int T, n, m, u, v, q, s, t, k;
ll w;
scanf("%d", &T);
while (T--)
{
scanf("%d%d", &n, &m);
memset(dp, 0x3f, sizeof(dp));
memset(G, 0x3f, sizeof(G));
for (int k = 0; k < 2; k++)
{
for (int i = 1; i <= n; i++)
{
dp[k][0][i][i] = 0;
}
}
for (int i = 1; i <= m; i++)
{
scanf("%d%d%lld", &u, &v, &w);
G[u][v] = min(G[u][v], w);
}
for (int l = 1; l <= 100; l++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
for (int k = 1; k <= n; k++)
{
dp[0][l][i][j] = min(dp[0][l][i][j], dp[0][l - 1][i][k] + G[k][j]);
}
}
}
}
for (int l = 1; l <= 100; l++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
for (int k = 1; k <= n; k++)
{
dp[1][l][i][j] = min(dp[1][l][i][j], dp[1][l - 1][i][k] + dp[0][100][k][j]);
}
}
}
}
for (int i = 1; i <= n; i++)
{
G[i][i] = 0;
}
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
}
}
}
for (int l = 0; l <= 100; l++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
for (int k = 1; k <= n; k++)
{
dp[2][l][i][j] = min(dp[2][l][i][j], dp[0][l][i][k] + G[k][j]);
}
}
}
}
scanf("%d", &q);
while (q--)
{
scanf("%d%d%d", &s, &t, &k);
ll ans = 0x3f3f3f3f3f3f3f3f;
for (int i = 1; i <= n; i++)
{
ans = min(ans, dp[1][k / 100][s][i] + dp[2][k % 100][i][t]);
}
if (ans == 0x3f3f3f3f3f3f3f3f) ans = -1;
printf("%lld\n", ans);
}
}
return 0;
}
3. HDU - 6370 - Werewolf(Tarjan + 乱搞)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6370
题目来源:2018 HDU 多校第六场
3.1 题意
有 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1≤n≤105) 个人,每个人可能是狼人,也可能是村民。
每个人 i i i 都会说一句话,表明 x x x 是狼人或村民。
已知下列事实:
- 狼人说的话不知道是真是假;
- 村民说的话一定是真的。
我们可以将这些人分成三类:
- 在所有情况下都是村民;
- 在所有情况下都是狼人;
- 在某些情况下是村民,在某些情况下是狼人。
现在需要你输出第 1 1 1 类和第 2 2 2 类人的数量。
共有 T ( 1 ≤ T ≤ 10 ) T(1 \le T \le 10) T(1≤T≤10) 组读入。
3.2 解题过程
根据题意,很显然需要建图。
每个人 i i i 都会说一句话,表明 x x x 是狼人或村民。
因此对于第 i i i 个人,如果它认为 x x x 时村民,建有向边 < i , x , 0 > <i, x, 0> <i,x,0>;否则建有向边 < i , x , 1 > <i, x, 1> <i,x,1>。
稍加分析即可发现任何人都可以是狼人,因此第 1 1 1 类人的数量一定为 0 0 0。
根据题意,我们建立的图中,每个人的出度一定为 1 1 1。因此一个强连通分量一定为一个简单环。
因此,我们跑一遍 Tarjan 即可确定所有的简单环。
仔细观察和推导,会发现对于一个简单环,如果其恰好只有一条边的权值为
1
1
1,则边权为
1
1
1 的那条边指向的点一定为狼人,其他人都可以为村民,否则会发生冲突。(可以假设以每个点为起点进行遍历,并假设起点为村民)
对于一个简单环的其他情况,这个简单环中的所有人均可为村民。
考虑完环,我们再来考虑链。
链中只存在一种特殊情况,就是如果链上某一一条边的权值为 0 0 0,并且指向了一定为狼人的点,那么这条边的起始点一定也为狼人,否则会发生矛盾。不满足上述情况的点,可以为村民。
其他情况下,链上所有点都可以为村民。
据此,我们只需一遍 DFS 即可计算出答案。
时间复杂度: O ( T ⋅ n ) O(T \cdot n) O(T⋅n)。
另外感觉这题可以直接 2-SAT,复杂度跟上面一样,等 HDU 开放之后试一试。
3.3 代码
int n, cnt, tot, top, num;
int head[maxn];
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
int stat[maxn];
bool vis[maxn], mark[maxn];
int ans;
struct edge
{
int v, w, nxt;
} Edge[2 * maxn];
void init()
{
for(int i = 0; i <= n; i++) head[i] = -1;
cnt = tot = top = num = ans = 0;
memset(instack, false, sizeof(instack));
memset(stat, 0, sizeof(stat));
memset(vis, false, sizeof(vis));
memset(mark, false, sizeof(mark));
memset(dfn, 0, sizeof(dfn));
memset(col, 0, sizeof(col));
}
void addedge(int u, int v, int w)
{
Edge[cnt].v = v;
Edge[cnt].w = w;
Edge[cnt].nxt = head[u];
head[u] = cnt++;
}
void tarjan(int id)
{
dfn[id] = low[id] = ++tot;
st[++top] = id;
instack[id] = true;
for(int i = head[id]; i != -1; i = Edge[i].nxt)
{
int v = Edge[i].v;
if(!dfn[v])
{
tarjan(v);
low[id] = min(low[id], low[v]);
}
else if(instack[v])
low[id] = min(low[id], dfn[v]);
}
if(dfn[id] == low[id])
{
int v;
num++;
do
{
v = st[top--];
instack[v] = false;
col[v] = num;
} while(v != id);
}
}
void dfs(int id)
{
int i = head[id];
int v = Edge[i].v;
int w = Edge[i].w;
vis[id] = true;
if(col[id] == col[v]) stat[col[id]] += (w == 1);
if(vis[v])
{
if(stat[col[id]] == 1) ans++;
}
else
{
dfs(v);
}
if(stat[col[id]] == 1 && w == 1)
{
mark[v] = true;
}
if(col[id] != col[v])
{
if(mark[v])
{
if(w == 0)
{
mark[id] = true;
ans++;
}
}
}
}
int main()
{
int t, x;
char op[15];
scanf("%d", &t);
while(t--)
{
scanf("%d", &n);
init();
for(int i = 1; i <= n; i++)
{
scanf("%d", &x);
scanf("%s", op);
if(op[0] == 'v')
{
addedge(i, x, 0);
}
else
{
addedge(i, x, 1);
}
}
for(int i = 1; i <= n; i++)
{
if(!col[i]) tarjan(i);
}
for(int i = 1; i <= n; i++)
{
if(!vis[i]) dfs(i);
}
printf("0 %d\n", ans);
}
return 0;
}
4. HDU- 6386 - Age of Moyu(DFS + BFS)
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=6386
题目来源:2018 HDU 多校第七场
4.1 题意
给你一张 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2≤n≤105) 个点和 m ( 0 ≤ m ≤ 2 ⋅ 1 0 5 ) m(0 \le m \le 2 \cdot 10^5) m(0≤m≤2⋅105) 条边的无向图,保证没有自环和重边。
每条边都有一个边权 C i ( 1 ≤ C i ≤ 1 0 6 ) C_i(1 \le C_i \le 10^6) Ci(1≤Ci≤106)。
现在你从 1 1 1 出发,要走到 n n n,边权变换一次的代价为 1 1 1,问最小代价,或者说明无法走到 n n n。
本题最多有 20 20 20 组输入。
4.2 解题过程
直接最短路的做法是假的!(可以通过造数据的方法,hack 掉这种做法)
正确的做法是 BFS 套 DFS,外层 BFS 正常跑最短路,每遍历到一个边,就通过 DFS 将周围的相同权值的边标记上。
无论是 DFS 还是 BFS,遇到被标记过的边,都直接跳过不走。
时间复杂度: O ( 20 ⋅ ( n + m ) ) O(20 \cdot (n + m)) O(20⋅(n+m))。
4.3 代码
int n, m, cnt;
int head[maxn];
ll d[maxn];
bool vis[maxn], vis_edge[4 * maxn];
struct edge
{
int v, w, nxt;
} Edge[4 * maxn];
void init()
{
for(int i = 0; i <= n; i++) head[i] = -1;
cnt = 0;
}
void addedge(int u, int v, int w)
{
Edge[cnt].v = v;
Edge[cnt].w = w;
Edge[cnt].nxt = head[u];
head[u] = cnt++;
}
queue<int> que;
void dfs(int id, int w, ll val)
{
if(!vis[id])
{
vis[id] = true;
d[id] = val;
if(id == n) return;
que.push(id);
}
for(int i = head[id]; i != -1; i = Edge[i].nxt)
{
int v = Edge[i].v;
if(w != Edge[i].w) continue;
if(vis_edge[i]) continue;
vis_edge[i] = vis_edge[i ^ 1] = true;
dfs(v, w, val);
}
}
void bfs()
{
while(!que.empty()) que.pop();
memset(vis, false, sizeof(vis));
memset(vis_edge, false, sizeof(vis_edge));
memset(d, 0x3f, sizeof(d));
d[1] = 0;
vis[1] = true;
que.push(1);
while(!que.empty())
{
int now = que.front();
que.pop();
for(int i = head[now]; i != -1; i = Edge[i].nxt)
{
if(vis_edge[i]) continue;
vis_edge[i] = vis_edge[i ^ 1] = true;
int v = Edge[i].v;
dfs(v, Edge[i].w, d[now] + 1);
}
}
}
int main()
{
int u, v, w;
while(~scanf("%d%d", &n, &m))
{
init();
for(int i = 1; i <= m; i++)
{
scanf("%d%d%d", &u, &v, &w);
addedge(u, v, w);
addedge(v, u, w);
}
bfs();
if(d[n] == 0x3f3f3f3f3f3f3f3f) printf("-1\n");
else printf("%lld\n", d[n]);
}
return 0;
}
5. HDU - 6311 - Cover(欧拉回路 + 乱搞)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6311
题目来源:2018 HDU 多校第二场
5.1 题意
给你一张 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1≤n≤105) 个点和 m ( 1 ≤ m ≤ 1 0 5 ) m(1 \le m \le 10^5) m(1≤m≤105) 条边的无向图,保证没有自环和重边。
每个士兵可以选择一个起点,然后沿着图不断走下去,但每条边走过去之后就不能再走。问最少需要多少士兵,使得图中的每条边恰好被走一次。
题目有多组读入,最多 20 20 20 组。
5.2 解题过程
首先,每个士兵走的道路形成的子图一定是一个欧拉通路或者欧拉回路。
设图中奇度顶点的个数为 c n t cnt cnt,则士兵的数量为 c n t 2 \frac{cnt}{2} 2cnt。
显然如果 c n t > 0 cnt > 0 cnt>0,整个图中是不存在欧拉回路的。
我们可以尝试两两将奇度顶点连边(每个奇度顶点只和一个奇度顶点连边),之后我们优先从原图的奇度顶点开始跑欧拉回路,当这些顶点跑完之后再次遍历所有顶点,如果发现还没有走过的顶点,就从这个没有走过的顶点开始跑新的欧拉回路。
在跑欧拉回路时,将每一条路径所经过的边分别存储。
如何分辨不同的路径呢?很简单,每一次重新开始一轮查找欧拉回路时或者遇到新建的虚边时,相当于走到了新的路径上。而且这种实现方法无需考虑原图的连通性。
最后根据所存储的每一条路径中的边,按照要求将答案输出即可。
时间复杂度: O ( 20 ⋅ n ) O(20 \cdot n) O(20⋅n)。
5.3 代码
int n, m, cnt, tot, num2;
int head[maxn], deg[maxn], col[maxn];
bool vis[4 * maxn], mark[4 * maxn], visp[maxn];
vector<int> ans[maxn], final_ans[maxn];
struct edge
{
int v, nxt;
} Edge[4 * maxn];
void init()
{
for(int i = 0; i <= n; i++) head[i] = -1;
cnt = 0, tot = 0, num2 = 0;
memset(col, 0, sizeof(col));
memset(deg, 0, sizeof(deg));
memset(vis, false, sizeof(vis));
memset(mark, false, sizeof(mark));
memset(visp, false, sizeof(visp));
for(int i = 0; i <= n; i++) ans[i].clear();
for(int i = 0; i <= n; i++) final_ans[i].clear();
}
void addedge(int u, int v)
{
Edge[cnt].v = v;
Edge[cnt].nxt = head[u];
head[u] = cnt++;
}
void euler(int id)
{
visp[id] = true;
for(int i = head[id]; i != -1; i = Edge[i].nxt)
{
int v = Edge[i].v;
if(!vis[i])
{
vis[i] = vis[i ^ 1] = true;
euler(v);
if(i >= 2 * m) num2++;
else if(i != -1)
{
ans[num2].pb(i);
}
}
}
}
void dfs(int id, int color)
{
if(col[id]) return;
col[id] = color;
for(int i = head[id]; i != -1; i = Edge[i].nxt)
{
int v = Edge[i].v;
if(col[v]) continue;
dfs(v, color);
}
}
int main()
{
int u, v;
while(~scanf("%d%d", &n, &m))
{
init();
for(int i = 1; i <= m; i++)
{
scanf("%d%d", &u, &v);
addedge(u, v);
addedge(v, u);
deg[u]++;
deg[v]++;
}
int num = 0;
for(int i = 1; i <= n; i++)
{
if(!col[i]) dfs(i, ++num);
}
int point = -1;
for(int i = 1; i <= n; i++)
{
if(deg[i] & 1)
{
if(point != -1)
{
addedge(point, i);
addedge(i, point);
point = -1;
}
else point = i;
}
}
for(int i = 1; i <= n; i++)
{
if(!visp[i] && (deg[i] & 1))
{
++num2;
euler(i);
//--num2;
}
}
for(int i = 1; i <= n; i++)
{
if(!visp[i])
{
++num2;
euler(i);
}
}
int num3 = 0;
for(int i = 0; i <= num2; i++)
{
if(ans[i].size() == 0) continue;
++num3;
int size = ans[i].size();
for(int j = size - 1; j >= 0; j--) final_ans[num3].pb(ans[i][j]);
}
printf("%d\n", num3);
for(int i = 1; i <= num3; i++)
{
printf("%d", (int)final_ans[i].size());
for(int j = 0; j < final_ans[i].size(); j++)
{
int id = final_ans[i][j];
if(id & 1)
{
id ^= 1;
printf(" -%d", id / 2 + 1);
}
else printf(" %d", id / 2 + 1);
}
printf("\n");
}
}
return 0;
}