归档 230502 - 230711 // 二分图

啃啃啃啃啃啃啃啃啃……


二分图

二分图总体概念不难。主要是其应用广泛,需要注意什么样的题目可以联系到二分图上来。

概念

若图 \(G\) 可将点集 \(V\) 分成两个互不相交的子集 \(X\)\(Y\),且每条边连接的两个点都满足一个在 \(X\) 中,一个在 \(Y\) 中,则称 \(G\) 为二分图。

也就是说,如果一个图有任何一种分组方式满足:把图中的点分成两组,每一组的点两两之间没有连边,那么这个图就是二分图。

举个例子:

每一组中的点两两之间没有连边,所以该图是二分图。

性质

  • 二分图的每条边连接的点属于不同的集合。

    显然。

  • 二分图中可能存在环,且长度一定为偶数。

    我们指定环中任意一个点,从该点出发,易得,经过奇数条边时到达另一个集合,反之回到该集合。因为路径是一个环,所以我们最后一定会回到起点所在集合,即经过偶数条边。

判定

通常,我们使用图的深度优先遍历每一个点 \(u\)

显然,若已知点 \(u\)\(X\) 集,那么所有与 \(u\) 有连边的点 \(v\) 一定在 \(Y\) 集(反之同理)。

当然,很多图是有环的,不免会产生 \(v\) 已经被分组的情况。若此时 \(v\) 恰好在 \(Y\) 集,皆大欢喜;若 \(v\) 也在 \(X\) 集,那么该图一定不为二分图。

由于每个点最多搜索一次,时间复杂度 \(\mathcal O(n)\)

int col[maxn];
bool DFS(int x, int c) {
	col[x] = c;
    for (auto i : g[x]) {
        if (col[i]) {
            if (col[i] == c)
                return 0;
        }
        else if (!DFS(i, 3 - c))
            return 0;
    }
    return 1;
}
int main() { DFS(1, 1); }

