【题解】[CQOI2008] 传感器网络
题意
给定一张有向无环图,从中选出一棵有根树(节点编号为 \(0\sim n\),树根为 \(n\)),使得 除根节点外 所有节点的出度的最大值最小。除根节点外,依次输出每个节点的父亲,并要求 字典序最小。(\(1\le n\le 50\))
*注意:由于个人习惯,这里将节点编号重编为 \(1\sim n+1\),根节点即为 \(n+1\)。
解法
Step1. 求解该最小值
关注到「最大的最小」不难想到 二分。
思考 check
的写法:(设当前二分的值为 \(k\))
于是对于树中除根节点外所有节点的出度给出了要求,即出度必须小于等于 \(k\),同时其入度必须等于 \(1\)(因为这是棵树)。
这其实也就是对子节点和其父节点提出了数量上的要求,而确定了每组节点匹配关系即可确定一棵树,所以这道题本质上就是求「一种有要求的匹配关系」,进而想到 网络流 求解。
将网络中的点分为两类:
- 编号 \(1\sim n\):分别对应原图的除根节点外的点(\(1\sim n\));
- 编号 \(n+1\sim 2n+1\):分别对应原图的所有节点的父节点(\(1\sim n+1\))。
先根据输入的限制,从第一类向第二类连有向边,表示第二类的点可以做对应第一类的父亲。
接下来考虑数量限制:(令 \(S=0\) 为源点,\(T=2n+2\) 为汇点)
-
「入度等于 \(1\)」:于是从 \(S\) 依次向第一类的点,连一条容量为 \(1\) 的边;
-
「出度小于等于 \(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;
}