【题解】BZOJ4205 卡牌配对
题意
已知有 \(n\) 个物品,每个物品有三个属性 \(a_i, b_i, c_i\)。所有物品可以分成 \(x, y\) 两类,分别有 \(n_1, n_2\) 张。如果两个不同类物品满足至多有一项属性的值互质,则这两个物品可以配对,每个物品至多配对一次。试求匹配次数的最大值。
\(n1, n2 \leq 3 \times 10^4, 1 \leq a_i, b_i, c_i \leq 200\)
思路
最大流。
朴素做法是模拟题意 \(\mathcal{O}(n^2)\) 建边跑二分图最大匹配,用匈牙利实现可以拿到 \(10\) 分,带当前弧优化的 dinic 可以拿到 \(60\) 分。
考虑优化建图,减少原图中的边数。
对于两个物品,如果它们存在两项属性值,使得它们的这两项属性值分别存在公共的因数 \(x, y\),则当这两个物品不同类时,它们可以配对。显然我们可以只考虑 \(x, y\) 均为质数的情况。注意到 \(200\) 以内的质数只有 \(46\) 个,所以可以考虑根据质因子建立虚点。
对于满足 \(x, y \leq 200\) 的质数对 \((x, y)\),我们建立s三个用于中转的虚点(属性值有一一对应关系,因此需要分类讨论 \(a_i, b_i, c_i\) 中相同的属性值),令其对应的虚点分别为 \(p_1(x, y), p_2(x, y), p_3(x, y)\)。对于属于 \(x\) 类的物品 \(i\),以 \(a_i, b_i\) 为例,若 \(x | a_i, y | b_i\) 且 \(x, y\) 均为质数,则我们从物品 \(i\) 向 \(p_1(x, y)\) 连一条容量为 \(1\) 的边。同时我们从源点 \(s\) 向物品 \(i\) 连边。\(a_i, c_i\) 和 \(b_i, c_i\) 的情况同理。
对于属于 \(y\) 类的物品 \(i\),同样以 \(a_i, b_i\) 为例,若 \(x | a_i, y | b_i\) 且 \(x, y\) 均为质数,则我们从 \(p_1(x, y)\) 向物品 \(i\) 连一条容量为 \(1\) 的边。同时我们从物品 \(i\) 向汇点 \(t\) 连一条容量为 \(1\) 的边。其余情况同理。
因为 \(2 \times 5 \times 7 \times 11 > 200\),所以每个属性值至多只会有 \(3\) 个不同的质因子。因此每个顶点至多向虚点连出 \(C_3^2 \times 3 \times 3 = 27\) 条边,即 \(6 \times 10^4\) 个顶点至多连出 \(1.62 \times 10^6\) 条边。在这种类似二分图且容量均为 \(1\) 的图上,当前弧优化的 dinic 效率极优,可以不卡常 AC。
质因子的虚点实际上表示的是至多只有一项属性值互质的限制。根据得到的性质,只有两项属性值分别存在公共质因子的物品之间才会存在以公共质因子对为中转点的虚点,因此跑出的最大流是满足限制下的最大匹配次数。
这或许启示我们可以通过在二分图的中间建立虚点,仅在满足限制的顶点之间构造以虚点中转的路径来表示限制。
代码
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1.6e5 + 5;
const int maxm = 3.5e6 + 5;
const int maxp = 50;
const int maxv = 205;
const int inf = 2147483647;
struct node
{
int to, nxt, w;
} edge[maxm];
int n1, n2, s, t;
int len, cnt = 1, ans;
int p[maxp];
int idx[maxp][maxp];
int a[maxn], b[maxn], c[maxn];
int head[maxn], cur[maxn], dep[maxn];
vector<int> fac[maxv];
void add_edge(int u, int v, int w)
{
cnt++;
edge[cnt].to = v;
edge[cnt].nxt = head[u];
edge[cnt].w = w;
head[u] = cnt;
}
void add(int u, int v, int w)
{
add_edge(u, v, w);
add_edge(v, u, 0);
}
bool bfs()
{
queue<int> q;
memset(dep, 0, (t + 1) * sizeof(int));
dep[s] = 1;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (edge[i].w && (!dep[v]))
{
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
return (dep[t] > 0);
}
int dfs(int u, int flow)
{
if (u == t) return flow;
for (int &i = cur[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (edge[i].w && (dep[v] == dep[u] + 1))
{
int val = dfs(v, min(flow, edge[i].w));
if (val)
{
edge[i].w -= val;
edge[i ^ 1].w += val;
return val;
}
}
}
return 0;
}
void dinic()
{
while (bfs())
{
int val;
memcpy(cur, head, (t + 1) * sizeof(int));
while (val = dfs(s, inf)) ans += val;
}
}
bool check(int x)
{
for (int i = 2; i * i <= x; i++)
if (!(x % i)) return false;
return true;
}
void init()
{
for (int i = 2; i <= 200; i++)
if (check(i)) p[++len] = i;
for (int i = 1; i <= len; i++)
for (int j = 1; j <= len; j++)
idx[i][j] = (i - 1) * len + j;
for (int i = 2; i <= 200; i++)
for (int j = 1; j <= len; j++)
if (!(i % p[j])) fac[i].push_back(j);
}
void work1(int u, int x, int y, int opt)
{
for (int i = 0; i < fac[x].size(); i++)
for (int j = 0; j < fac[y].size(); j++)
add(u, n1 + n2 + opt * 46 * 46 + idx[fac[x][i]][fac[y][j]], 1);
}
void work2(int u, int x, int y, int opt)
{
for (int i = 0; i < fac[x].size(); i++)
for (int j = 0; j < fac[y].size(); j++)
add(n1 + n2 + opt * 46 * 46 + idx[fac[x][i]][fac[y][j]], u, 1);
}
int main()
{
init();
scanf("%d%d", &n1, &n2);
s = 0, t = n1 + n2 + 46 * 46 * 3 + 1;
for (int i = 1; i <= n1; i++)
{
add(s, i, 1);
scanf("%d%d%d", &a[i], &b[i], &c[i]);
work1(i, a[i], b[i], 0);
work1(i, a[i], c[i], 1);
work1(i, b[i], c[i], 2);
}
for (int i = n1 + 1; i <= n1 + n2; i++)
{
add(i, t, 1);
scanf("%d%d%d", &a[i], &b[i], &c[i]);
work2(i, a[i], b[i], 0);
work2(i, a[i], c[i], 1);
work2(i, b[i], c[i], 2);
}
dinic();
printf("%d\n", ans);
return 0;
}