厚颜无耻地推销一下 题目(((

匹配

定义:对于一个二分图中的若干条边,若这些边没有任何公共点,则称这些边组成的集合 \(M\) 是数量为 \(|M|\)匹配

图中红色边展示了一个数量为 4 的匹配

容易看出,对于点 \(u\),只会存在 「有一条 \(M\) 集合内的边与 \(u\) 相连接」 和 「\(u\) 连接的边均不在 \(M\) 集合内」 两种情况。也就是说,从 \(u\) 出发的 \(M\) 集合内的边,最多有 \(1\) 条。

接下来,我们称 「有任何一条与之相连的边在匹配集合内」 的点为匹配点,「在匹配集合内的边」 为匹配边。

完备匹配

如果 \(|M|=\dfrac n2\),即 \(M\) 恰好连接了 \(1\sim n\) 所有点,我们就称匹配 \(M\)完备匹配

一个完备匹配的例子

比方说,现在我们知道一些男孩和女孩,他们之间有若干条互相喜欢的关系,我们把此关系抽象成一个二分图,如果每个人都能与自己喜欢的异性配对,那么我们认为这个关系网存在完备匹配。

显然,完备匹配存在,仅当两集合大小相等。

匈牙利算法

匈牙利算法一般用于求解 \(\max\{|M|\}\)

我们将图上满足下列条件的路径 \(P\) 称为增广路

  • \(P\) 的起点和终点均是非匹配点
  • \(P\) 的起点和终点不在二分图的同一组内
  • 路径 \(P\) 经过的边按 匹配边,匹配边,\(\cdots\) 匹配边的规律交替。

最终,\(P\) 会呈类 「\(\text Z\)」 形(值得一提的是,增广路不能经过一整个环,否则其长度将会因为二分图中只存在偶环而变为无穷大)。

显然,非匹配边比匹配边的数量始终多 \(1\)

此时,我们对 \(P\) 上匹配的状态取反。也就是说,原来的非匹配边变成匹配边,匹配边变成非匹配边。这样做相当于是在匹配边集仍然合法的情况下将匹配边集的大小扩大了 \(1\)

那么增广路经过的边按非匹配边,匹配边,\(\cdots\),非匹配边顺序交替的原因就很显而易见了。取反前,匹配边不可能连续出现;取反后,匹配边(即取反前的非匹配边)也不可能连续出现。

而匈牙利算法的主要思路,就是反复寻找增广路,直到无法找到为止。

这里就必须再提到一个性质:\(M\) 为图 \(G\) 的最大匹配,当且仅当无法在 \(M\) 的基础上找到增广路。

证明如下:

  • 有引理:对于图 \(G\) 的任意两个匹配 \(M\)\(M'\),它们的 对称差 \(M\Delta M'\) 中的每一个连通块都是一条链或一个包含边数为偶数的环。

    证明:

    根据对称差的定义,对于任意边 \(e\in M\Delta M'\)\(e\) 要么是 \(M\) 中的一条匹配边,要么是 \(M'\) 中的一条匹配边,但不同时被 \(M\)\(M'\) 包含。

    因为在同一个匹配中,任意两条匹配边不存在公共顶点,所以对于任意与 \(e\) 有公共顶点的匹配边 \(e'\)\(e\)\(e'\) 必然来自两个不同的匹配。

    由此可得,对于任意匹配点 \(u\)\(u\) 的度数为 \(1\)\(2\)

    所以,对称差中的每一个连通块都是链或环。

    对于其中的环,所有相邻的边必定来自不同的匹配,所以环包含的边数为偶数。

  • 必要性:当 \(M\) 为最大匹配时,无法找到 \(M\) 的增广路。

    我们已经知道了,找到某匹配的增广路 \(P\) 并将其匹配状态取反,可以使匹配大小加一。

    如果 \(M\) 存在增广路,则我们可以将其取反,得到一个比 \(M\) 大小更大的匹配。与 \(M\) 是最大匹配矛盾。

    所以一定不存在 \(M\) 的增广路。

  • 充分性:如果不存在 \(M\) 的增广路,\(M\)\(G\) 的最大匹配。

    \(M'\) 是一个比 \(M\) 更大的匹配。

    由引理得:

    在它们的对称差 \(M\Delta M'\) 中,连通块为链或环。

    其中,环包含边的数量为偶数,所以必然有同样多的边来自 \(M\)\(M'\)。所以我们可以忽视这些环。

    由于 \(|M|<|M'|\),存在至少一条链 \(L\),且 \(|L|=k-1\),包含 \(k\)\(M\) 中的边,\(k+1\) 条来自于 \(M'\) 的边。

    显然,\(L\) 就是一条 \(M\) 的增广路,所以我们必然可以找到一条 \(M\) 的增广路,命题成立。

对于 「寻找增广路」 这个过程,我们使用 DFS 算法实现。

对于点 \(x\),若与 \(x\) 有连边的点 \(y\) 可匹配上 \(x\),需要满足下列两个条件之一:

  • \(y\) 是非匹配点,此时 \(x\to y\) 构成一条增广路,非匹配边的数量已经比匹配边数量多 \(1\)
  • \((u,y)\) 是已匹配边,且 \((u,v)\) 是未匹配但合法的边,此时 \(x\to y\to u\to v\) 构成一条增广路。

在实现中,我们依次令 \(1\sim n\)所有的非匹配点 作为起始点 \(x\) 尝试找到任何一条增广路。当碰到任意非匹配点时结束(增广路判定:起点与终点均为非匹配点),否则向 该匹配点匹配的点 继续搜索。

也就是说,一层 DFS 会寻找一条非匹配边并作为起点,产生以下两种行为:

  1. 该非匹配边终点为非匹配点,以该匹配边结束增广路。
  2. 经过该非匹配边后还能再找到一条匹配边(若情况 1 不满足,显然一定能找到这样一条边),则在终点进行下一层 DFS,寻找下一条非匹配边。

时间复杂度为 \(\mathcal O(n^2+nm)\),但一般二分图题目的 \(X\)\(Y\) 部间的连边偏稠密,所以简化为 \(\mathcal O(nm)\)

bool Find(int x) {
	vis[x] = now; // 时间戳标记
	for (auto i : g[x]) {
		if (vis[i] == now) // 不经过访问过的 i
			continue;
		if (!mat[i] /* 非匹配点,即终点 */ ||
			(vis[mat[i]] != now /* mat[i] 未访问过,可以经过 */
			&& Find(mat[i]) /* 可找到增广路 */)) {
			mat[i] = x; // 匹配
			return 1;
		}
	}
	return 0;
}
inline int Hungary(int n) {
	int res = 0;
	for (int i = 1; i <= n; ++i) {
		++now;
		res += Find(i);
	}
	return res;
}

一般来说,二分图题目对点、边、分组方法和匹配范围的识别较为模糊。但一般的二分图题目都会有一些特点:

  • 结点能分为两组,且各组内结点间没有连边
  • 每个结点只能与一条边匹配

有时候,题目要求判定是否存在 「完备匹配」,也就是说,\(ans=n\)。即任意一次 find(i) 返回 false 时,完备匹配不存在。

最后给出与匈牙利算法有关的两个问题:

  1. 最小点覆盖:给定一个二分图,求出一个最小的点集,使得这个点集发出的所有边可以覆盖整个二分图。

    定理:该点集的大小是二分图的最大匹配包含的边数。

  2. 最大独立集:给定一个无向图,求出一个最大的点集,使得该点集中的所有点两两之间没有边相连。

    定理:当该无向图是二分图时,最大独立集的大小等于 \(n\) 减去最大匹配数。

    证明:由于最小点覆盖可以覆盖所有边,故不存在两个点,使得它们不属于最小点覆盖且有连边。

    所以,当去掉最小点覆盖后,剩余点两两之间没有连边。因为最小点覆盖大小就是最大匹配大小,故原命题成立。


注意二分图的点和边是可以互相转化的,即,若发现信息集中在点上,也可以用二分图解决。匹配边的数量即最终参与匹配的点数较多的一方的匹配点数量。

对于二分图建图的一个判断方式是,找冲突。找到彼此之间有冲突的两方,连边。这样就能建出二分图。当然要保证两方之间没有交集。

所谓冲突,就是我们通常理解中的选了一个就不能选另一个。因此也可以通过冲突存在的形式思考建图方式。


A. 棋盘上的骑士

http://222.180.160.110:1024/contest/3699/problem/1

这道题就是我们提到的边转化为点的情况。

为棋盘上的每个格子编号。骑士走的是日字,所以我们要把周围每个格子日字方向八个格子都连上边。

那要怎么将所有 \(n\times n\) 个格子分为有冲突的两方呢?注意到日字连接的两个格子一定奇偶性相异,故我们以奇偶性分类。

被挖掉的格子无视即可,不能连任何边,否则该边都有可能被选。然后跑一个最大匹配就行。

namespace XSC062 {
using namespace fastIO;
const int maxn = 205;
const int maxm = 1e5 + 5;
const int fx[] = { 1, 1, -1, -1, 2, 2, -2, -2 };
const int fy[] = { 2, -2, 2, -2, 1, -1, 1, -1 };
int a[maxn][maxn];
int mat[maxm], vis[maxm];
std::vector<int> g[maxm];
int n, m, x, y, now, cnt, tot;
bool Find(int x) {
	vis[x] = now;
	for (auto i : g[x]) {
		if (vis[i] == now)
			continue;
		if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
			mat[i] = x;
			return 1;
		}
	}
	return 0;
}
inline int Hungary(int n) {
	int res = 0;
	for (int i = 1; i <= n; ++i)
		++now, res += Find(i);
	return res;
}
inline void add(int x, int y) {
	g[x].push_back(y);
	return;
}
int main() {
	read(n), read(m);
	while (m--) {
		read(x), read(y);
		a[x][y] = -1;
	}
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			if (a[i][j] != -1 && !((i + j) & 1))
				a[i][j] = ++cnt;
		}
	}
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			if (a[i][j] == -1 || !((i + j) & 1))
				continue;
			a[i][j] = ++tot;
			for (int k = 0; k < 8; ++k) {
				int nx = i + fx[k];
				int ny = j + fy[k];
				if (nx < 1 || ny < 1 || nx > n ||
					ny > n || a[nx][ny] == -1)
					continue;
				add(a[i][j], a[nx][ny] + n * n);
			}
		}
	}
	print(cnt + tot - Hungary(tot), '\n');
	return 0;
}
} // namespace XSC062

B. 火力网

http://222.180.160.110:1024/contest/3699/problem/2

这里用到了二分图在地图上一个较为常用的分组方法。

找到每行每列的「连通块」,满足在行上的连通块任意放一个炮台,炮台覆盖连通块内所有点,所有点均被连通块覆盖,列范围内同理。为连通块分别编号,则样例的情况可转化如下:

/* 对于行 */        /* 对于列 */
  1 - 2 2            1 - 5 6
  3 3 3 3            1 3 5 6
  - - 4 4            - - 5 6
  5 5 5 5            2 4 5 6

若一个格子为空地,则将其在行 / 列范围内所属的连通块连边。这样,我们在选取该格后,就相当于选取了这条边,由于匹配边不共点,所以合法。

namespace XSC062 {
using namespace fastIO;
const int maxn = 25;
const int maxm = 1e5 + 5;
char a[maxn][maxn];
int mat[maxm], vis[maxm];
std::vector<int> g[maxm];
int n, x, y, now, cnt1, cnt2;
int t[maxn][maxn], p[maxn][maxn];
bool Find(int x) {
	vis[x] = now;
	for (auto i : g[x]) {
		if (vis[i] == now)
			continue;
		if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
			mat[i] = x;
			return 1;
		}
	}
	return 0;
}
inline int Hungary(int n) {
	int res = 0;
	for (int i = 1; i <= n; ++i)
		++now, res += Find(i);
	return res;
}
inline void add(int x, int y) {
	g[x].push_back(y);
	return;
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i)
		scanf("%s", a[i] + 1);
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			if (a[i][j] == 'X')
				continue;
			if (a[i][j - 1] != '.')
				t[i][j] = ++cnt1;
			else t[i][j] = t[i][j - 1];
		}
	}
	for (int j = 1; j <= n; ++j) {
		for (int i = 1; i <= n; ++i) {
			if (a[i][j] == 'X')
				continue;
			if (a[i - 1][j] != '.')
				p[i][j] = ++cnt2;
			else p[i][j] = p[i - 1][j];
		}
	}
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			if (a[i][j] == '.')
				add(t[i][j], p[i][j] + cnt1);
		}
	}
	print(Hungary(cnt1), '\n');
	return 0;
}
} // namespace XSC062

