「解题报告」UOJ552 [UNR #4] 同构判定鸭
print("Same")
嗯。期望得分 100。
首先考虑到题目要求所有字符串的出现次数相同,这意味着两个图能表示出来的字符串的多重集相等。
先考虑有向无环图的情况,发现这时候这个多重集一定是一个有限集,且字符串的长度不超过 \(\min(n_1, n_2)\)。判定两个集合是否相等,考虑哈希。我们可以将字符串的哈希定义为每一位的哈希值相乘再相加,这样容易每次给集合统一拓展一位。每一位的哈希值可以根据位置与字符随机一个权值。设 \(f_{u, k}\) 表示从 \(u\) 节点开始的所有长度为 \(k\) 的字符串的哈希之和,那么这个东西是容易在 \(O(nm)\) 的复杂度内递推出来的。
那么我们此时只需要找到第一个集合不相等的长度,然后贪心填每一位判定。具体判定方法就是,设 \(g_u\) 为以 \(u\) 结尾的能匹配上当前的字符串 \(s\) 的路径数,这样我们只需要判定 \(\sum g_u f_{u, k}\) 是否相等即可。\(g_u\) 也是容易递推的。
那么对于一般图呢?可以猜测,一般图下最大长度也不会很长,不然也没法让你输出方案。实际上,最长的长度为 \(n_1 + n_2\),那么就直接套用上面的做法即可。
两个问题:
为什么最长的长度为 \(n_1 + n_2\)?
我们回归最开始的描述,即每个字符串的出现次数相等。对于一个字符串来说,我们是容易通过递推求出出现次数的,而同时我们发现,这个递推过程是线性的。也就是说,我们可以使用矩阵来刻画这个转移过程。
我们将两个图合并在一起考虑,这样我们要求的就是 \(\sum_{i=1}^{n_1} f(i) = \sum_{i=n_1 + 1}^{n_1 + n_2} f(i)\)。我们将 \(f\) 写成横向量 \(u\),那么对于每一个字符,我们都可以用矩阵的方式去描述它,也就是说对于一个字符串 \(s_1s_2\cdots s_k\),我们可以将结果表示为 \(a = u M_{s_1} M_{s_2} \cdots M_{s_k}\),那么令 \(b\) 为一个前 \(n_1\) 位为 \(1\),后 \(n_2\) 位为 \(-1\) 的向量,那么两个出现次数不相等当且仅当 \(a^Tb \ne 0\)。这其实意味着 \(a, b\) 不正交。假如我们设 \(S_k\) 为所有长度不超过 \(k\) 的字符串最后的向量集合,那么我们要求的就是 \(\exists a \in S_k, a^T b \ne 0\)。这等价于 \(\exists a \in \mathrm{span}(S_k), a^T b \ne 0\)。
而我们发现,随着 \(k\) 的增加,一定有 \(\mathrm{span}(S_{k - 1}) \subseteq \mathrm{span}(S_{k})\),这意味着 \(\mathrm{span}(S_k)\) 要不然维度增加,要不然永远不变。而维度最大值显然只有 \(n_1 + n_2\),这也就证明了长度上界为 \(n_1 + n_2\)。
上述哈希正确率是否很高?
确实。
简单分析:如果我们将随机的权值看作变量 \(x_{k, c}\),那么我们的哈希函数就是关于 \(x_{k, c}\) 的一个 \(n_1 + n_2\) 次多项式。
Schwartz-Zippel 引理:对于某个域 \(F\) 上的不超过 \(d\) 次的多项式 \(f(x_1, \dots, x_m)\),如果每个 \(x_1, \cdots, x_m\) 都是从 \(F\) 中的一个大小为 \(S\) 的子集中独立地均匀随机选取的,那么 \(f(x) = 0\) 的概率不超过 \(\frac{d}{S}\)。
那么我们可以得知,假如模数为 \(p\),如果我们随机 \(x_{k, c}\),那么单次碰撞的概率为 \(O(\frac{n_1 + n_2}{p})\)。而我们需要计算 \(O((n_1 + n_2) \Sigma)\) 次,根据 Union Bound,可以得知总碰撞概率为 \(O(\frac{(n_1 + n_2)^2 \Sigma}{p})\)。所以我们只需要取一个较大的 \(p\)(或者选取双哈希),碰撞概率就很低了。
推荐个 unsigned long long
级别的好记的模数:0xffff_ffff_0000_0001
。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
typedef __uint128_t hash_t;
const hash_t P = 0xffffffff00000001;
hash_t val[MAXN][26];
int len;
struct Graph {
int n, m;
vector<pair<int, int>> e[MAXN];
void init() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; char c;
scanf("%d%d %c", &u, &v, &c);
e[u].push_back({v, c - 'a'});
}
}
hash_t f[MAXN][MAXN];
hash_t a[MAXN], b[MAXN];
void calc() {
for (int u = 1; u <= n; u++) f[u][0] = 1;
for (int k = 1; k <= len; k++) {
for (int u = 1; u <= n; u++) {
for (auto [v, c] : e[u]) {
f[u][k] = (f[u][k] + val[k][c] * f[v][k - 1]) % P;
}
}
}
for (int i = 1; i <= n; i++)
b[i] = 1;
}
hash_t get(int x) {
hash_t ret = 0;
for (int i = 1; i <= n; i++) {
ret = (ret + b[i] * f[i][x]) % P;
}
return ret;
}
void confirm() {
for (int i = 1; i <= n; i++) {
a[i] = b[i];
}
}
void test(int ec) {
for (int u = 1; u <= n; u++) {
b[u] = 0;
}
for (int u = 1; u <= n; u++) {
for (auto [v, c] : e[u]) if (c == ec) {
b[v] = (b[v] + a[u]) % P;
}
}
}
} g1, g2;
char ans[MAXN];
int main() {
// freopen("ex_string4.in", "r", stdin);
g1.init();
g2.init();
len = g1.n + g2.n;
mt19937_64 Rand(chrono::system_clock::now().time_since_epoch().count());
for (int i = 1; i <= len; i++) {
for (int j = 0; j < 26; j++) {
val[i][j] = Rand() % P;
}
}
g1.calc(), g2.calc();
g1.confirm(), g2.confirm();
for (int i = 1; i <= len; i++) {
if (g1.get(i) != g2.get(i)) {
for (int k = 1; k <= i; k++) {
for (int j = 0; j < 26; j++) {
g1.test(j), g2.test(j);
if (g1.get(i - k) != g2.get(i - k)) {
g1.confirm(), g2.confirm();
ans[k] = 'a' + j;
break;
}
}
}
printf("%s\n", ans + 1);
return 0;
}
}
printf("Same\n");
return 0;
}