【题解】[CQOI2008] 传感器网络

题意

给定一张有向无环图,从中选出一棵有根树(节点编号为 \(0\sim n\),树根为 \(n\)),使得 除根节点外 所有节点的出度的最大值最小。除根节点外,依次输出每个节点的父亲,并要求 字典序最小。(\(1\le n\le 50\)

*注意:由于个人习惯,这里将节点编号重编为 \(1\sim n+1\),根节点即为 \(n+1\)


解法

Step1. 求解该最小值

关注到「最大的最小」不难想到 二分

思考 check 的写法:(设当前二分的值为 \(k\)

于是对于树中除根节点外所有节点的出度给出了要求,即出度必须小于等于 \(k\),同时其入度必须等于 \(1\)(因为这是棵树)。

这其实也就是对子节点和其父节点提出了数量上的要求,而确定了每组节点匹配关系即可确定一棵树,所以这道题本质上就是求「一种有要求的匹配关系」,进而想到 网络流 求解。

将网络中的点分为两类:

  1. 编号 \(1\sim n\):分别对应原图的除根节点外的点(\(1\sim n\));
  2. 编号 \(n+1\sim 2n+1\):分别对应原图的所有节点的父节点(\(1\sim n+1\))。

先根据输入的限制,从第一类向第二类连有向边,表示第二类的点可以做对应第一类的父亲。

接下来考虑数量限制:(令 \(S=0\) 为源点,\(T=2n+2\) 为汇点)

  1. 「入度等于 \(1\)」:于是从 \(S\) 依次向第一类的点,连一条容量为 \(1\) 的边;

  2. 「出度小于等于 \(k」\):于是从第二类的点(除根节点 \(2n+1\))依次向 \(T\),连一条容量为 \(k\) 的边。

    对于根节点 \(2n+1\),因为对它的出度没有限制,所以直接从它向 \(T\) 连一条 \(+\infty\) 的边即可。

此时我们跑一遍最大流,如果最大流等于 \(n\),则说明对于 \(1\sim n\) 的所有点都被匹配到了对应的父亲,故此时的 \(k\) 是可行的。


Step2. 输出方案

\(N\) 为网络中的节点数,\(M\) 为网络中的边数。

对于一般的最大流求解算法都不太好保证方案字典序最小,同时考虑到此时的时间复杂度为 Dinic 的$ \mathcal{O}(N2M)\le1002\times2600=2.6\times10^7$ 虽然不是很充裕 (但是有如下的乱搞((

我们假设节点 \(1\sim i-1\) 与其父亲已经得到了最优的匹配。

则对于节点 \(i\),从贪心的角度,我们可以从 \(1\)\(n+1\) 枚举它的父亲 \(j\)

如果我们在网络中对于 \(i\) 的出边只保留 \((i,j+n)\) 这一条,但是整张网络的最大流仍是 \(n\),则说明此时 \(j\) 作为 \(i\) 的父亲是合理的。

至此,我们得到了一个时间复杂度为 \(\mathcal{O}(n^2N^2M)\) 的算法,但由于 Dinic 跑不满,还是能通过此题。

小优化:二分

还是对于节点 \(i\),如果我们知道,如果其父亲 \(j\) 合法,那么对于 \(k(k>j)\) 作为 \(i\) 父亲也一定合法,具有单调性。

于是我们可以二分该区间的右端点,每次保留从 \(i\)\([1,mid+n]\) 的所有边,同样与暴力一样判断合法。

直至得到第一个合法的 \(j\)


实现 & 代码

为了实现方便,这里使用的直接枚举 \(i\) 的父亲的方法。

\(fa[i][j]\) 表示 \(j\) 可以尝试作为 \(i\) 的父亲(初值均置为 \(1\)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 55, MAXM = 2600, INF = 0x3f3f3f3f;
int n, S, T, N, _tot, tot = 1, head[MAXN<<1], ans[MAXN];
char s[MAXN][MAXN];
bool fa[MAXN][MAXN];
struct Edge { int v, c, nxt; } edge[MAXM<<1];
void AddEdge (int u, int v, int c) {
	edge[++tot] = {v, c, head[u]}, head[u] = tot;
	edge[++tot] = {u, 0, head[v]}, head[v] = tot;
}
int d[MAXN<<1], now[MAXN<<1];
bool bfs () {
	for (int i = 0; i <= N; i++) d[i] = 0, now[i] = head[i];
	queue<int> q; q.push(S), d[S] = 1;
	while (!q.empty()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = edge[i].nxt) {
			int v = edge[i].v, c = edge[i].c;
			if (c > 0 && !d[v]) {
				d[v] = d[u] + 1;
				if (v == T) return true;
				q.push(v);
			}
		}
	}
	return false;
}
int dinic (int u, int flow) {
	if (u == T) return flow;
	int rest = flow;
	for (int i = now[u]; i && rest; i = edge[i].nxt) {
		now[u] = i;
		int v = edge[i].v, c = edge[i].c;
		if (c > 0 && d[v] == d[u] + 1) {
			int k = dinic(v, min(rest, c));
			if (!k) d[v] = 0;
			rest -= k, edge[i].c -= k, edge[i^1].c += k;
		}
	}
	return flow - rest;
}
int MaxFlow () {
	int res = 0;
	while (bfs()) res += dinic(S, INF);
	return res;
}
bool check (int mid) {
	fill(head, head+N+1, 0);
	tot = 1;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n+1; j++)
			if (s[i][j] == 'Y' && fa[i][j])
				AddEdge(i, j+n, 1);
	for (int i = 1; i <= n; i++) AddEdge(S, i, 1);
	for (int i = 1; i <= n; i++) AddEdge(n+i, T, mid);
	AddEdge(n+n+1, T, INF);
	return MaxFlow() >= n;
}
int BinarySearch (int l, int r) {
	while (l < r) {
		int mid = l + r >> 1;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}
	return l;
}
int main () {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	memset(fa, 1, sizeof fa);
	cin >> n; S = 0, T = N = n+n+2; getchar();
	for (int i = 0; i <= n; i++) cin >> (s[i]+1);
	for (int i = 1; i <= n; i++) s[i][n+1] = s[0][i];
	int res = BinarySearch(0, n);
	for (int i = 1; i <= n; i++) {
		fill(fa[i], fa[i]+n+2, 0);
		for (int j = 1; j <= n+1; j++) if (s[i][j] == 'Y') {
			fa[i][j] = 1;
			if (check(res)) { ans[i] = j; break; }
			fa[i][j] = 0;
		}
	}
	for (int i = 1; i <= n; i++) cout << ans[i] - 1 << ' ';
	return 0;
}
posted @ 2023-09-29 15:27  CloudWings  阅读(17)  评论(0编辑  收藏  举报