C. 超级英雄 Hero

http://222.180.160.110:1024/contest/3699/problem/3

分析冲突。每个锦囊只能被一道题使用,一道题只能使用一个锦囊,故考虑将锦囊和题连边。题目要求连续解题最多,故以题目进行匈牙利。

最后输出匹配数组即可。

namespace XSC062 {
using namespace fastIO;
const int maxn = 2e4 + 5;
int n, m, x, y, now, t;
int mat[maxn], vis[maxn];
std::vector<int> g[maxn];
bool Find(int x) {
	vis[x] = now;
	for (auto i : g[x]) {
		if (vis[i] == now)
			continue;
		if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
			mat[i] = x, mat[x] = i;
			return 1;
		}
	}
	return 0;
}
inline int Hungary(int n) {
	int res = 0;
	for (int i = 1; i <= n; ++i) {
		++now;
		if (Find(i))
			++res;
		else break;
	}
	return res;
}
inline void add(int x, int y) {
	g[x].push_back(y);
	return;
}
int main() {
	read(n), read(m);
	for (int i = 1; i <= m; ++i) {
		read(x), read(y);
		add(i, x + m + 1);
		add(i, y + m + 1);
	}
	print(t = Hungary(m), '\n');
	for (int i = 1; i <= t; ++i)
		print(mat[i] - m - 1, '\n');
	return 0;
}
} // namespace XSC062

