前排提醒:如果数学公式看不清楚,可以把鼠标悬停上去,会放大。
简要题意
给定一张
你需要找出尽可能多的简单路径,使得:
- 这些路径间两两不交(不经过重复点);
- 路径的起点与终点需要是有颜色但是颜色不同的点;
- 除了起点终点外,路径只能经过没有颜色的点。
需要构造出答案。
背景与参考文献
这个问题是经典问题,叫做 S-path problem。
本文将陈述此问题的
参考文献为
- Algebraic Algorithms for Linear Matroid Parity Problems, Ho Yee Cheung, Lap Chi Lau, Kai Man Leung
理论与算法
1. 问题转化
我们先把原问题转化为:
问题一:选出原图中尽量多的边,使得这些边构成森林,且森林中每棵树里最多有两个有颜色的点,并且不能有两个相同颜色的点。
这样的话,最优解中每棵树里肯定要么有一个有颜色的点,要么有两个有颜色且颜色不同的点(除非原图中某个连通块里一个有颜色的点都没有,这样的连通块显然可以直接扔掉)。
所以原问题的答案一定就是总的有颜色的点的个数,减去新问题中连通块个数。而新问题中连通块个数又等于
因此选的边越多,得出的简单路径就越多。这些简单路径也可以由新问题里选出的边轻易构造出来。
这一步转化是容易的,我们接下来会将这个新问题再转化为一个线性代数问题。
2. 线性拟阵配对问题
这个线性代数问题如下。
问题二(线性拟阵配对): 给定
维空间里的 对向量,即 个向量 . 你需要找出其中尽可能多对,使得这些对向量线性无关。
如果你并不知道“线性无关”的定义,建议参考OI-Wiki: 线性基。 此外,OI-Wiki 中线性代数相关的其他概念也可能对你理解本文有帮助。
如何将前面的问题一转化为该问题呢?有一个非常精妙的构造如下。
我们先取一个大空间
为了方便,我们记
接下来对原图中每条边
如果只是这样的话,那么只有当选出的边构成一个环的时候,才会线性相关:
比如如果选出了
为了满足我们的额外条件(每棵树最多有两个有颜色的点,并且颜色不能相同),还需要引入一个额外的东西。
对于每个颜色,我们选取
对每个点
也就是说
可以发现,如果我们选出了一些边,这些边连通了某两个点
那么如果我们选出的边连接了两个颜色相同的点,比如
而如果我们选出的边连接了三个有颜色的点,比如
也就是说,如果我们强制选择
怎么强制选择这些向量呢?可以考虑把所有
3. 转化的具体实现
首先,我们先(假装)求出这
接下来我们做投影。注意我们其实不需要正交,只需要把他们投影到某个子空间
所以我们不妨设 v[2*u] += c[u] * v[2*u - 1]; v[2*u-1] = 0;
(然后我们也可以把第
4. 新问题的求解
接下来我们就要求解这个线性代数问题。
我们先定义一个记号:若
我们给出本文中四个不加证明的定理:
首先是解决线性拟阵配对问题的核心定理:
定理 [Lovász]: 给定
维空间里的 对列向量,即 个向量 . 我们引入 个未知数 ,并且在他们的分式域里考虑矩阵(即矩阵的每个分量可以是这些未知数的有理式)。 定义矩阵 其中 。 则 ,其中 是问题二的答案,即最多可以选出的线性无关向量对数。
定理 [Schwartz-Zippel 引理]: 在模素数
的意义下,若 是不超过 次的非零多元多项式,那么任取 为 的随机值, 的概率不超过 .
然后是让我们之后算法的重点:
定理 [Harvey]: 设
是一个 可逆矩阵, 。若 和 长得很像——具体地说,有一个子集 , 使得除了 之外 的分量都和 相等。记 。那么
可逆当且仅当 可逆。 - 若第一条成立,则
; - 若第一条成立,则
。
最后是其实可能不必要的一个玩意。
定理: 若
是斜对称矩阵,即 ,而 ,则存在 使得 可逆。 具体地,只要 中 对应的行线性无关(即 ),就有 可逆。 如果没有斜对称的条件,那么只能知道存在
使得 可逆。
对可能不熟悉的读者:秩
首先,我们可以取一个足够大的素数
接下来,我们算出前面这个矩阵
为了方便,我们可以求出
我们找到方案的办法非常简单粗暴:对于每一对
但是这样的话每次判断是否可逆的复杂度,即 Gauss 消元,是
如果是一般的情况,其实有 Sherman-Morrison-Woodbury 定理,可以做到每次
5. 最后的算法
我们发现其实在本题我们有一个很好的性质,那就是
因此我们可以使用 Harvey 定理——等一下,
我们可以采用分治算法:如果我们有一堆边,他们对应的
如果把这个思路用到极致,就可以写出一个分治算法(伪代码):
SOLVE(R, C): 记 S = R 并 C // 执行时,要求 N_(S, S) 是正确的 // 执行后,已经尝试删除了所有一端点属于 R 而另一端点属于 C 的边 IF |R| > 2 or |C| > 2: 将 R 划分为 R1, R2 将 C 划分为 C1, C2 FOR (i, j) in { (1, 1), (1, 2), (2, 1), (2, 2) }: SOLVE(Ri, Cj) 使用 Harvery 定理更新 N_(S, S) ELSE IF R = { 2u-1, 2u }, C = { 2v-1, 2v } 且存在左端点为 u 右端点为 v 的边: 使用 Harvery 定理判断是否可以删掉这条边 如果是,删掉 // 不需要更新 N,因为递归上去之后会更新
注意伪代码里默认了结点
这样的话,复杂度为
代码
由于这个矩阵是
笔者和出题人码风不通,没看懂他的实现。笔者本人的实现最后使用了
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
typedef long long LL;
const int mod = 100000007;
const int N = 303;
const int NN = 606;
const int M = N * N / 2;
LL pow_mod(LL a, LL b) {
LL ans = 1;
for (; b; b >>= 1, a = a * a % mod)
if (b & 1) ans = ans * a % mod;
return ans;
}
inline void mul(int m, LL *A, LL x) {
for (int i = 0; i < m; ++i) A[i] = A[i] * x % mod;
}
inline void add(int m, LL *A, const LL *B, LL x) {
for (int i = 0; i < m; ++i) A[i] = (A[i] + x * B[i]) % mod;
}
inline void swap(int m, LL *A, LL *B) {
for (int i = 0; i < m; ++i) std::swap(A[i], B[i]);
}
void rank(int n, int m, LL A[NN][NN], int &r, int *S) {
// A[S][*] has rank r.
static int id[NN];
for (int i = 0; i < n; ++i) id[i] = i;
for (int i = 0, j = 0; j < m; ++j) {
int k = i;
while (k < n && A[k][j] == 0) ++k;
if (k == n) continue;
if (i != k) {
std::swap(id[i], id[k]);
swap(m, A[i], A[k]);
}
S[r++] = id[i];
LL t = pow_mod(A[i][j], mod - 2);
for (int k = i + 1; k < n; ++k)
add(m, A[k], A[i], -t * A[k][j] % mod);
++i;
}
}
bool inv(int n, const LL A_[NN][NN], LL B[NN][NN]) {
static LL A[NN][NN];
// return true iff A is invertible.
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
A[i][j] = A_[i][j], B[i][j] = i == j;
for (int i = 0; i < n; ++i) {
int k = i;
while (k < n && A[k][i] == 0) ++k;
if (k == n) return false;
if (i != k) {
swap(n, A[i], A[k]);
swap(n, B[i], B[k]);
}
LL t = pow_mod(A[i][i], mod - 2);
mul(n, A[i], t); mul(n, B[i], t);
for (int k = 0; k < n; ++k) if (k != i) {
add(n, B[k], B[i], -A[k][i] % mod);
add(n, A[k], A[i], -A[k][i] % mod);
}
}
return true;
}
void mul(int n, int m, int r, const LL A[NN][NN], const LL B[NN][NN], LL C[NN][NN]) {
static LL tmp[NN][NN];
for (int i = 0; i < n; ++i)
for (int j = 0; j < r; ++j) tmp[i][j] = 0;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
for (int k = 0; k < r; ++k)
tmp[i][k] = tmp[i][k] + A[i][j] * B[j][k];
for (int i = 0; i < n; ++i)
for (int j = 0; j < r; ++j) C[i][j] = tmp[i][j] % mod;
}
#define take(A, X, Y, FORMULA) \
for (int i = 0; i < X; ++i) \
for (int j = 0; j < Y; ++j) \
A[i][j] = FORMULA
bool modify(const LL A[NN][NN], LL B[NN][NN], const LL A1[NN][NN],
int s, const int *S, int t, const int *T) {
// assuming B = A^(-1) and A[i][j] != A1 only when i, j in S
// if A1 is singular, return false
// else:
// modify B such that B_(T, T) = A1^(-1)_(T, T)
// only use A[S][S], A1[S][S], B[T][T].
static LL D[NN][NN], C[NN][NN], P[NN][NN];
take(D, s, s, A1[S[i]][S[j]] - A[S[i]][S[j]]);
take(P, s, s, B[S[i]][S[j]]);
mul(s, s, s, D, P, P);
for (int i = 0; i < s; ++i) (++P[i][i]) %= mod;
if (!inv(s, P, C)) return false;
mul(s, s, s, C, D, C);
take(P, t, s, B[T[i]][S[j]]);
mul(t, s, s, P, C, C);
take(P, s, t, B[S[i]][T[j]]);
mul(t, s, t, C, P, C);
for (int i = 0; i < t; ++i)
for (int j = 0; j < t; ++j) {
int ti = T[i], tj = T[j];
B[ti][tj] = (B[ti][tj] - C[i][j]) % mod;
}
return true;
}
int C[N], L[N];
LL X[N][N];
bool link[N][N];
int n, nn;
int id[NN];
int t, T[NN];
void Add(LL A[NN][NN], int u, int v, int i, int j, LL p) {
int x = L[u] + i, y = L[v] + j;
if (C[u] && i) x = L[u], p = p * C[u] % mod;
if (C[v] && j) y = L[v], p = p * C[v] % mod;
if (id[x] >= 0 && id[y] >= 0) A[id[x]][id[y]] = (A[id[x]][id[y]] + p) % mod;
}
inline void Add(LL A[NN][NN], int u, int v, LL p) {
// b = e(u0) - e(v0)
// c = e(u1) - e(v1)
// A += p (b wedge c)
Add(A, u, u, 0, 1, p);
Add(A, u, v, 0, 1, -p);
Add(A, v, u, 0, 1, -p);
Add(A, v, v, 0, 1, p);
Add(A, u, u, 1, 0, -p);
Add(A, u, v, 1, 0, p);
Add(A, v, u, 1, 0, p);
Add(A, v, v, 1, 0, -p);
}
LL Y[NN][NN], Z[NN][NN];
int ss[10], SS[10][NN];
LL AA[10][NN][NN], BB[10][NN][NN];
bool solve(int l1, int r1, int l2, int r2, int dep) {
#define copyS(U, V) \
for (int i = 0; i < s; ++i) \
for (int j = 0; j < s; ++j) \
U[S[i]][S[j]] = V[S[i]][S[j]]
if (r1 < l1 || r2 < l2) return false;
int &s = ss[dep] = 0;
int *S = SS[dep];
LL (*A)[NN] = AA[dep], (*B)[NN] = BB[dep];
for (int i = L[l1]; i < L[r1 + 1]; ++i) if (id[i] >= 0) S[s++] = id[i];
for (int i = L[l2]; i < L[r2 + 1]; ++i) if (id[i] >= 0) S[s++] = id[i];
std::sort(S, S + s); s = std::unique(S, S + s) - S;
copyS(A, Y);
copyS(B, Z);
if (r1 == l1 && r2 == l2) {
if (!link[l1][l2]) return false;
Add(A, l1, l2, -X[l1][l2]);
if (!modify(Y, B, A, s, S, 0, NULL)) return false;
link[l1][l2] = false;
Add(Y, l1, l2, -X[l1][l2]);
return true;
}
int m1 = (l1 + r1) / 2, m2 = (l2 + r2) / 2;
bool changed = false;
for (int a = 0; a < 2; ++a)
for (int b = 0; b < 2; ++b) {
int u1 = l1, v1 = m1, u2 = l2, v2 = m2;
if (a) u1 = m1 + 1, v1 = r1;
if (b) u2 = m2 + 1, v2 = r2;
if (solve(u1, v1, u2, v2, dep + 1)) {
changed = true;
// fprintf(stderr, "%d %d\n", ss[dep + 1], s);
if (ss[dep + 1] != s) {
if (false) {
inv(s, Y, Z);
} else {
copyS(Z, B);
modify(A, Z, Y, ss[dep + 1], SS[dep + 1], s, S);
}
}
copyS(A, Y);
copyS(B, Z);
}
}
return changed;
}
bool vis[N];
int path[N][N], len[N], ans;
bool dfs(int x, bool rt) {
vis[x] = true;
if (C[x] && !rt) {
++ans;
path[ans][len[ans]++] = x;
return true;
}
for (int y = 1; y <= n; ++y)
if ((link[x][y] || link[y][x]) && !vis[y]) {
int t = dfs(y, false);
if (t) {
path[ans][len[ans]++] = x;
return true;
}
}
return false;
}
int main() {
int m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d", &C[i]);
L[i] = nn++;
if (!C[i]) nn++;
}
L[n + 1] = nn;
for (int i = 0; i < nn; ++i) id[i] = i;
for (int i = 0; i < m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
if (u == v || link[u][v] || link[v][u]) continue;
link[u][v] = true;
X[u][v] = rand() % mod; // TODO: ERROR
Add(Y, u, v, X[u][v]);
}
rank(nn, nn, Y, t, T);
memset(id, -1, sizeof id);
for (int i = 0; i < t; ++i) id[T[i]] = i;
memset(Y, 0, sizeof Y);
for (int u = 1; u <= n; ++u)
for (int v = 1; v <= n; ++v)
if (link[u][v]) Add(Y, u, v, X[u][v]);
inv(nn, Y, Z);
solve(1, n, 1, n, 0);
for (int i = 1; i <= n; ++i)
if (!vis[i] && C[i]) dfs(i, true);
printf("%d\n", ans);
for (int i = 1; i <= ans; ++i) {
printf("%d", len[i]);
for (int j = 0; j < len[i]; ++j)
printf(" %d", path[i][j]);
puts("");
}
return 0;
}