題目傳送門:洛谷 P4298。
題意簡述:
給定一個 \(n\) 個點,\(m\) 條邊的簡單有向無環圖(DAG),求出它的最長反鏈,並構造方案。
最長反鏈:一張有向無環圖的最長反鏈為一個集合 \(S \subseteq V\),滿足對於 \(S\) 中的任意兩個不同的點 \(u, v \in S\)(\(u \ne v\)),\(u\) 不能到達 \(v\),\(v\) 也不能到達 \(u\),且 \(S\) 的大小盡量大。
題解:
根據 Dilworth 定理,一個 DAG 中最長反鏈的大小,等於其中最小可重鏈覆蓋大小。
最小可重鏈覆蓋:在 DAG 中選出若干條鏈,經過每個點至少一次,一個點可被一條鏈經過多次,且鏈數盡量少。
其實 Dilworth 定理描述的是:一個偏序集中的最長反鏈大小,等於其中最小不可重鏈覆蓋大小。
但是因為求出 DAG 的傳遞閉包后,DAG 也可以表示成偏序集的形式,所以相當於可重鏈覆蓋。
總之先求出 DAG 的傳遞閉包,然后求形成的偏序集上的最小不可重鏈覆蓋。
那么,最小不可重鏈覆蓋應該怎么求呢?
考慮從每個點自成一條鏈的形態出發,此時恰好有 \(n\) 條鏈。
可以發現最終答案一定是合並(首尾相接)若干條鏈形成的。考慮重新描述這個過程:
對於一個點,它在最終的鏈上,一定只有最多一個前驅,和最多一個后繼。
我們考慮把每個點拆成入點和出點,那么入點和出點應該只能匹配上最多一個點(表示前驅或者后繼)。
這似乎是二分圖匹配的形式,具體地,我們考慮:
把一個點 \(x\) 拆成兩個點:\(x_{out}\) 和 \(x_{in}\),表示出點和入點。
對於一條邊 \(x \to y\),連接 \(x_{out}\) 與 \(y_{in}\),表示原圖中 \(x\) 的出邊指向 \(y\)(這條邊是 \(y\) 的入邊)。
那么最終形成了一個二分圖,左側是所有 \(x_{out}\),右側是所有 \(x_{in}\)。而且所有邊都是連接左側的點和右側的點的。
在這個二分圖 \(G = \langle \langle V_{out}, V_{in} \rangle , E' \rangle\) 上做二分圖最大匹配:
每一個匹配邊 \(x_{out} \leftrightarrow y_{in}\) 都可以還原原圖中鏈的一條邊 \(x \to y\)。
每匹配 \(1\) 條邊,鏈的個數就減少 \(1\),則有最小鏈覆蓋的大小等於 \(n\) 減去最大匹配的大小。
繼續考慮如何從二分圖最大匹配中,構造出最長反鏈。以下部分參考了 r_64 的題解。
我們首先需要構造二分圖最大獨立集,這部分參考了「圖的最大匹配算法」這篇博客:
考慮下圖,可以求出它的其中一種最大匹配為 \(\{ \langle 2, D \rangle, \langle 3, E \rangle, \langle 4, A \rangle, \langle 5, C \rangle \}\),設最大匹配大小為 \(m\),這里 \(m = 4\):
從右側的非匹配點(這里為 \(B\),可能有多個)開始 DFS,右側的點只能走非匹配邊向左訪問,左側的點只能走匹配邊向右訪問:
可以發現 DFS 到了 \(3, 5, B, C, E\) 這些點。
我們取左側被 DFS 到的點,以及右側沒被 DFS 到的點,也就是 \(3, 5, A, D\) 這些點,記做集合 \(S\),可以證明 \(S\) 是一個最小點覆蓋。
最小點覆蓋:選取最少的點,覆蓋每條邊,也就是說每條邊的兩個端點至少有一個被選中了。
證明:
-
首先有:最小點覆蓋等於最大匹配。我們可以證明 \(|S| = m\)。
這是因為:右側的非匹配點一定都被 DFS 到了,所以在右側選取的必然是匹配點。如果一個右側的匹配點沒被選取,即它被 DFS 到了,而這只有可能是因為它在左側匹配到的點被 DFS 到了,那么左側匹配到的點就會被選上。即是:每條匹配邊的兩端點恰好會被選一個。而左側的非匹配點一定不會被 DFS 到,這是因為如果被 DFS 到了,必然會形成一條交錯路(匈牙利算法中的),不滿足最大匹配的條件。所以有且僅有匹配邊的端點會被選上,而且每條匹配邊的兩端點恰好被選一個,所以 \(\boldsymbol{|S| = m}\)。 -
\(S\) 可以覆蓋所有的邊。
我們把邊按照左右端點是否被 DFS 到,分成 \(2 \times 2 = 4\) 類。那么如果出現了左端點沒被 DFS 到,但是右端點被 DFS 到了的邊,它才不會被覆蓋。然而這是不可能的,這是因為對於一個右側被 DFS 到的點,與它相連的左側的點一定都被 DFS 到了。
然后有最大獨立集等於最小點覆蓋的補集。也就是只要選出左側沒被 DFS 到的點和右側被 DFS 到的點就行了。
在上圖中就是 \(1, 2, 4, B, C, E\) 這 \(6\) 個點。
回到 DAG 的情況(注意到我們舉的例子並不是 DAG 導出的二分圖,所以這個例子不能用來解釋最長反鏈):
令最大獨立集為 \(I\),考慮選出所有 \(x_{out}\) 和 \(x_{in}\) 都屬於 \(I\) 的點,記做集合 \(A\),它們構成一個最長反鏈。
證明:
先證 \(A\) 的確是一個反鏈:這是容易的,因為任取 \(x \in A\),\(x_{in}\) 就一定是被 DFS 到的點,而 \(x_{out}\) 一定是沒被 DFS 到的點,任何兩個 \(x, y \in A\) 之間若是有連邊就和 DFS 的過程沖突了。
首先有 \(|I| = 2n - |S| = 2n - m\),而 \(|I| - |A|\) 可以看作是滿足「\(x_{out}\) 或 \(x_{in}\) 屬於 \(I\)」的 \(x\) 的個數,顯然這樣的 \(x\) 不會超過 \(n\) 個,所以 \(|I| - |A| \le n\),所以 \(|A| \ge |I| - n = n - m\)。
但是 \(A\) 再大,也不能大過 \(n - m\),所以 \(|A| = n - m\),也就是一個最長反鏈。
總結:只要選出 \(x_{out}\) 沒被 DFS 到,且 \(x_{in}\) 被 DFS 到了的點,這些點就組成一個最長反鏈。
然后是第三問,這只要默認該點被選中,也就是刪除這個點和與其有偏序關系的所有點后,再求一次最長反鏈,如果最長反鏈的大小只減小了 \(1\),那么這個點就能在最長反鏈中,否則不能。
下面是代碼,復雜度為 \(\mathcal O (n^{3.5})\):
#include <cstdio>
#include <algorithm>
#include <bitset>
namespace Dinic {
const int Inf = 0x3f3f3f3f;
const int MN = 205, MM = 5155;
int N, S, T;
int h[MN], iter[MN], nxt[MM * 2], to[MM * 2], w[MM * 2], tot;
inline void Init(int _N) {
N = _N, tot = 1;
for (int i = 1; i <= N; ++i) h[i] = 0;
}
inline void SetST(int _S, int _T) { S = _S, T = _T; }
inline void ins(int u, int v, int x) { nxt[++tot] = h[u], to[tot] = v, w[tot] = x, h[u] = tot; }
inline void insw(int u, int v, int w1 = Inf, int w2 = 0) {
if (!u) u = S; if (!v) v = T;
ins(u, v, w1), ins(v, u, w2);
}
int lv[MN], que[MN], l, r;
inline bool Lvl() {
for (int i = 1; i <= N; ++i) lv[i] = 0;
lv[S] = 1;
que[l = r = 1] = S;
while (l <= r) {
int u = que[l++];
for (int i = h[u]; i; i = nxt[i])
if (w[i] && !lv[to[i]]) {
lv[to[i]] = lv[u] + 1;
que[++r] = to[i];
}
}
return lv[T] != 0;
}
int Flow(int u, int f) {
if (u == T) return f;
int d = 0, s = 0;
for (int &i = iter[u]; i; i = nxt[i])
if (w[i] && lv[to[i]] == lv[u] + 1) {
d = Flow(to[i], std::min(f, w[i]));
f -= d, s += d;
w[i] -= d, w[i ^ 1] += d;
if (!f) break;
}
return s;
}
inline int DoDinic() {
int Ans = 0;
while (Lvl()) {
for (int i = 1; i <= N; ++i) iter[i] = h[i];
Ans += Flow(S, Inf);
}
return Ans;
}
}
using Dinic::Init;
using Dinic::SetST;
using Dinic::insw;
using Dinic::DoDinic;
using Dinic::h;
using Dinic::nxt;
using Dinic::to;
using Dinic::w;
const int MN = 105;
int N, M, Ans;
std::bitset<101> g[MN];
int match[MN], tagl[MN], tagr[MN];
void DFS(int u) {
tagr[u] = 1;
for (int i = 1; i <= N; ++i)
if (g[i][u] && !tagl[i])
tagl[i] = 1, DFS(match[i]);
}
int main() {
scanf("%d%d", &N, &M);
for (int i = 1; i <= M; ++i) {
int x, y;
scanf("%d%d", &x, &y);
g[x][y] = 1;
}
for (int k = 1; k <= N; ++k)
for (int i = 1; i <= N; ++i)
if (g[i][k]) g[i] |= g[k];
Init(N + N + 2), SetST(N + N + 1, N + N + 2);
for (int i = 1; i <= N; ++i)
insw(0, i, 1), insw(N + i, 0, 1);
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= N; ++j)
if (g[i][j]) insw(i, N + j, 1);
Ans = N - DoDinic();
printf("%d\n", Ans);
for (int i = 1; i <= N; ++i) if (!w[4 * i - 2]) {
for (int j = h[i]; j; j = nxt[j])
if (!w[j]) { match[i] = to[j] - N; break; }
}
for (int i = 1; i <= N; ++i) if (w[4 * i]) DFS(i);
for (int i = 1; i <= N; ++i) printf("%d", !tagl[i] && tagr[i]);
puts("");
for (int u = 1; u <= N; ++u) {
static int del[MN]; int cnt = 0;
for (int i = 1; i <= N; ++i) del[i] = i == u || g[i][u] || g[u][i];
Init(N + N + 2), SetST(N + N + 1, N + N + 2);
for (int i = 1; i <= N; ++i) if (!del[i])
insw(0, i, 1), insw(N + i, 0, 1), ++cnt;
for (int i = 1; i <= N; ++i) if (!del[i])
for (int j = 1; j <= N; ++j) if (!del[j])
if (g[i][j]) insw(i, N + j, 1);
printf("%d", cnt - DoDinic() == Ans - 1);
} puts("");
return 0;
}