KM 算法

还没写…… 咕咕咕


A. Ants

https://vjudge.net/contest/554888#problem/A

板板题。把黑蚂蚁和白蚂蚁按欧几里得距离连边后 KM 即可。

namespace XSC062 {
using namespace fastIO;
typedef double db;
const db inf = 1e18;
const db eps = 1e-5;
const int maxn = 205;
int n, now;
db g[maxn][maxn];
db u[maxn], up[maxn];
int vis[maxn], mat[maxn];
int a[maxn][2], b[maxn][2];
inline db max(db x, db y) {
	return x > y ? x : y;
}
inline db min(db x, db y) {
	return x < y ? x : y;
}
inline bool eq(db x, db y) {
	return fabs(x - y) <= eps;
}
bool Find(int x) {
	vis[x] = now;
	for (int i = n + 1; i <= 2 * n; ++i) {
		if (vis[i] == now)
			continue;
		if (eq(u[x] + u[i], g[x][i])) {
			vis[i] = now;
			if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
				mat[i] = x;
				return 1;
			}
		}
		else {
			up[i] = min(up[i],
				u[x] + u[i] - g[x][i]);
		}
	}
	return 0;
}
inline void Solve(void) {
	for (int i = 1; i <= n; ++i) {
		u[i] = -inf;
		for (int j = n + 1; j <= 2 * n; ++j)
			u[i] = max(u[i], g[i][j]);
	}
	for (int i = 1; i <= n; ++i) {
		for (;;) {
			++now;
			for (int j = n + 1; j <= 2 * n; ++j)
				up[j] = inf;
			if (Find(i))
				break;
			db Delta = inf;
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] != now)
					Delta = min(Delta, up[j]);
			}			for (int j = 1; j <= n; ++j) {
				if (vis[j] == now)
					u[j] -= Delta;
			}
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] == now)
					u[j] += Delta;
			}
		}
	}
	return;
}
inline db dist(int x1, int y1, int x2, int y2) {
	return sqrt((db)(x1 - x2) * (x1 - x2) +
					(y1 - y2) * (y1 - y2));
}
int main() {
	read(n);
	for (int i = 1; i <= n; ++i)
		read(a[i][0]), read(a[i][1]);
	for (int i = 1; i <= n; ++i)
		read(b[i][0]), read(b[i][1]);
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			g[j][i + n] = -dist(a[i][0], a[i][1],
							b[j][0], b[j][1]);
		}
	}
	Solve();
	for (int i = n + 1; i <= 2 * n; ++i)
		print(mat[i], '\n');
	return 0;
}
} // namespace XSC062

B. 奔小康赚大钱

https://vjudge.net/contest/554888#problem/B

板板题。把居民和房子连边即可。

namespace XSC062 {
using namespace fastIO;
const int maxn = 605;
const int inf = 0x3f3f3f3f;
int n, now, res;
int g[maxn][maxn];
int u[maxn], up[maxn];
int vis[maxn], mat[maxn];
inline int max(int x, int y) {
	return x > y ? x : y;
}
inline int min(int x, int y) {
	return x < y ? x : y;
}
bool Find(int x) {
	vis[x] = now;
	for (int i = n + 1; i <= 2 * n; ++i) {
		if (vis[i] == now)
			continue;
		if (u[x] + u[i] == g[x][i]) {
			vis[i] = now;
			if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
				mat[i] = x;
				return 1;
			}
		}
		else {
			up[i] = min(up[i],
				u[x] + u[i] - g[x][i]);
		}
	}
	return 0;
}
inline void Solve(void) {
	for (int i = 1; i <= 2 * n; ++i)
		mat[i] = u[i] = 0;
	for (int i = 1; i <= n; ++i) {
		u[i] = -inf;
		for (int j = n + 1; j <= 2 * n; ++j)
			u[i] = max(u[i], g[i][j]);
	}
	for (int i = 1; i <= n; ++i) {
		for (;;) {
			++now;
			for (int j = n + 1; j <= 2 * n; ++j)
				up[j] = inf;
			if (Find(i))
				break;
			int Delta = inf;
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] != now)
					Delta = min(Delta, up[j]);
			}
			for (int j = 1; j <= n; ++j) {
				if (vis[j] == now)
					u[j] -= Delta;
			}
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] == now)
					u[j] += Delta;
			}
		}
	}
	return;
}
int main() {
	while(read(n)) {
		res = 0;
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j)
				read(g[i][j + n]);
		}
		Solve();
		for (int i = n + 1; i <= 2 * n; ++i)
			res += g[mat[i]][i];
		print(res, '\n');
	}
	return 0;
}
} // namespace XSC062

C. Going Home

https://vjudge.net/contest/554888#problem/C

板板题。把人和房子按曼哈顿距离连边即可。

namespace XSC062 {
using namespace fastIO;
const int maxn = 205;
const int inf = 0x3f3f3f3f;
int g[maxn][maxn];
char s[maxn][maxn];
int u[maxn], up[maxn];
int h, w, n, m, now, res;
int vis[maxn], mat[maxn];
int a[maxn][2], b[maxn][2];
inline int max(int x, int y) {
	return x > y ? x : y;
}
inline int min(int x, int y) {
	return x < y ? x : y;
}
bool Find(int x) {
	vis[x] = now;
	for (int i = n + 1; i <= 2 * n; ++i) {
		if (vis[i] == now)
			continue;
		if (u[x] + u[i] == g[x][i]) {
			vis[i] = now;
			if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
				mat[i] = x;
				return 1;
			}
		}
		else {
			up[i] = min(up[i],
				u[x] + u[i] - g[x][i]);
		}
	}
	return 0;
}
inline void Solve(void) {
	for (int i = 1; i <= 2 * n; ++i)
		mat[i] = u[i] = 0;
	for (int i = 1; i <= n; ++i) {
		u[i] = -inf;
		for (int j = n + 1; j <= 2 * n; ++j)
			u[i] = max(u[i], g[i][j]);
	}
	for (int i = 1; i <= n; ++i) {
		for (;;) {
			++now;
			for (int j = n + 1; j <= 2 * n; ++j)
				up[j] = inf;
			if (Find(i))
				break;
			int Delta = inf;
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] != now)
					Delta = min(Delta, up[j]);
			}
			for (int j = 1; j <= n; ++j) {
				if (vis[j] == now)
					u[j] -= Delta;
			}
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] == now)
					u[j] += Delta;
			}
		}
	}
	return;
}
inline int abs(int x) {
	return x >= 0 ? x : -x;
}
inline int dist(int x1, int y1, int x2, int y2) {
	return abs(x1 - x2) + abs(y1- y2);
}
inline void Init(void) {
	res = n = m = 0;
	memset(g, 0, sizeof (g));
	return;
}
int main() {
	scanf("%d %d", &h, &w);
	while (h || w) {
		Init();
		for (int i = 1; i <= h; ++i) {
			scanf("%s", s[i] + 1);
			for (int j = 1; j <= w; ++j) {
				if (s[i][j] == 'H')
					a[++n][0] = i, a[n][1] = j;
				else if (s[i][j] == 'm')
					b[++m][0] = i, b[m][1] = j;
			}
		}
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j) {
				g[i][j + n] = -dist(a[i][0],
					a[i][1], b[j][0], b[j][1]);
			}
		}
		Solve();
		for (int i = n + 1; i <= 2 * n; ++i)
			res += g[mat[i]][i];
		print(-res, '\n');
		scanf("%d %d", &h, &w);
	}
	return 0;
}
} // namespace XSC062

D. Cyclic Tour

https://vjudge.net/contest/554888#problem/D

题意在讲什么啊,看了半天看不懂。

给定一个有向图,找到若干个互不相交的环覆盖整个图,使得所有环上边权和最小,若找不到方案输出 -1。

我们知道与这道题相类似的最小路径覆盖问题可以用二分图 + 拆点来解决。那么这里我们也可以小小地拆一拆点。把一个点拆成两个,一个作为起点,一个作为终点,两个点之间连双向边,这样该图就和原图等价。

拆出来起点之间没有边,拆出来的终点之间也没有边,所以原图是二分图。

不难发现,假设原图中的环上共有 \(x\) 个点、\(x\) 条边,那么拆点后就会有 \(2\times x\) 个点和 \(2\times x\) 条边,其中 \(x\) 条边是点 \(i\) 连向点 \(i'\)(或反之)的边。

需要匹配到剩余的实边(而非自己连向自己的虚边)共有 \(x\) 条,左右部节点都有 \(x\) 个,考虑设虚边边权为正无穷,进行最小权完美匹配(点和自己连边的操作保证了一定能找到解,不会进入死循环)。

那么什么时候无解呢?当算法不得不选中虚边时,就说明找不到环了。所以我们判一下匹配有没有包含正无穷边即可。

记得判重边!

namespace XSC062 {
using namespace fastIO;
const int maxn = 205;
const int inf = 0x3f3f3f3f;
int g[maxn][maxn];
int u[maxn], up[maxn];
int vis[maxn], mat[maxn];
int n, m, x, y, now, res, w;
inline int max(int x, int y) {
	return x > y ? x : y;
}
inline int min(int x, int y) {
	return x < y ? x : y;
}
bool Find(int x) {
	vis[x] = now;
	for (int i = n + 1; i <= 2 * n; ++i) {
		if (vis[i] == now)
			continue;
		if (u[x] + u[i] == g[x][i]) {
			vis[i] = now;
			if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
				mat[i] = x;
				return 1;
			}
		}
		else {
			up[i] = min(up[i],
				u[x] + u[i] - g[x][i]);
		}
	}
	return 0;
}
inline void Solve(void) {
	for (int i = 1; i <= 2 * n; ++i)
		mat[i] = u[i] = 0;
	for (int i = 1; i <= n; ++i) {
		u[i] = -inf;
		for (int j = n + 1; j <= 2 * n; ++j)
			u[i] = max(u[i], g[i][j]);
	}
	for (int i = 1; i <= n; ++i) {
		for (;;) {
			++now;
			for (int j = n + 1; j <= 2 * n; ++j)
				up[j] = inf;
			if (Find(i))
				break;
			int Delta = inf;
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] != now)
					Delta = min(Delta, up[j]);
			}
			for (int j = 1; j <= n; ++j) {
				if (vis[j] == now)
					u[j] -= Delta;
			}
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] == now)
					u[j] += Delta;
			}
		}
	}
	return;
}
int main() {
	while (read(n)) {
		read(m);
		res = 0;
		for (int i = 1; i <= 2 * n; ++i) {
			for (int j = 1; j <= 2 * n; ++j) 
				g[i][j] = -inf;
		}
		while (m--) {
			read(x), read(y), read(w);
			g[x][y + n] = max(g[x][y + n], -w);
		}
		Solve();
		for (int i = n + 1; i <= 2 * n; ++i) {
			if (g[mat[i]][i] <= -inf) {
				puts("-1");
				goto NoSol;
			}
			res += -g[mat[i]][i];
		}
		print(res, '\n');
		NoSol: ;
	}
	return 0;
}
} // namespace XSC062

E. Tour

https://vjudge.net/contest/554888#problem/E

把上一题输入方式和数据范围改一改就好了。

namespace XSC062 {
using namespace fastIO;
const int maxn = 405;
const int inf = 0x3f3f3f3f;
int g[maxn][maxn];
int u[maxn], up[maxn];
int vis[maxn], mat[maxn];
int T, n, m, x, y, now, res, w;
inline int max(int x, int y) {
	return x > y ? x : y;
}
inline int min(int x, int y) {
	return x < y ? x : y;
}
bool Find(int x) {
	vis[x] = now;
	for (int i = n + 1; i <= 2 * n; ++i) {
		if (vis[i] == now)
			continue;
		if (u[x] + u[i] == g[x][i]) {
			vis[i] = now;
			if (!mat[i] || (vis[mat[i]] != now
							&& Find(mat[i]))) {
				mat[i] = x;
				return 1;
			}
		}
		else {
			up[i] = min(up[i],
				u[x] + u[i] - g[x][i]);
		}
	}
	return 0;
}
inline void Solve(void) {
	for (int i = 1; i <= 2 * n; ++i)
		mat[i] = u[i] = 0;
	for (int i = 1; i <= n; ++i) {
		u[i] = -inf;
		for (int j = n + 1; j <= 2 * n; ++j)
			u[i] = max(u[i], g[i][j]);
	}
	for (int i = 1; i <= n; ++i) {
		for (;;) {
			++now;
			for (int j = n + 1; j <= 2 * n; ++j)
				up[j] = inf;
			if (Find(i))
				break;
			int Delta = inf;
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] != now)
					Delta = min(Delta, up[j]);
			}
			for (int j = 1; j <= n; ++j) {
				if (vis[j] == now)
					u[j] -= Delta;
			}
			for (int j = n + 1;
							j <= 2 * n; ++j) {
				if (vis[j] == now)
					u[j] += Delta;
			}
		}
	}
	return;
}
int main() {
	read(T);
	while(T--) {
		read(n), read(m);
		res = 0;
		for (int i = 1; i <= 2 * n; ++i) {
			for (int j = 1; j <= 2 * n; ++j) 
				g[i][j] = -inf;
		}
		while (m--) {
			read(x), read(y), read(w);
			g[x][y + n] = max(g[x][y + n], -w);
		}
		Solve();
		for (int i = n + 1; i <= 2 * n; ++i) {
			if (g[mat[i]][i] <= -inf) {
				puts("-1");
				goto NoSol;
			}
			res += -g[mat[i]][i];
		}
		print(res, '\n');
		NoSol: ;
	}
	return 0;
}
} // namespace XSC062

D. 导弹防御塔

http://222.180.160.110:1024/contest/3699/problem/4

GM 说的好哇(指 毛病多,一会儿分钟一会儿秒)。

寻找冲突。一个敌人只能被一炮打死,故考虑

但一个塔可以打很多炮,考虑拆点。

posted @ 2023-05-09 20:48  XSC062  阅读(17)  评论(0编辑  收藏  举报